feat:访客

This commit is contained in:
weiwei committed 2025-09-22 18:21:43 +08:00
1 parent f8d8e00c48
commit bd069f200a
7 files changed
+31 -1146

No files matched your search

+1 -1
View File
@@ -549,7 +549,7 @@
}
},
{
"path": "visitor/invitationRecord",
"path": "visitor/invitation-record",
"style": {
"navigationBarTitleText": "邀约记录"
}
+1 -1
View File
@@ -79,7 +79,7 @@ const alertList = {
1: { color: "#f5a623", text: "访客未接受邀约" },
2: { color: "#2db7f5", text: "访客待到访" },
3: { color: "#52c41a", text: "访客拒绝了邀约" },
5: { color: "#f5222d", text: "访客取消了邀约" },
5: { color: "#f5222d", text: "您取消了邀约" },
}
const detail = ref<any>({})
@@ -65,7 +65,10 @@
<!-- <view>{{ v.name }}</view> -->
<view>访客手机号:{{ v.mobile }}</view>
<!-- <view v-if="v.reasonValue">拜访原因:{{ visitorReasonList.find((item) => item.id == v.reasonValue)?.title || "" }}</view> -->
<tm-button class="px-1" size="xs" @click.stop="goDetail(v)">查看详情</tm-button>
<view>
<tm-button class="px-1 mr-2" size="xs" @click.stop="goDetail(v)">查看详情</tm-button>
<tm-button v-if="v.checkStatus != '5'" class="px-1" size="xs" color="danger" @click.stop="cancel(v)">取消</tm-button>
</view>
</view>
<!-- <view class="flex justify-end items-center gap-2"> -->
<!-- <view v-if="v.checkStatus == 1" class="flex items-center gap-2">
@@ -162,6 +165,29 @@ const goDetail = (v: any) => {
router.jump("/pages/integrated/visitor/detail", { id: v.id, token: v.token })
}
// 取消邀约
const cancel = async (v: any) => {
if (!v.id) return uni.showToast({ title: "id不存在", icon: "error" })
uni.showModal({
title: "提示",
content: "是否确定取消预约",
confirmColor: "#0094ff",
success: async (res) => {
if (!res.confirm) return
try {
uni.showLoading({ title: "取消邀约中...", mask: true })
const { code, msg } = await invitationApi.updateCheckStatus({ id: v.id, checkStatus: 5 })
if (code != 0) return uni.showToast({ title: msg || "取消失败", icon: "error" })
pageRef.value.reload(false)
uni.showToast({ title: "取消成功", icon: "success" })
} finally {
uni.hideLoading()
}
},
})
}
const goInvitation = () => {
uni.redirectTo({ url: "/pages/integrated/visitor/invitation" })
}
+2 -2
View File
@@ -225,7 +225,7 @@ const confirmSubmit = async () => {
uni.showToast({ title: "提交成功", icon: "success" })
resetForm()
confirmModalShow.value = false
uni.navigateTo({ url: "/pages/integrated/visitor/invitationRecord" })
uni.navigateTo({ url: "/pages/integrated/visitor/invitation-record" })
} catch (error: any) {
uni.showToast({ title: error.message || "提交失败", icon: "none" })
}
@@ -247,6 +247,6 @@ const resetForm = () => {
}
const goInvitationRecord = () => {
uni.redirectTo({ url: "/pages/integrated/visitor/invitationRecord" })
uni.redirectTo({ url: "/pages/integrated/visitor/invitation-record" })
}
</script>
-236
View File
@@ -1,236 +0,0 @@
<template>
<view class="visitor-detail">
<view class="detail-header">
<view class="visitor-info">
<text class="visitor-name">{{ detail.visitorName }}</text>
<text class="visitor-phone">{{ detail.visitorPhone }}</text>
</view>
<view class="status-tag" :class="getStatusClass(detail.status)">{{ getStatusText(detail.status) }}</view>
</view>
<view class="detail-content">
<view class="info-row">
<text class="label">来访目的:</text>
<text class="value">{{ getPurposeLabel(detail.purpose) }}</text>
</view>
<view class="info-row">
<text class="label">被访人:</text>
<text class="value">{{ detail.hostName }}</text>
</view>
<view class="info-row">
<text class="label">来访时间:</text>
<text class="value">{{ formatVisitTime(detail.visitDate, detail.visitTime) }}</text>
</view>
<view v-if="detail.remark" class="info-row">
<text class="label">备注说明:</text>
<text class="value">{{ detail.remark }}</text>
</view>
</view>
<view v-if="detail.rejectReason" class="reject-section">
<view class="reject-title">拒绝原因</view>
<view class="reject-content">{{ detail.rejectReason }}</view>
</view>
<view class="detail-footer">
<text class="apply-time">申请时间:{{ formatTime(detail.createTime) }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import meetingRoomApi from '@/sheep/api/meetingRoom'
const detail = ref<any>({})
const purposeOptions = [
{ value: '1', label: '商务洽谈' },
{ value: '2', label: '技术交流' },
{ value: '3', label: '面试' },
{ value: '4', label: '参观访问' },
{ value: '5', label: '其他' }
]
const timeOptions = [
{ value: '1', label: '上午 (9:00-12:00)' },
{ value: '2', label: '下午 (14:00-18:00)' },
{ value: '3', label: '全天 (9:00-18:00)' }
]
function getPurposeLabel(purpose: string) {
const option = purposeOptions.find(item => item.value === purpose)
return option ? option.label : '其他'
}
function getStatusText(status: string) {
const statusMap: Record<string, string> = {
'1': '待审核',
'2': '已通过',
'3': '已拒绝',
'4': '已取消',
'5': '已完成'
}
return statusMap[status] || '未知'
}
function getStatusClass(status: string) {
const classMap: Record<string, string> = {
'1': 'status-pending',
'2': 'status-approved',
'3': 'status-rejected',
'4': 'status-cancelled',
'5': 'status-completed'
}
return classMap[status] || 'status-pending'
}
function formatVisitTime(date: string, time: string) {
const timeLabel = timeOptions.find(item => item.value === time)?.label || ''
return `${date} ${timeLabel}`
}
function formatTime(time: string) {
if (!time) return ''
return new Date(time).toLocaleString()
}
onLoad(async (options: any) => {
const id = options.id
if (!id) return
try {
const response: any = await meetingRoomApi.getVisitorDetail(id)
if (response.code === 0 && response.data) {
detail.value = response.data
}
} catch (error) {
console.error('获取访客详情失败:', error)
uni.showToast({ title: '获取详情失败', icon: 'none' })
}
})
</script>
<style lang="scss" scoped>
.visitor-detail {
padding: 32rpx;
background: #f5f5f5;
min-height: 100vh;
}
.detail-header {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
.visitor-info {
.visitor-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.visitor-phone {
font-size: 26rpx;
color: #666;
}
}
.status-tag {
padding: 12rpx 24rpx;
border-radius: 24rpx;
font-size: 26rpx;
&.status-pending {
background: #fff3e0;
color: #f57c00;
}
&.status-approved {
background: #e8f5e8;
color: #388e3c;
}
&.status-rejected {
background: #ffebee;
color: #d32f2f;
}
&.status-cancelled {
background: #f5f5f5;
color: #999;
}
&.status-completed {
background: #e3f2fd;
color: #1976d2;
}
}
}
.detail-content {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.info-row {
display: flex;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
color: #666;
width: 160rpx;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333;
flex: 1;
}
}
}
.reject-section {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.reject-title {
font-size: 30rpx;
font-weight: bold;
color: #d32f2f;
margin-bottom: 16rpx;
}
.reject-content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
}
.detail-footer {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
.apply-time {
font-size: 26rpx;
color: #999;
}
}
</style>
-421
View File
@@ -1,421 +0,0 @@
<template>
<view class="visitor-container">
<!-- 访客申请表单 -->
<view class="visitor-form">
<view class="form-title">访客申请</view>
<view class="form-item">
<view class="label">访客姓名</view>
<input v-model="visitorName" placeholder="请输入访客姓名" class="name-input" />
</view>
<view class="form-item">
<view class="label">访客手机号</view>
<input v-model="visitorPhone" placeholder="请输入访客手机号" class="phone-input" />
</view>
<view class="form-item">
<view class="label">来访目的</view>
<picker @change="onPurposeChange" :value="purposeIndex" :range="purposeOptions" range-key="label">
<view class="picker">{{ purposeOptions[purposeIndex].label }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">来访时间</view>
<picker mode="date" @change="onDateChange" :value="visitDate">
<view class="picker">{{ visitDate || "请选择来访日期" }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">来访时段</view>
<picker @change="onTimeChange" :value="timeIndex" :range="timeOptions" range-key="label">
<view class="picker">{{ timeOptions[timeIndex].label }}</view>
</picker>
</view>
<view class="form-item">
<view class="label">被访人</view>
<input v-model="hostName" placeholder="请输入被访人姓名" class="host-input" />
</view>
<view class="form-item">
<view class="label">备注说明</view>
<textarea v-model="remark" placeholder="请输入备注说明(选填)" class="remark-textarea" maxlength="200" />
<view class="char-count">{{ remark.length }}/200</view>
</view>
<button @click="submitVisitor" class="submit-btn" :disabled="!canSubmit">提交申请</button>
</view>
<!-- 访客记录列表 -->
<view class="visitor-list">
<view class="list-title">访客记录</view>
<view v-if="visitorList.length === 0" class="empty-tip"> 暂无访客记录 </view>
<view v-else>
<view v-for="item in visitorList" :key="item.id" class="visitor-item" @click="viewDetail(item)">
<view class="item-header">
<view class="visitor-info">
<text class="visitor-name">{{ item.visitorName }}</text>
<text class="visitor-phone">{{ item.visitorPhone }}</text>
</view>
<view class="status-tag" :class="getStatusClass(item.status)">{{ getStatusText(item.status) }}</view>
</view>
<view class="item-content">
<view class="purpose">来访目的:{{ getPurposeLabel(item.purpose) }}</view>
<view class="host">被访人:{{ item.hostName }}</view>
<view class="time">来访时间:{{ formatVisitTime(item.visitDate, item.visitTime) }}</view>
</view>
<view class="item-footer">
<text class="apply-time">申请时间:{{ formatTime(item.createTime) }}</text>
<text v-if="item.remark" class="has-remark">有备注</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from "vue"
import meetingRoomApi from "@/sheep/api/meetingRoom"
const purposeOptions = [
{ value: "1", label: "商务洽谈" },
{ value: "2", label: "技术交流" },
{ value: "3", label: "面试" },
{ value: "4", label: "参观访问" },
{ value: "5", label: "其他" },
]
const timeOptions = [
{ value: "1", label: "上午 (9:00-12:00)" },
{ value: "2", label: "下午 (14:00-18:00)" },
{ value: "3", label: "全天 (9:00-18:00)" },
]
const visitorName = ref("")
const visitorPhone = ref("")
const purposeIndex = ref(0)
const visitDate = ref("")
const timeIndex = ref(0)
const hostName = ref("")
const remark = ref("")
const visitorList = ref<any[]>([])
const canSubmit = computed(() => {
return visitorName.value.trim() && visitorPhone.value.trim() && visitDate.value && hostName.value.trim()
})
function onPurposeChange(e: any) {
purposeIndex.value = e.detail.value
}
function onDateChange(e: any) {
visitDate.value = e.detail.value
}
function onTimeChange(e: any) {
timeIndex.value = e.detail.value
}
function getPurposeLabel(purpose: string) {
const option = purposeOptions.find((item) => item.value === purpose)
return option ? option.label : "其他"
}
function getStatusText(status: string) {
const statusMap: Record<string, string> = {
"1": "待审核",
"2": "已通过",
"3": "已拒绝",
"4": "已取消",
"5": "已完成",
}
return statusMap[status] || "未知"
}
function getStatusClass(status: string) {
const classMap: Record<string, string> = {
"1": "status-pending",
"2": "status-approved",
"3": "status-rejected",
"4": "status-cancelled",
"5": "status-completed",
}
return classMap[status] || "status-pending"
}
function formatVisitTime(date: string, time: string) {
const timeLabel = timeOptions.find((item) => item.value === time)?.label || ""
return `${date} ${timeLabel}`
}
function formatTime(time: string) {
if (!time) return ""
return new Date(time).toLocaleDateString()
}
async function submitVisitor() {
if (!canSubmit.value) {
uni.showToast({ title: "请填写完整信息", icon: "none" })
return
}
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(visitorPhone.value)) {
uni.showToast({ title: "请输入正确的手机号", icon: "none" })
return
}
try {
uni.showLoading({ title: "提交中..." })
const data = {
visitorName: visitorName.value.trim(),
visitorPhone: visitorPhone.value.trim(),
purpose: purposeOptions[purposeIndex.value].value,
visitDate: visitDate.value,
visitTime: timeOptions[timeIndex.value].value,
hostName: hostName.value.trim(),
remark: remark.value.trim(),
}
const response: any = await meetingRoomApi.submitVisitor(data)
uni.hideLoading()
if (response.code === 0) {
uni.showToast({ title: "提交成功", icon: "success" })
// 清空表单
visitorName.value = ""
visitorPhone.value = ""
purposeIndex.value = 0
visitDate.value = ""
timeIndex.value = 0
hostName.value = ""
remark.value = ""
// 刷新列表
loadVisitorList()
} else {
uni.showToast({ title: response.msg || "提交失败", icon: "none" })
}
} catch (error) {
uni.hideLoading()
uni.showToast({ title: "提交失败", icon: "none" })
}
}
function viewDetail(item: any) {
uni.navigateTo({
url: `/pages/integrated/visitor/detail?id=${item.id}`,
})
}
async function loadVisitorList() {
try {
const response: any = await meetingRoomApi.getVisitorList({
current: 1,
size: 20,
})
if (response.code === 0 && response.data) {
visitorList.value = response.data.records || []
}
} catch (error) {
console.error("加载访客记录失败:", error)
}
}
onMounted(() => {
loadVisitorList()
})
</script>
<style lang="scss" scoped>
.visitor-container {
padding: 24rpx;
background: #f5f5f5;
min-height: 100vh;
}
.visitor-form {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.form-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.form-item {
margin-bottom: 24rpx;
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
}
.picker {
height: 80rpx;
line-height: 80rpx;
padding: 0 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
}
.name-input,
.phone-input,
.host-input {
height: 80rpx;
padding: 0 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
}
.remark-textarea {
width: 100%;
height: 120rpx;
padding: 24rpx;
background: #f7f8fa;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.char-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
}
.submit-btn {
width: 100%;
height: 88rpx;
background: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 32rpx;
&:disabled {
background: #ccc;
}
}
}
.visitor-list {
background: #fff;
border-radius: 16rpx;
padding: 32rpx;
.list-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 60rpx 0;
}
.visitor-item {
border-bottom: 1rpx solid #f0f0f0;
padding: 24rpx 0;
&:last-child {
border-bottom: none;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.visitor-info {
.visitor-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.visitor-phone {
font-size: 26rpx;
color: #666;
}
}
.status-tag {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&.status-pending {
background: #fff3e0;
color: #f57c00;
}
&.status-approved {
background: #e8f5e8;
color: #388e3c;
}
&.status-rejected {
background: #ffebee;
color: #d32f2f;
}
&.status-cancelled {
background: #f5f5f5;
color: #999;
}
&.status-completed {
background: #e3f2fd;
color: #1976d2;
}
}
}
.item-content {
margin-bottom: 16rpx;
.purpose,
.host,
.time {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
line-height: 1.4;
}
}
.item-footer {
display: flex;
justify-content: space-between;
align-items: center;
.apply-time {
font-size: 24rpx;
color: #999;
}
.has-remark {
font-size: 24rpx;
color: #007aff;
background: #e3f2fd;
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
}
}
}
</style>
-484
View File
@@ -1,484 +0,0 @@
<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>