feat:访客
This commit is contained in:
1 parent
f8d8e00c48
commit
bd069f200a
7 files changed
+31
-1146
No files matched your search
+1
-1
@@ -549,7 +549,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "visitor/invitationRecord",
|
||||
"path": "visitor/invitation-record",
|
||||
"style": {
|
||||
"navigationBarTitleText": "邀约记录"
|
||||
}
|
||||
|
||||
@@ -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>({})
|
||||
|
||||
+27
-1
@@ -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" })
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user