<script setup lang="ts">
|
import { cancelMyAppointment, deleteMyAppointment, fetchMyAppointmentDetail } from '@/api/appointment'
|
import type { ApprovalFlowNodeItem, MiniMyAppointmentDetailItem } from '@/types/appointment'
|
|
definePage({
|
name: 'review',
|
layout: 'default',
|
style: {
|
navigationStyle: 'custom',
|
},
|
})
|
|
const router = useRouter()
|
const sysInfo = uni.getSystemInfoSync()
|
const statusBarHeight = sysInfo.statusBarHeight || 0
|
const navBarHeight = 44
|
|
const showCancelModal = ref(false)
|
const showDeleteModal = ref(false)
|
const detail = ref<MiniMyAppointmentDetailItem | null>(null)
|
const loading = ref(false)
|
const canceling = ref(false)
|
const deleting = ref(false)
|
const orderId = ref<number>()
|
const orderUuid = ref('')
|
|
const appointmentIcons = {
|
male: '/static/appointment/man.png',
|
female: '/static/appointment/woman.png',
|
company: '/static/appointment/company.png',
|
}
|
|
const appointmentStatus = computed(() => detail.value?.displayStatus || 'PENDING_VISIT')
|
const invalidLike = computed(() => appointmentStatus.value === 'INVALID')
|
const headerBg = computed(() => invalidLike.value ? '/static/review/bg-grey.png' : '/static/review/bg-t.png')
|
const titleText = computed(() => detail.value?.statusText || '预约记录')
|
const showVisitorCodeEntry = computed(() => appointmentStatus.value === 'PENDING_VISIT' || appointmentStatus.value === 'EFFECTIVE')
|
|
function handleBack() {
|
router.back()
|
}
|
|
function handleCancel() {
|
if (!detail.value?.cancelable) {
|
return
|
}
|
showCancelModal.value = true
|
}
|
|
async function confirmCancel() {
|
if (!orderId.value || !orderUuid.value || canceling.value) {
|
return
|
}
|
canceling.value = true
|
try {
|
await cancelMyAppointment(orderId.value, orderUuid.value)
|
showCancelModal.value = false
|
uni.showToast({ title: '已取消预约', icon: 'success' })
|
await loadDetail()
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '取消预约失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
canceling.value = false
|
}
|
}
|
|
function handleRebook() {
|
if (!detail.value) {
|
return
|
}
|
uni.navigateTo({
|
url: `/pages/appointment/index?rebookOrderId=${detail.value.orderId}&rebookOrderUuid=${encodeURIComponent(detail.value.orderUuid)}`,
|
})
|
}
|
|
function handleDelete() {
|
if (!detail.value?.deletable) {
|
return
|
}
|
showDeleteModal.value = true
|
}
|
|
async function confirmDelete() {
|
if (!orderId.value || !orderUuid.value || deleting.value) {
|
return
|
}
|
deleting.value = true
|
try {
|
await deleteMyAppointment(orderId.value, orderUuid.value)
|
showDeleteModal.value = false
|
uni.showToast({ title: '已删除记录', icon: 'success' })
|
setTimeout(() => {
|
uni.navigateBack()
|
}, 500)
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '删除记录失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
deleting.value = false
|
}
|
}
|
|
function handleViewVisitorCode() {
|
uni.switchTab({ url: '/pages/index/index' })
|
}
|
|
function visitorGenderIcon(gender?: string | null) {
|
return gender === 'FEMALE' ? appointmentIcons.female : appointmentIcons.male
|
}
|
|
function flowStatusText(status?: string | null) {
|
switch (status) {
|
case 'APPROVED':
|
return '已通过'
|
case 'REJECTED':
|
return '已驳回'
|
case 'CANCELLED':
|
return '已取消'
|
case 'SKIPPED':
|
return '已跳过'
|
case 'PENDING':
|
return '审批中'
|
default:
|
return '待处理'
|
}
|
}
|
|
function flowStatusClass(status?: string | null) {
|
switch (status) {
|
case 'APPROVED':
|
return 'text-[#52C41A]'
|
case 'REJECTED':
|
return 'text-[#FF4D4F]'
|
case 'CANCELLED':
|
return 'text-[#999999]'
|
case 'PENDING':
|
return 'text-[#0068FF]'
|
default:
|
return 'text-[#999999]'
|
}
|
}
|
|
function flowUsers(node: ApprovalFlowNodeItem) {
|
return node.groups.flatMap(group => group.users.map(user => ({
|
id: user.taskId,
|
name: user.userName || group.groupName || '审批人',
|
status: user.taskStatus,
|
time: user.actionTime,
|
})))
|
}
|
|
async function loadDetail() {
|
if (!orderId.value || !orderUuid.value) {
|
uni.showToast({ title: '缺少预约参数', icon: 'none' })
|
return
|
}
|
loading.value = true
|
try {
|
detail.value = await fetchMyAppointmentDetail(orderId.value, orderUuid.value)
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '加载预约详情失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
loading.value = false
|
}
|
}
|
|
onLoad((options) => {
|
orderId.value = options?.orderId ? Number(options.orderId) : undefined
|
orderUuid.value = 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="headerBg" class="w-full h-full" mode="aspectFill" />
|
</view>
|
|
<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' }">
|
<image :src="headerBg" 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 class="relative z-10 px-[24rpx] flex-1">
|
<view v-if="loading" class="px-[24rpx] pt-[120rpx] text-center text-white text-[28rpx]">
|
加载中...
|
</view>
|
|
<template v-else-if="detail">
|
<view class="px-[24rpx] mb-[32rpx]" :class="showVisitorCodeEntry ? '' : 'mt-[76rpx]'">
|
<view class="flex items-center mb-[20rpx]">
|
<image
|
:src="appointmentStatus === 'EFFECTIVE' ? '/static/review/success.png' : invalidLike ? '/static/review/cancel.png' : '/static/review/Pending.png'"
|
class="w-[40rpx] h-[40rpx] mr-[8rpx]"
|
mode="aspectFit"
|
/>
|
<text class="text-[32rpx] text-white font-bold">{{ titleText }}</text>
|
</view>
|
|
<view class="w-full h-[2rpx] bg-white/30 mb-[24rpx]" />
|
|
<view v-if="showVisitorCodeEntry" 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-[26rpx] font-medium">{{ detail.visitTimeText }}</text>
|
</view>
|
</view>
|
|
<view v-if="showVisitorCodeEntry" class="flex justify-center mb-[48rpx]">
|
<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="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]">{{ detail.visitedUserDisplay }}</text>
|
</view>
|
<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]">{{ detail.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] text-right ml-[24rpx]">{{ detail.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]">{{ detail.visitTypeText }}</text>
|
</view>
|
|
<view class="px-[32rpx] py-[16rpx]">
|
<view
|
v-for="(visitor, index) in detail.visitors"
|
:key="visitor.visitorId"
|
class="flex items-center py-[24rpx] border-b2 mb-[16rpx]"
|
:class="index === detail.visitors.length - 1 ? 'last-visitor' : ''"
|
>
|
<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="visitorGenderIcon(visitor.gender)" class="w-[32rpx] h-[32rpx] mr-[12rpx] shrink-0" mode="aspectFit" />
|
<text>{{ visitor.mobileMasked || '-' }}</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.companyName || '-' }}</text>
|
</view>
|
<view v-if="visitor.plateNo" 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.plateNo }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="flex items-end mb-[26rpx] px-[8rpx]">
|
<text class="text-[32rpx] text-[#000000] font-[500]">审批情况</text>
|
</view>
|
|
<view class="bg-white rounded-[16rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full mb-[32rpx] px-[40rpx] py-[24rpx] box-border">
|
<view v-for="node in detail.flowTree" :key="node.nodeCode" class="py-[20rpx]" :class="node !== detail.flowTree[detail.flowTree.length - 1] ? 'border-b1' : ''">
|
<view class="flex items-center justify-between mb-[12rpx]">
|
<text class="text-[#000000] text-[28rpx] font-medium">{{ node.nodeName }}</text>
|
<text class="text-[24rpx]" :class="flowStatusClass(node.nodeStatus)">{{ flowStatusText(node.nodeStatus) }}</text>
|
</view>
|
<view v-for="user in flowUsers(node)" :key="user.id" class="flex items-center justify-between py-[8rpx]">
|
<text class="text-[#666666] text-[24rpx]">{{ user.name }}</text>
|
<text class="text-[#999999] text-[22rpx]">{{ user.time ? user.time.replace('T', ' ').slice(0, 16) : flowStatusText(user.status) }}</text>
|
</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 justify-between items-center h-[96rpx]">
|
<text class="text-[#000000] text-[28rpx] font-normal">提交时间</text>
|
<text class="text-[#666666] text-[28rpx] font-[350]">{{ detail.createTime.replace('T', ' ').slice(0, 16) }}</text>
|
</view>
|
</view>
|
</template>
|
</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"
|
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-else-if="detail?.deletable"
|
class="w-[240rpx] h-[76rpx] rounded-full flex items-center justify-center bg-[#F5F5F5] transition-opacity active:opacity-70"
|
@click="handleDelete"
|
>
|
<wd-icon name="delete-thin" size="32rpx" color="#999999" class="mr-[8rpx]" />
|
<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]">{{ canceling ? '取消中...' : '确认取消' }}</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>
|
<wd-icon name="delete-thin" size="120rpx" color="#999999" class="mb-[32rpx]" />
|
<text class="text-[30rpx] text-[#333333] mb-[24rpx]">删除后将不再出现在我的预约记录中</text>
|
<text class="text-[24rpx] text-[#999999] 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]">{{ deleting ? '删除中...' : '确认删除' }}</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;
|
}
|
}
|
|
.last-visitor {
|
border-bottom: none;
|
margin-bottom: 0;
|
}
|
</style>
|