This commit is contained in:
zjf committed 2025-06-05 13:42:33 +08:00
1 parent dc67e56537
commit 8eee595c01
1 file changed
+484
+484
View File
@@ -0,0 +1,484 @@
<template>
<view class="visitor-list">
<!-- 顶部标签页 -->
<view class="tabs">
<view v-for="(tab, index) in tabs" :key="index" class="tab-item" :class="{ active: currentTab === index }" @tap="switchTab(index)">
<text>{{ tab }}</text>
</view>
</view>
<!-- 访客记录列表 -->
<scroll-view
scroll-y
class="list-content"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="loadMore"
>
<view v-for="item in visitorList" :key="item.id" class="visitor-item" @tap="goToDetail(item)">
<view class="item-header">
<text class="visit-time">{{ formatTime(item.vstartDate, item.vendDate) }}</text>
<text :class="['status-tag', getStatusClass(item.checkStatus)]">
{{ getStatusText(item.checkStatus) }}
</text>
</view>
<view class="item-content">
<view class="info-row">
<text class="label">接待人:</text>
<text class="value">{{ item.userName }}</text>
</view>
<view class="info-row">
<text class="label">联系方式:</text>
<text class="value">{{ item.mobile }}</text>
</view>
<view class="info-row">
<text class="label">预约时间:</text>
<text class="value">{{ formatCreateTime(item.createTime) }}</text>
</view>
<view v-if="item.description" class="info-row">
<text class="label">备注:</text>
<text class="value">{{ item.description }}</text>
</view>
<view v-if="item.checkReason" class="info-row check-reason">
<text class="label">审核意见:</text>
<text class="value">{{ item.checkReason }}</text>
</view>
</view>
<view class="item-footer" v-if="canCancel(item.checkStatus) || canVisit(item.checkStatus)">
<view class="btn-group">
<view v-if="canVisit(item.checkStatus)" class="visit-btn" @tap.stop="handleVisit(item)">已到访</view>
<view v-if="canCancel(item.checkStatus)" class="cancel-btn" @tap.stop="handleCancel(item)">取消</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading && !refreshing" class="loading">加载中...</view>
<view v-if="!loading && !hasMore" class="no-more">没有更多数据了</view>
<view v-if="!loading && visitorList.length === 0" class="empty">暂无访客记录</view>
</scroll-view>
<tabbar :tabBarList="tabBarList" :current="tabbarStore.current" />
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from "vue"
import visitorApi from "@/sheep/api/integrated/visitors"
import { onLoad, onShow } from "@dcloudio/uni-app"
import useTabbarStore from "@/sheep/stores/tabbar"
import tabbar from "@/sheep/components/tabbar.vue"
const tabbarStore = useTabbarStore()
const tabBarList = computed(() => tabbarStore.tabbarList)
onLoad(() => {
const app = getApp()
app._onPageLoad()
// 初始化 tabbar
tabbarStore.initTabbar()
})
onShow(() => {
// 设置当前页面索引
const currentIndex = tabBarList.value.findIndex((item) => item.pagePath === "/pages/integrated/visitor/myvisitor")
if (currentIndex !== -1) {
tabbarStore.setCurrent(currentIndex)
}
})
// 标签页配置
const tabs = ["全部", "待审核", "待到访", "已到访", "已取消"]
const currentTab = ref(0)
// 列表数据
const visitorList = ref<any[]>([])
const loading = ref(false)
const refreshing = ref(false)
const hasMore = ref(true)
const current = ref(1)
const size = 10
// 格式化访问时间
const formatTime = (start: string, end: string) => {
if (!start || !end) return ""
const startDate = new Date(start)
const endDate = new Date(end)
return `${startDate.getMonth() + 1}/${startDate.getDate()} ${startDate.getHours()}:00-${endDate.getHours()}:00`
}
// 格式化创建时间
const formatCreateTime = (time: string) => {
if (!time) return ""
const date = new Date(time)
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes()}`
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
"1": "待审核",
"2": "待到访",
"0": "已到访",
"5": "已取消",
}
return statusMap[status] || status
}
// 获取状态样式
const getStatusClass = (status: string) => {
const statusMap: Record<string, string> = {
"1": "status-pending",
"2": "status-waiting",
"0": "status-visited",
"5": "status-cancelled",
}
return statusMap[status] || ""
}
// 切换标签页
const switchTab = (index: number) => {
if (currentTab.value === index) return
currentTab.value = index
resetList()
getVisitorList()
}
// 重置列表状态
const resetList = () => {
current.value = 1
visitorList.value = []
hasMore.value = true
}
// 获取访客记录列表
const getVisitorList = async () => {
if (loading.value || (!hasMore.value && !refreshing.value)) return
loading.value = true
try {
const params: any = {
current: current.value,
size,
}
// 添加状态筛选
if (currentTab.value > 0) {
const statusMap = ["", "1", "2", "0", "5"]
params.checkStatus = statusMap[currentTab.value]
}
const { code, data }: any = await visitorApi.getVisitorList(params)
if (code === 0 && data) {
if (refreshing.value || current.value === 1) {
visitorList.value = data.records || []
} else {
visitorList.value.push(...(data.records || []))
}
hasMore.value = (data.records || []).length === size
if (hasMore.value) {
current.value++
}
}
} catch (error) {
console.error("获取访客记录失败:", error)
uni.showToast({
title: "获取数据失败",
icon: "none",
})
} finally {
loading.value = false
refreshing.value = false
}
}
// 下拉刷新
const onRefresh = async () => {
refreshing.value = true
resetList()
await getVisitorList()
}
// 上拉加载更多
const loadMore = () => {
if (!loading.value && hasMore.value) {
getVisitorList()
}
}
// 判断是否可以取消
const canCancel = (status: string) => {
return status === "1" || status === "2" || status === "5"
}
// 判断是否可以标记到访
const canVisit = (status: string) => {
return status === "2"
}
// 修改取消预约方法
const handleCancel = (item: any) => {
if (!item.id) return
uni.showModal({
title: "提示",
content: "确定要取消此次预约吗?",
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: "取消中...",
mask: true,
})
const { code, msg }: any = await visitorApi.cancelVisitor(item.id)
uni.hideLoading()
if (code === 0) {
uni.showToast({
title: "取消成功",
icon: "success",
})
// 刷新列表
resetList()
getVisitorList()
} else {
uni.showToast({
title: msg || "取消失败",
icon: "none",
})
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: "取消失败",
icon: "none",
})
}
}
},
})
}
// 处理到访
const handleVisit = (item: any) => {
if (!item.id) return
uni.showModal({
title: "提示",
content: "确认访客已到访?",
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: "处理中...",
mask: true,
})
const { code, msg }: any = await visitorApi.haveVisitor(item.id)
uni.hideLoading()
if (code === 0) {
uni.showToast({
title: "操作成功",
icon: "success",
})
// 刷新列表
resetList()
getVisitorList()
} else {
uni.showToast({
title: msg || "操作失败",
icon: "none",
})
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: "操作失败",
icon: "none",
})
}
}
},
})
}
// 跳转到详情页
const goToDetail = (item: any) => {
uni.navigateTo({
url: `/pages/integrated/visitor/visitorDetail?id=${item.id}&token=${item.token}`,
})
}
onMounted(() => {
getVisitorList()
})
</script>
<style lang="scss" scoped>
.visitor-list {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
.tabs {
display: flex;
background-color: #fff;
padding: 20rpx 0;
position: sticky;
top: 0;
z-index: 100;
border-bottom: 1rpx solid #eee;
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
&.active {
color: #1777ff;
font-weight: bold;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #1777ff;
border-radius: 2rpx;
}
}
}
}
.list-content {
flex: 1;
height: calc(100vh - 100rpx);
padding: 20rpx;
box-sizing: border-box;
}
.visitor-item {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.visit-time {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.status-tag {
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.status-pending {
background-color: #fff7e6;
color: #fa8c16;
}
&.status-waiting {
background-color: #e6f7ff;
color: #1890ff;
}
&.status-visited {
background-color: #f6ffed;
color: #52c41a;
}
&.status-cancelled {
background-color: #fff1f0;
color: #f5222d;
}
}
}
.item-content {
.info-row {
display: flex;
margin-bottom: 12rpx;
font-size: 26rpx;
.label {
color: #999;
width: 140rpx;
}
.value {
color: #333;
flex: 1;
}
&.check-reason {
.value {
color: #f5222d;
}
}
}
}
.item-footer {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #eee;
.btn-group {
display: flex;
justify-content: flex-end;
gap: 20rpx;
.visit-btn,
.cancel-btn {
padding: 10rpx 30rpx;
border-radius: 30rpx;
font-size: 26rpx;
}
.visit-btn {
background-color: #1777ff;
color: #fff;
}
.cancel-btn {
background-color: #fff;
color: #666;
border: 1rpx solid #ddd;
}
}
}
}
.loading,
.no-more,
.empty {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 26rpx;
}
}
</style>