<script setup lang="ts">
|
import { computed, ref } from 'vue'
|
import { onLoad } from '@dcloudio/uni-app'
|
import {
|
cancelMyAppointment,
|
deleteMyAppointment,
|
fetchMyAppointmentDetail,
|
} from '@/api/appointment'
|
import type {
|
ApprovalFlowNodeItem,
|
ApprovalFlowTaskUserItem,
|
MiniApprovalDetailVisitorItem,
|
MiniMyAppointmentDetailItem,
|
} from '@/types/appointment'
|
import { normalizeVisitReasonText } from '@/utils/visitReason'
|
|
definePage({
|
name: 'review',
|
layout: 'default',
|
style: {
|
navigationStyle: 'custom',
|
},
|
})
|
|
const router = useRouter()
|
const sysInfo = uni.getSystemInfoSync()
|
const statusBarHeight = sysInfo.statusBarHeight || 0
|
const navBarHeight = 44
|
|
type AppointmentStatusView = 'reviewing' | 'pending_effect' | 'effective' | 'cancelled' | 'invalid' | 'rejected'
|
type ApprovalStepStatus = 'completed' | 'current' | 'rejected' | 'pending'
|
|
interface ApprovalStepItem {
|
title: string
|
time: string
|
status: ApprovalStepStatus
|
}
|
|
interface ReviewVisitor {
|
id: number
|
name: string
|
gender: 'male' | 'female'
|
idCard: string
|
company: string
|
plate: string
|
}
|
|
const NODE_CODE_ORDER: Record<string, number> = {
|
LEVEL1_HANDLER: 1,
|
LEVEL2_DEPT_LEADER: 2,
|
LEVEL3_LOCAL_DEPT_LEADER: 3,
|
LEVEL4_VISIT_DEPT: 4,
|
LEVEL5_VISIT_DEPT_LEADER: 5,
|
}
|
|
const NODE_CODE_TITLE: Record<string, string> = {
|
LEVEL1_HANDLER: '经办人',
|
LEVEL2_DEPT_LEADER: '部门负责人',
|
LEVEL3_LOCAL_DEPT_LEADER: '属地管理部门领导',
|
LEVEL4_VISIT_DEPT: '访客主管部门',
|
LEVEL5_VISIT_DEPT_LEADER: '访客主管部门领导',
|
}
|
|
const showCancelModal = ref(false)
|
const showDeleteModal = ref(false)
|
const loading = ref(false)
|
const actionLoading = ref(false)
|
const appointmentStatus = ref<AppointmentStatusView>('reviewing')
|
const detail = ref<MiniMyAppointmentDetailItem | null>(null)
|
const visitors = ref<ReviewVisitor[]>([])
|
const approvalSteps = ref<ApprovalStepItem[]>([])
|
const rejectionTipText = ref('')
|
const query = ref<{
|
orderId: number | null
|
orderUuid: string
|
}>({
|
orderId: null,
|
orderUuid: '',
|
})
|
|
function handleBack() {
|
router.back()
|
}
|
|
function handleCancel() {
|
if (!detail.value?.cancelable) {
|
return
|
}
|
showCancelModal.value = true
|
}
|
|
function handleDelete() {
|
if (!detail.value?.deletable) {
|
return
|
}
|
showDeleteModal.value = true
|
}
|
|
async function confirmCancel() {
|
if (!query.value.orderId || !query.value.orderUuid || actionLoading.value) {
|
showCancelModal.value = false
|
return
|
}
|
actionLoading.value = true
|
showCancelModal.value = false
|
try {
|
await cancelMyAppointment(query.value.orderId, query.value.orderUuid)
|
uni.showToast({ title: '已取消预约', icon: 'success' })
|
setTimeout(() => {
|
router.back()
|
}, 600)
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '取消预约失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
actionLoading.value = false
|
}
|
}
|
|
async function confirmDelete() {
|
if (!query.value.orderId || !query.value.orderUuid || actionLoading.value) {
|
showDeleteModal.value = false
|
return
|
}
|
actionLoading.value = true
|
showDeleteModal.value = false
|
try {
|
await deleteMyAppointment(query.value.orderId, query.value.orderUuid)
|
uni.showToast({ title: '已删除记录', icon: 'success' })
|
setTimeout(() => {
|
router.back()
|
}, 600)
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '删除记录失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
actionLoading.value = false
|
}
|
}
|
|
function handleRebook() {
|
const orderId = detail.value?.orderId || query.value.orderId
|
const orderUuid = detail.value?.orderUuid || query.value.orderUuid
|
if (!orderId || !orderUuid) {
|
uni.redirectTo({ url: '/pages/appointment/index' })
|
return
|
}
|
uni.redirectTo({
|
url: `/pages/appointment/index?rebookOrderId=${orderId}&rebookOrderUuid=${encodeURIComponent(orderUuid)}`,
|
})
|
}
|
|
function handleViewVisitorCode() {
|
const orderUuid = detail.value?.orderUuid || query.value.orderUuid
|
if (!orderUuid) {
|
uni.showToast({
|
title: '缺少访客码参数',
|
icon: 'none',
|
})
|
return
|
}
|
uni.navigateTo({
|
url: `/pages/visitorCode/index?uuid=${encodeURIComponent(orderUuid)}`,
|
})
|
}
|
|
const appointmentIcons = {
|
male: '/static/appointment/man.png',
|
female: '/static/appointment/woman.png',
|
company: '/static/appointment/company.png',
|
}
|
|
const isGrayHeader = computed(() => ['cancelled', 'invalid', 'rejected'].includes(appointmentStatus.value))
|
const reviewHeaderText = computed(() => appointmentStatus.value === 'rejected' ? '审批驳回' : '审批中...')
|
const visitReasonText = computed(() => {
|
const reasonCode = (detail.value?.visitReasonCode || '').trim().toUpperCase()
|
if (reasonCode === 'OTHER') {
|
const otherText = (detail.value?.visitReasonOther || '').trim()
|
if (otherText) {
|
return normalizeVisitReasonText(`其他:${otherText}`)
|
}
|
}
|
return detail.value?.visitReasonText || '-'
|
})
|
const visitTimeText = computed(() => detail.value?.visitTimeText || '-')
|
const visitedUserText = computed(() => {
|
const name = (detail.value?.visitedUserName || '').trim()
|
const dept = (detail.value?.visitedDeptName || '').trim()
|
if (!name) {
|
return '-'
|
}
|
if (!dept) {
|
return name
|
}
|
return `${name}(${dept})`
|
})
|
const visitTypeText = computed(() => detail.value?.vehicleEntry ? '人员+车辆' : '人员')
|
const createTimeText = computed(() => formatDateTime(detail.value?.createTime) || '-')
|
const approvedTimeText = computed(() => findLatestActionTime(['APPROVED']) || '-')
|
const cancelledTimeText = computed(() => findLatestActionTime(['CANCELLED']) || '-')
|
|
function formatDateTime(value?: string | null) {
|
if (!value) {
|
return ''
|
}
|
return value.replace('T', ' ').slice(0, 16)
|
}
|
|
function formatFlowTime(value?: string | null) {
|
if (!value) {
|
return ''
|
}
|
const normalized = value.replace('T', ' ').trim()
|
const match = normalized.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/)
|
if (!match) {
|
return formatDateTime(value)
|
}
|
const month = Number(match[2])
|
const day = Number(match[3])
|
const hh = match[4]
|
const mm = match[5]
|
return `${month}-${day} ${hh}:${mm}`
|
}
|
|
function normalizeVisitorGender(gender?: string | null): ReviewVisitor['gender'] {
|
return gender === '女' || gender === 'FEMALE' ? 'female' : 'male'
|
}
|
|
function stripBracketSuffix(value?: string | null) {
|
return (value || '').replace(/[((].*?[))]/g, '').trim()
|
}
|
|
function flattenNodeUsers(node?: ApprovalFlowNodeItem | null) {
|
if (!node) {
|
return [] as ApprovalFlowTaskUserItem[]
|
}
|
return (node.groups || []).flatMap(group => group.users || [])
|
}
|
|
function findNodeActionUser(node?: ApprovalFlowNodeItem | null, taskStatuses?: string[]) {
|
const matchedUsers = flattenNodeUsers(node)
|
.filter((user) => {
|
if (taskStatuses && taskStatuses.length > 0 && !taskStatuses.includes(user.taskStatus)) {
|
return false
|
}
|
return Boolean(user.actionTime || user.comment)
|
})
|
.sort((left, right) => (left.actionTime || '').localeCompare(right.actionTime || ''))
|
return matchedUsers.length > 0 ? matchedUsers[matchedUsers.length - 1] : undefined
|
}
|
|
function mapApprovalStepStatus(
|
node: ApprovalFlowNodeItem | undefined,
|
rejectedAnchorIndex: number,
|
currentIndex: number,
|
): ApprovalStepStatus {
|
if (!node) {
|
return 'pending'
|
}
|
if (node.nodeStatus === 'APPROVED') {
|
return 'completed'
|
}
|
if (node.nodeStatus === 'PENDING') {
|
return 'current'
|
}
|
if (node.nodeStatus === 'REJECTED') {
|
if (rejectedAnchorIndex >= 0 && currentIndex > rejectedAnchorIndex) {
|
return 'pending'
|
}
|
return 'rejected'
|
}
|
return 'pending'
|
}
|
|
function findRejectedAnchorIndex(nodes: ApprovalFlowNodeItem[]) {
|
return nodes.findIndex((node) => {
|
const rejectedUser = findNodeActionUser(node, ['REJECTED'])
|
return Boolean(rejectedUser)
|
})
|
}
|
|
function buildApprovalSteps(nodes?: ApprovalFlowNodeItem[] | null) {
|
const rows = (nodes || [])
|
.filter(node => Boolean(node?.nodeCode) && Object.prototype.hasOwnProperty.call(NODE_CODE_ORDER, node.nodeCode))
|
.slice()
|
.sort((left, right) => NODE_CODE_ORDER[left.nodeCode] - NODE_CODE_ORDER[right.nodeCode])
|
const rejectedAnchorIndex = findRejectedAnchorIndex(rows)
|
return rows.map((node, index) => {
|
const hiddenAfterRejected = rejectedAnchorIndex >= 0 && index > rejectedAnchorIndex
|
return {
|
title: stripBracketSuffix(node.nodeName) || NODE_CODE_TITLE[node.nodeCode] || node.nodeCode,
|
time: hiddenAfterRejected ? '' : formatFlowTime(findNodeActionUser(node)?.actionTime),
|
status: mapApprovalStepStatus(node, rejectedAnchorIndex, index),
|
} satisfies ApprovalStepItem
|
})
|
}
|
|
function mapVisitors(rows?: MiniApprovalDetailVisitorItem[] | null) {
|
return (rows || []).map((visitor, index) => ({
|
id: visitor.visitorId || index + 1,
|
name: visitor.name || '来访人',
|
gender: normalizeVisitorGender(visitor.gender),
|
idCard: visitor.idCardMasked || '-',
|
company: visitor.companyName || '-',
|
plate: visitor.plateNo || '',
|
}))
|
}
|
|
function findLatestActionTime(taskStatuses: string[]) {
|
const actionTimes = (detail.value?.flowTree || [])
|
.flatMap(node => flattenNodeUsers(node))
|
.filter(user => taskStatuses.includes(user.taskStatus) && user.actionTime)
|
.map(user => user.actionTime as string)
|
.sort()
|
const actionTime = actionTimes.length > 0 ? actionTimes[actionTimes.length - 1] : ''
|
return formatDateTime(actionTime)
|
}
|
|
function resolveAppointmentStatus(detailData: MiniMyAppointmentDetailItem): AppointmentStatusView {
|
if (detailData.status === 'REJECTED') {
|
return 'rejected'
|
}
|
if (detailData.status === 'CANCELLED') {
|
return 'cancelled'
|
}
|
if (detailData.displayStatus === 'INVALID') {
|
return 'invalid'
|
}
|
if (detailData.displayStatus === 'EFFECTIVE') {
|
return 'effective'
|
}
|
if (detailData.status === 'APPROVED' && detailData.displayStatus === 'PENDING_VISIT') {
|
return 'pending_effect'
|
}
|
return 'reviewing'
|
}
|
|
function findRejectComment(nodes?: ApprovalFlowNodeItem[] | null) {
|
for (const node of nodes || []) {
|
const rejectedUser = findNodeActionUser(node, ['REJECTED'])
|
if (rejectedUser?.comment) {
|
return rejectedUser.comment
|
}
|
}
|
return ''
|
}
|
|
function stepArrowClass(step: ApprovalStepItem) {
|
return step.status === 'completed' ? '' : 'opacity-40'
|
}
|
|
function stepNumberClass(step: ApprovalStepItem) {
|
if (step.status === 'completed') {
|
return 'text-[#0068FF]'
|
}
|
if (step.status === 'current' || step.status === 'rejected') {
|
return 'text-white'
|
}
|
return 'text-[#989898]'
|
}
|
|
function stepCircleClass(step: ApprovalStepItem) {
|
if (step.status === 'completed') {
|
return 'bg-white'
|
}
|
if (step.status === 'current') {
|
return 'bg-[#3B72FF]'
|
}
|
if (step.status === 'rejected') {
|
return 'bg-[#FF4D4F]'
|
}
|
return 'bg-white'
|
}
|
|
function stepLabelClass(step: ApprovalStepItem) {
|
return step.status === 'pending' ? 'text-white/80' : 'text-white'
|
}
|
|
function showStepRipple(step: ApprovalStepItem) {
|
return step.status === 'current' || step.status === 'rejected'
|
}
|
|
async function loadDetail() {
|
if (!query.value.orderId || !query.value.orderUuid) {
|
uni.showToast({
|
title: '缺少预约参数',
|
icon: 'none',
|
})
|
return
|
}
|
loading.value = true
|
try {
|
const detailData = await fetchMyAppointmentDetail(query.value.orderId, query.value.orderUuid)
|
detail.value = detailData
|
appointmentStatus.value = resolveAppointmentStatus(detailData)
|
approvalSteps.value = buildApprovalSteps(detailData.flowTree)
|
rejectionTipText.value = findRejectComment(detailData.flowTree)
|
visitors.value = mapVisitors(detailData.visitors)
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '加载预约详情失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
loading.value = false
|
}
|
}
|
|
onLoad((options) => {
|
if (options?.status) {
|
appointmentStatus.value = options.status as AppointmentStatusView
|
}
|
query.value.orderId = options?.orderId ? Number(options.orderId) : null
|
query.value.orderUuid = options?.orderUuid ? decodeURIComponent(options.orderUuid) : ''
|
void loadDetail()
|
})
|
</script>
|
|
<template>
|
<view class="min-h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col pb-[160rpx]">
|
|
<!-- 顶部背景图 (被覆盖层) -->
|
<view class="absolute left-0 top-0 z-0 h-[480rpx] w-full overflow-hidden">
|
<image :src="isGrayHeader ? '/static/review/bg-grey.png' : '/static/review/bg-t.png'" class="w-full h-full" mode="aspectFill" />
|
</view>
|
|
<!-- 整体内容背景图 (通过 z-index 层级置于顶部背景之上,利用背景图自身弧度产生遮盖效果) -->
|
<view class="absolute left-0 top-[410rpx] w-full z-0 pointer-events-none" style="height: calc(100% - 380rpx);">
|
<image src="/static/review/contentbg.png" class="w-full h-full" mode="scaleToFill" />
|
</view>
|
|
<!-- 自定义导航栏 -->
|
<view class="relative z-50 w-full shrink-0" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
|
<!-- 固定在顶部的导航栏 -->
|
<view class="fixed left-0 top-0 w-full z-50 overflow-hidden" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }">
|
<!-- 截取顶部背景图作为 navBar 背景,防止滚动时内容透出 -->
|
<image :src="isGrayHeader ? '/static/review/bg-grey.png' : '/static/review/bg-t.png'" class="absolute left-0 top-0 w-full z-0" mode="widthFix" />
|
|
<view class="flex items-center justify-between w-full absolute left-0 z-10"
|
:style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }">
|
<view class="flex h-full w-[88rpx] items-center justify-center relative z-10" @click="handleBack">
|
<wd-icon name="arrow-left" size="40rpx" color="#ffffff" />
|
</view>
|
<view class="absolute left-0 w-full text-center text-[34rpx] font-medium text-white z-0">
|
预约记录
|
</view>
|
<view class="w-[88rpx]"></view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 内容区 -->
|
<view class="relative z-10 mt-[56rpx] px-[24rpx] flex-1">
|
|
<!-- 审批状态区 -->
|
<view class="px-[24rpx] mb-[108rpx]" v-if="['reviewing', 'rejected'].includes(appointmentStatus)">
|
<view class="flex items-center mb-[10rpx]">
|
<image v-if="appointmentStatus === 'reviewing'" src="/static/review/chapter.png" class="w-[40rpx] h-[40rpx] mr-[8rpx]" mode="aspectFit" />
|
<image v-else-if="appointmentStatus === 'rejected'" src="/static/review/Pending.png" class="w-[40rpx] h-[40rpx] mr-[8rpx]" mode="aspectFit" />
|
<text class="text-[32rpx] text-white font-bold">{{ reviewHeaderText }}</text>
|
</view>
|
|
<!-- 分割线 -->
|
<view class="w-full h-[2rpx] bg-white/20 mb-[24rpx]"></view>
|
|
<!-- 进度条模拟 -->
|
<view class="flex items-start justify-between relative px-[16rpx]">
|
<template v-for="(step, index) in approvalSteps" :key="`${step.title}-${index}`">
|
<view class="flex flex-col items-center relative z-10 w-[60rpx]">
|
<view
|
class="w-[40rpx] h-[40rpx] rounded-full flex items-center justify-center mb-[24rpx] relative"
|
:class="stepCircleClass(step)"
|
>
|
<template v-if="showStepRipple(step)">
|
<view class="ripple-wave absolute w-full h-full rounded-full border-[4rpx] border-solid border-white box-border"></view>
|
<view class="ripple-wave ripple-delay absolute w-full h-full rounded-full border-[4rpx] border-solid border-white box-border"></view>
|
<view class="absolute w-[40rpx] h-[40rpx] rounded-full border-[2rpx] border-solid border-white top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 box-border z-0"></view>
|
<view class="absolute w-[52rpx] h-[52rpx] rounded-full border-[4rpx] border-solid border-white/40 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 box-border z-0"></view>
|
</template>
|
<wd-icon v-if="step.status === 'rejected'" name="close" size="24rpx" color="#ffffff" class="z-10" />
|
<text v-else class="text-[24rpx] font-bold z-10" :class="stepNumberClass(step)">{{ index + 1 }}</text>
|
</view>
|
<text class="text-[20rpx] whitespace-nowrap text-center leading-[28rpx]" :class="stepLabelClass(step)">{{ step.title }}</text>
|
<text v-if="step.time" class="text-white/80 text-[18rpx] whitespace-nowrap mt-[4rpx]">{{ step.time }}</text>
|
</view>
|
|
<view v-if="index < approvalSteps.length - 1" class="flex-1 flex justify-center pt-[0rpx]">
|
<image src="/static/review/arrow.png" class="w-[64rpx] h-[40rpx]" :class="stepArrowClass(step)" mode="aspectFit" />
|
</view>
|
</template>
|
</view>
|
|
<!-- 驳回提示信息 -->
|
<view v-if="appointmentStatus === 'rejected' && rejectionTipText" class="mt-[16rpx] ml-[-16rpx] w-[702rpx] bg-[rgba(255,230,189,0.6)] border-[2rpx] border-solid border-[#E73232] rounded-[16rpx] px-[20rpx] py-[12rpx] flex items-start box-border relative -left-[8rpx]">
|
<wd-icon name="info-circle" size="28rpx" color="#F5222D" class="mr-[12rpx] mt-[2rpx] shrink-0" />
|
<text class="text-[#F5222D] text-[26rpx] leading-[36rpx] break-all">{{ rejectionTipText }}</text>
|
</view>
|
</view>
|
|
<!-- 待生效状态区 -->
|
<view class="px-[24rpx] mb-[32rpx]" v-if="appointmentStatus === 'pending_effect'">
|
<view class="flex items-center mb-[20rpx]">
|
<image src="/static/review/Pending.png" class="w-[40rpx] h-[40rpx] mr-[8rpx]" mode="aspectFit" />
|
<text class="text-[32rpx] text-white font-bold">待生效...</text>
|
</view>
|
|
<!-- 分割线 -->
|
<view class="w-full h-[2rpx] bg-white/30 mb-[24rpx]"></view>
|
|
<!-- 有效期 -->
|
<view class="flex items-center mb-[24rpx]">
|
<view class="bg-[#52C41A] w-full rounded-full h-[60rpx] px-[18rpx] flex items-center justify-center">
|
<text class="text-white text-[28rpx] font-medium">有效期: {{ visitTimeText }}</text>
|
</view>
|
</view>
|
|
<!-- 查看访客码按钮 -->
|
<view class="flex justify-center mb-[85rpx]">
|
<view class="border-[2rpx] border-solid border-white/60 rounded-full w-[280rpx] h-[64rpx] flex items-center justify-center bg-white/20 transition-opacity active:opacity-70" @click="handleViewVisitorCode">
|
<wd-icon name="view" size="32rpx" color="#ffffff" class="mr-[12rpx]" />
|
<text class="text-white text-[28rpx]">查看访客码</text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 生效中状态区 -->
|
<view class="px-[24rpx] mb-[32rpx]" v-if="appointmentStatus === 'effective'">
|
<view class="flex items-center mb-[20rpx]">
|
<image src="/static/review/success.png" class="w-[40rpx] h-[40rpx] mr-[8rpx]" mode="aspectFit" />
|
<text class="text-[32rpx] text-white font-bold">生效中</text>
|
</view>
|
|
<!-- 分割线 -->
|
<view class="w-full h-[2rpx] bg-white/30 mb-[24rpx]"></view>
|
|
<!-- 有效期 -->
|
<view class="flex items-center mb-[24rpx]">
|
<view class="bg-[#52C41A] w-full rounded-full h-[60rpx] px-[18rpx] flex items-center justify-center">
|
<text class="text-white text-[28rpx] font-medium">有效期: {{ visitTimeText }}</text>
|
</view>
|
</view>
|
|
<!-- 查看访客码按钮 -->
|
<view class="flex justify-center mb-[85rpx]">
|
<view class="border-[2rpx] border-solid border-white/60 rounded-full w-[280rpx] h-[64rpx] flex items-center justify-center bg-white/20 transition-opacity active:opacity-70" @click="handleViewVisitorCode">
|
<wd-icon name="view" size="32rpx" color="#ffffff" class="mr-[12rpx]" />
|
<text class="text-white text-[28rpx]">查看访客码</text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 已失效/已取消 状态区 -->
|
<view class="px-[24rpx] mb-[148rpx] mt-[76rpx] flex flex-col items-center" v-if="['cancelled', 'invalid'].includes(appointmentStatus)">
|
<image src="/static/review/cancel.png" class="w-[40rpx] h-[40rpx] mb-[16rpx]" mode="aspectFit" />
|
<text class="text-[32rpx] text-white font-bold">{{ appointmentStatus === 'cancelled' ? '已取消' : '已失效' }}</text>
|
</view>
|
|
<!-- 拜访信息标题 -->
|
<view class="flex items-end mb-[26rpx] px-[8rpx]">
|
<text class="text-[32rpx] text-[#000000] font-[500]">拜访信息</text>
|
<text class="text-[24rpx] text-[#676767] font-[350] ml-[16rpx] mb-[4rpx]">您要见的人</text>
|
</view>
|
|
<!-- 拜访信息卡片 -->
|
<view class="bg-white rounded-[16rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full mb-[48rpx] px-[40rpx] py-[12rpx] box-border">
|
<view class="flex flex-col">
|
<view class="flex justify-between items-center h-[96rpx] border-b1">
|
<text class="text-[#000000] text-[28rpx] font-normal">被访人姓名</text>
|
<text class="text-[#666666] text-[28rpx] font-[350]">{{ visitedUserText }}</text>
|
</view>
|
<view class="flex justify-between items-start min-h-[96rpx] border-b1 py-[24rpx] box-border">
|
<text class="text-[#000000] text-[28rpx] font-normal">拜访事由</text>
|
<text class="text-[#666666] text-[28rpx] font-[350] flex-1 text-right ml-[24rpx] line-clamp-2 break-all">{{ visitReasonText }}</text>
|
</view>
|
<view class="flex justify-between items-center h-[96rpx]">
|
<text class="text-[#000000] text-[28rpx] font-normal">拜访时间</text>
|
<text class="text-[#666666] text-[28rpx] font-[350]">{{ visitTimeText }}</text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 来访人信息标题 -->
|
<view class="flex items-end mb-[26rpx] px-[8rpx]">
|
<text class="text-[32rpx] text-[#000000] font-[500]">来访人信息</text>
|
<text class="text-[24rpx] text-[#676767] font-[350] ml-[16rpx] mb-[4rpx]">选择您及同行人员</text>
|
</view>
|
|
<!-- 来访人信息卡片 -->
|
<view class="bg-white rounded-[16rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full mb-[24rpx] box-border pb-[24rpx]">
|
<view class="flex justify-between items-center px-[40rpx] py-[28rpx] border-b1">
|
<text class="text-[#000000] text-[28rpx] font-normal">进场类型</text>
|
<text class="text-[#616161] text-[28rpx] font-[350]">{{ visitTypeText }}</text>
|
</view>
|
|
<view class="px-[32rpx] py-[16rpx]">
|
<view
|
v-for="(visitor, index) in visitors"
|
:key="visitor.id"
|
class="flex items-center py-[24rpx] border-b2 mb-[16rpx]"
|
:class="index !== visitors.length - 1 ? '' : ''"
|
>
|
<text class="text-[#333333] text-[30rpx] font-medium w-[100rpx] pl-[20rpx] shrink-0">{{ visitor.name }}</text>
|
<view class="flex flex-col gap-[12rpx] flex-1 min-w-0 pl-[36rpx]">
|
<view class="flex items-center text-[#B3B3B3] text-[26rpx]">
|
<image :src="visitor.gender === 'male' ? appointmentIcons.male : appointmentIcons.female" class="w-[32rpx] h-[32rpx] mr-[12rpx] shrink-0" mode="aspectFit" />
|
<text>{{ visitor.idCard }}</text>
|
</view>
|
<view class="flex items-center text-[#B3B3B3] text-[26rpx]">
|
<image :src="appointmentIcons.company" class="w-[32rpx] h-[32rpx] mr-[12rpx] shrink-0" mode="aspectFit" />
|
<text class="truncate">{{ visitor.company }}</text>
|
</view>
|
<view v-if="visitor.plate" class="flex items-center text-[#B3B3B3] text-[26rpx]">
|
<view class="w-[28rpx] h-[20rpx] border-[2rpx] border-solid border-[#0068FF] rounded-[4rpx] flex items-center justify-center mr-[12rpx]">
|
<text class="text-[#0068FF] text-[12rpx] transform scale-75">NO.</text>
|
</view>
|
<text>{{ visitor.plate }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 提交时间卡片 -->
|
<view class="bg-white rounded-[16rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full mb-[32rpx] px-[40rpx] py-[12rpx] box-border">
|
<view class="flex flex-col">
|
<view class="flex justify-between items-center h-[96rpx]" :class="(appointmentStatus !== 'reviewing' && appointmentStatus !== 'invalid') ? 'border-b1' : ''">
|
<text class="text-[#000000] text-[28rpx] font-normal">提交时间</text>
|
<text class="text-[#666666] text-[28rpx] font-[350]">{{ createTimeText }}</text>
|
</view>
|
<view v-if="['pending_effect', 'effective'].includes(appointmentStatus)" class="flex justify-between items-center h-[96rpx]">
|
<text class="text-[#000000] text-[28rpx] font-normal">审核通过时间</text>
|
<text class="text-[#666666] text-[28rpx] font-[350]">{{ approvedTimeText }}</text>
|
</view>
|
<view v-if="appointmentStatus === 'cancelled'" class="flex justify-between items-center h-[96rpx]">
|
<text class="text-[#000000] text-[28rpx] font-normal">取消时间</text>
|
<text class="text-[#666666] text-[28rpx] font-[350]">{{ cancelledTimeText }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 底部操作按钮 -->
|
<view class="fixed bottom-0 left-0 w-[750rpx] h-[108rpx] bg-[#FFFFFF] rounded-t-[20rpx] box-content pb-[env(safe-area-inset-bottom)] z-[100] shadow-[inset_0_2rpx_0_0_#E1E1E1] flex justify-end items-center gap-[32rpx]" >
|
<view
|
v-if="detail?.cancelable && appointmentStatus === 'reviewing'"
|
class="w-[240rpx] h-[76rpx] rounded-full flex items-center justify-center bg-[#F5F5F5] transition-opacity active:opacity-70"
|
@click="handleCancel"
|
>
|
<image src="/static/review/reduce.png" class="w-[40rpx] h-[40rpx] mr-[8rpx]" mode="aspectFit" />
|
<text class="text-[30rpx] text-[#999999] font-medium">取消预约</text>
|
</view>
|
<view
|
v-if="appointmentStatus === 'rejected'"
|
class="w-[240rpx] h-[76rpx] rounded-full flex items-center justify-center bg-white border-[2rpx] border-solid border-[#0068FF] transition-opacity active:opacity-70 box-border"
|
@click="handleRebook"
|
>
|
<text class="text-[30rpx] text-[#0068FF] font-medium">重新提交</text>
|
</view>
|
<view
|
v-if="detail?.deletable && ['cancelled', 'invalid'].includes(appointmentStatus)"
|
class="w-[240rpx] h-[76rpx] rounded-full flex items-center justify-center bg-[#F5F5F5] transition-opacity active:opacity-70"
|
@click="handleDelete"
|
>
|
<image src="/static/appointment/delete.png" class="w-[40rpx] h-[40rpx] mr-[8rpx] opacity-50" mode="aspectFit" />
|
<text class="text-[30rpx] text-[#999999] font-medium">删除</text>
|
</view>
|
<view
|
class="w-[240rpx] h-[76rpx] mr-[40rpx] rounded-full flex items-center justify-center bg-[#0068FF] shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)] transition-opacity active:opacity-70"
|
@click="handleRebook"
|
>
|
<image src="/static/review/refesh.png" class="w-[40rpx] h-[40rpx] mr-[8rpx]" mode="aspectFit" />
|
<text class="text-[30rpx] text-white font-medium">再次预约</text>
|
</view>
|
</view>
|
|
<!-- 取消预约弹窗 -->
|
<wd-popup v-model="showCancelModal" custom-style="border-radius: 32rpx; background: transparent;" :safe-area-inset-bottom="false">
|
<view class="w-[600rpx] bg-white rounded-[32rpx] flex flex-col items-center pt-[48rpx] pb-[48rpx] px-[40rpx] box-border relative overflow-hidden" style="background: linear-gradient(180deg, #EBF4FF 0%, #FFFFFF 40%);">
|
<text class="text-[34rpx] text-[#333333] font-bold mb-[32rpx]">提示</text>
|
<image src="/static/review/cancelIcon.png" class="w-[202rpx] h-[202rpx] mb-[32rpx]" mode="aspectFit" />
|
<text class="text-[30rpx] text-[#333333] mb-[64rpx]">是否取消本次预约</text>
|
|
<view class="flex w-full justify-center gap-[32rpx]">
|
<view
|
class="w-[212rpx] h-[80rpx] rounded-full bg-[#F5F5F5] flex items-center justify-center transition-opacity active:opacity-70"
|
@click="showCancelModal = false"
|
>
|
<text class="text-[#999999] text-[30rpx]">再想想</text>
|
</view>
|
<view
|
class="w-[212rpx] h-[80rpx] rounded-full bg-[#0068FF] flex items-center justify-center transition-opacity active:opacity-70"
|
@click="confirmCancel"
|
>
|
<text class="text-white text-[30rpx]">确认取消</text>
|
</view>
|
</view>
|
</view>
|
</wd-popup>
|
|
<!-- 删除记录弹窗 -->
|
<wd-popup v-model="showDeleteModal" custom-style="border-radius: 32rpx; background: transparent;" :safe-area-inset-bottom="false">
|
<view class="w-[600rpx] bg-white rounded-[32rpx] flex flex-col items-center pt-[48rpx] pb-[48rpx] px-[40rpx] box-border relative overflow-hidden" style="background: linear-gradient(180deg, #EBF4FF 0%, #FFFFFF 40%);">
|
<text class="text-[34rpx] text-[#333333] font-bold mb-[32rpx]">提示</text>
|
<image src="/static/review/cancelIcon.png" class="w-[202rpx] h-[202rpx] mb-[32rpx]" mode="aspectFit" />
|
<text class="text-[30rpx] text-[#333333] mb-[64rpx]">是否有确认删除此记录?</text>
|
|
<view class="flex w-full justify-center gap-[32rpx]">
|
<view
|
class="w-[212rpx] h-[80rpx] rounded-full bg-[#F5F5F5] flex items-center justify-center transition-opacity active:opacity-70"
|
@click="showDeleteModal = false"
|
>
|
<text class="text-[#999999] text-[30rpx]">再想想</text>
|
</view>
|
<view
|
class="w-[212rpx] h-[80rpx] rounded-full bg-[#0068FF] flex items-center justify-center transition-opacity active:opacity-70"
|
@click="confirmDelete"
|
>
|
<text class="text-white text-[30rpx]">确认删除</text>
|
</view>
|
</view>
|
</view>
|
</wd-popup>
|
</view>
|
</template>
|
|
<style scoped lang="scss">
|
.border-b1{
|
border-bottom: 2rpx solid #F5F5F5;
|
}
|
|
.border-b2{
|
border-bottom: 2rpx dashed #F5F5F5;
|
|
&:last-child{
|
border-bottom: none;
|
margin-bottom: 0;
|
}
|
}
|
@keyframes ripple {
|
0% {
|
transform: scale(1);
|
opacity: 0.6;
|
}
|
100% {
|
transform: scale(1.6);
|
opacity: 0;
|
}
|
}
|
|
.ripple-wave {
|
animation: ripple 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
}
|
|
.ripple-delay {
|
animation-delay: 1s;
|
}
|
</style>
|