| | |
| | | <script setup lang="ts"> |
| | | import { fetchMyApprovalHistory, fetchMyApprovalTodos } from '@/api/appointment' |
| | | import type { MiniAppointmentListItem, MiniApprovalTodoItem } from '@/types/appointment' |
| | | import { fetchMyApprovalAll, fetchMyApprovalHistory, fetchMyApprovalTodos } from '@/api/appointment' |
| | | import type { MiniAppointmentListItem, MiniApprovalAllItem, MiniApprovalTodoItem } from '@/types/appointment' |
| | | import { normalizeVisitReasonText } from '@/utils/visitReason' |
| | | |
| | | definePage({ |
| | | name: 'reviewList', |
| | |
| | | key: string |
| | | status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'CANCELLED' | 'OTHER' |
| | | statusText: string |
| | | name: string |
| | | visitorText: string |
| | | plateText: string |
| | | nodeText: string |
| | | orderNo: string |
| | | time: string |
| | | reasonText: string |
| | | reasonOtherText: string |
| | | createTime: string |
| | | taskId?: number |
| | | taskUuid?: string |
| | |
| | | orderUuid: string |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | | const navBarHeight = 44 |
| | | const fixedHeaderHeightPx = computed(() => |
| | | statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(220) + uni.upx2px(24) + uni.upx2px(24), |
| | | ) |
| | | |
| | | const searchKeyword = ref('') |
| | | const dateValue = ref('') |
| | | const showDatePicker = ref(false) |
| | | const activeTab = ref(0) |
| | | const loading = ref(false) |
| | | const loadingMore = ref(false) |
| | | const records = ref<ReviewRecord[]>([]) |
| | | const page = ref(1) |
| | | const pageSize = 20 |
| | | const hasMore = ref(true) |
| | | const refreshing = ref(false) |
| | | const scrollIntoViewId = ref('') |
| | | const listRenderKey = ref(0) |
| | | const scrollLoadLocked = ref(false) |
| | | |
| | | const tabs = [ |
| | | { title: '待审核' }, |
| | |
| | | const dateColumns = ref<string[][]>([[], [], []]) |
| | | const datePickerIndex = ref([0, 0, 0]) |
| | | |
| | | function backOrHome() { |
| | | const pages = getCurrentPages() |
| | | if (pages.length > 1) { |
| | | uni.navigateBack({ |
| | | delta: 1, |
| | | fail: () => { |
| | | uni.switchTab({ url: '/pages/index/index' }) |
| | | }, |
| | | }) |
| | | return |
| | | } |
| | | uni.switchTab({ |
| | | url: '/pages/index/index', |
| | | fail: () => { |
| | | uni.reLaunch({ url: '/pages/index/index' }) |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | function handleBack() { |
| | | router.back() |
| | | backOrHome() |
| | | } |
| | | |
| | | function getCurrentDate() { |
| | |
| | | months.findIndex(item => item === selectedMonth.toString()), |
| | | days.findIndex(item => item === selectedDay.toString()), |
| | | ] |
| | | } |
| | | |
| | | function formatVisitReasonDisplay(reasonText?: string | null, reasonCode?: string | null) { |
| | | const normalizedText = normalizeVisitReasonText(reasonText) |
| | | const code = (reasonCode || '').trim().toUpperCase() |
| | | if (code === 'OTHER' || normalizedText === '其他') { |
| | | return '其他' |
| | | } |
| | | return normalizedText || code || '-' |
| | | } |
| | | |
| | | function handleDateChange(e: any) { |
| | |
| | | key: `todo-${item.taskId}`, |
| | | status: 'PENDING', |
| | | statusText: '待审核', |
| | | name: item.visitedUserName, |
| | | visitorText: item.visitorDisplayText || '-', |
| | | plateText: item.plateNoText || '', |
| | | nodeText: item.nodeName || '待审核', |
| | | orderNo: item.orderNo, |
| | | time: `${item.visitStartDate || '-'}${item.visitEndDate && item.visitEndDate !== item.visitStartDate ? ` 至 ${item.visitEndDate}` : ''}`, |
| | | time: normalizeVisitTimeText(item.visitTimeText || `${item.visitStartDate || '-'}${item.visitEndDate && item.visitEndDate !== item.visitStartDate ? ` 至 ${item.visitEndDate}` : ''}`), |
| | | reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode), |
| | | reasonOtherText: (item.visitReasonOther || '').trim() || '-', |
| | | createTime: item.createTime, |
| | | taskId: item.taskId, |
| | | taskUuid: item.taskUuid, |
| | |
| | | key: `history-${item.id}`, |
| | | status, |
| | | statusText, |
| | | name: item.visitedUserName, |
| | | visitorText: item.visitorDisplayText || '-', |
| | | plateText: item.plateNoText || '', |
| | | nodeText, |
| | | orderNo: item.orderNo, |
| | | time: item.visitTimeText, |
| | | time: normalizeVisitTimeText(item.visitTimeText), |
| | | reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode), |
| | | reasonOtherText: (item.visitReasonOther || '').trim() || '-', |
| | | createTime: item.createTime, |
| | | orderId: item.id, |
| | | orderUuid: item.uuid, |
| | | } |
| | | } |
| | | |
| | | function mapAll(item: MiniApprovalAllItem): ReviewRecord { |
| | | return { |
| | | key: item.taskId ? `todo-${item.taskId}` : `history-${item.orderId}`, |
| | | status: (item.status as ReviewRecord['status']) || 'OTHER', |
| | | statusText: item.statusText || '已处理', |
| | | visitorText: item.visitorDisplayText || '-', |
| | | plateText: item.plateNoText || '', |
| | | nodeText: item.nodeText || (item.statusText || '已处理'), |
| | | orderNo: item.orderNo, |
| | | time: normalizeVisitTimeText(item.visitTimeText || '-'), |
| | | reasonText: formatVisitReasonDisplay(item.visitReasonText, item.visitReasonCode), |
| | | reasonOtherText: (item.visitReasonOther || '').trim() || '-', |
| | | createTime: item.createTime, |
| | | taskId: item.taskId || undefined, |
| | | taskUuid: item.taskUuid || undefined, |
| | | orderId: item.orderId, |
| | | orderUuid: item.orderUuid, |
| | | } |
| | | } |
| | | |
| | |
| | | return value.replace('T', ' ').slice(0, 16) |
| | | } |
| | | |
| | | function normalizeVisitTimeText(value?: string | null) { |
| | | const text = (value || '').trim() |
| | | if (!text) { |
| | | return '-' |
| | | } |
| | | return text |
| | | .replace(' 全天', ' 00:00-23:59') |
| | | .replace(' 上午', ' 00:00-11:59') |
| | | .replace(' 下午', ' 12:00-23:59') |
| | | } |
| | | |
| | | async function loadRecords() { |
| | | if (loading.value) { |
| | | if (loading.value || loadingMore.value) { |
| | | return |
| | | } |
| | | loading.value = true |
| | | try { |
| | | const targetPage = 1 |
| | | const params = { |
| | | page: 1, |
| | | size: 50, |
| | | visitedUserNameKeyword: searchKeyword.value.trim() || undefined, |
| | | page: targetPage, |
| | | size: pageSize, |
| | | visitorNameKeyword: searchKeyword.value.trim() || undefined, |
| | | visitDate: dateValue.value || undefined, |
| | | } |
| | | |
| | | if (activeTab.value === 0) { |
| | | const todoResult = await fetchMyApprovalTodos(params) |
| | | records.value = todoResult.records.map(mapTodo) |
| | | page.value = targetPage |
| | | hasMore.value = todoResult.page < todoResult.pages |
| | | } |
| | | else if (activeTab.value === 1) { |
| | | const historyResult = await fetchMyApprovalHistory({ |
| | |
| | | status: 'REJECTED', |
| | | }) |
| | | records.value = historyResult.records.map(mapHistory) |
| | | page.value = targetPage |
| | | hasMore.value = historyResult.page < historyResult.pages |
| | | } |
| | | else if (activeTab.value === 2) { |
| | | const historyResult = await fetchMyApprovalHistory({ |
| | |
| | | status: 'APPROVED', |
| | | }) |
| | | records.value = historyResult.records.map(mapHistory) |
| | | page.value = targetPage |
| | | hasMore.value = historyResult.page < historyResult.pages |
| | | } |
| | | else { |
| | | const [todoResult, historyResult] = await Promise.all([ |
| | | fetchMyApprovalTodos(params), |
| | | fetchMyApprovalHistory(params), |
| | | ]) |
| | | records.value = sortRecords([ |
| | | ...todoResult.records.map(mapTodo), |
| | | ...historyResult.records.map(mapHistory), |
| | | ]) |
| | | const allResult = await fetchMyApprovalAll(params) |
| | | records.value = allResult.records.map(mapAll) |
| | | page.value = targetPage |
| | | hasMore.value = allResult.page < allResult.pages |
| | | } |
| | | } |
| | | catch (error) { |
| | |
| | | } |
| | | } |
| | | |
| | | async function loadMoreRecords() { |
| | | if (loading.value || loadingMore.value || !hasMore.value) { |
| | | return |
| | | } |
| | | loadingMore.value = true |
| | | const nextPage = page.value + 1 |
| | | try { |
| | | const params = { |
| | | page: nextPage, |
| | | size: pageSize, |
| | | visitorNameKeyword: searchKeyword.value.trim() || undefined, |
| | | visitDate: dateValue.value || undefined, |
| | | } |
| | | |
| | | if (activeTab.value === 0) { |
| | | const todoResult = await fetchMyApprovalTodos(params) |
| | | records.value = [...records.value, ...todoResult.records.map(mapTodo)] |
| | | page.value = nextPage |
| | | hasMore.value = todoResult.page < todoResult.pages |
| | | return |
| | | } |
| | | |
| | | if (activeTab.value === 1) { |
| | | const historyResult = await fetchMyApprovalHistory({ |
| | | ...params, |
| | | status: 'REJECTED', |
| | | }) |
| | | records.value = [...records.value, ...historyResult.records.map(mapHistory)] |
| | | page.value = nextPage |
| | | hasMore.value = historyResult.page < historyResult.pages |
| | | return |
| | | } |
| | | |
| | | if (activeTab.value === 2) { |
| | | const historyResult = await fetchMyApprovalHistory({ |
| | | ...params, |
| | | status: 'APPROVED', |
| | | }) |
| | | records.value = [...records.value, ...historyResult.records.map(mapHistory)] |
| | | page.value = nextPage |
| | | hasMore.value = historyResult.page < historyResult.pages |
| | | return |
| | | } |
| | | |
| | | const allResult = await fetchMyApprovalAll(params) |
| | | records.value = [...records.value, ...allResult.records.map(mapAll)] |
| | | page.value = nextPage |
| | | hasMore.value = allResult.page < allResult.pages |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载更多失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | loadingMore.value = false |
| | | scrollLoadLocked.value = false |
| | | } |
| | | } |
| | | |
| | | function handleListScroll(e: any) { |
| | | const detail = e?.detail |
| | | if (!detail || scrollLoadLocked.value || loading.value || loadingMore.value || !hasMore.value) { |
| | | return |
| | | } |
| | | const scrollTop = Number(detail.scrollTop || 0) |
| | | const clientHeight = Number(detail.clientHeight || 0) |
| | | const scrollHeight = Number(detail.scrollHeight || 0) |
| | | if (scrollHeight <= 0 || clientHeight <= 0) { |
| | | return |
| | | } |
| | | const remain = scrollHeight - (scrollTop + clientHeight) |
| | | if (remain <= 120) { |
| | | scrollLoadLocked.value = true |
| | | void loadMoreRecords() |
| | | } |
| | | } |
| | | |
| | | function handleTabClick(index: number) { |
| | | activeTab.value = index |
| | | listRenderKey.value += 1 |
| | | scrollIntoViewId.value = '' |
| | | nextTick(() => { |
| | | scrollIntoViewId.value = 'review-list-top-anchor' |
| | | }) |
| | | void loadRecords() |
| | | } |
| | | |
| | |
| | | onShow(() => { |
| | | void loadRecords() |
| | | }) |
| | | |
| | | function handleScrollRefresh() { |
| | | if (refreshing.value) { |
| | | return |
| | | } |
| | | refreshing.value = true |
| | | void loadRecords().finally(() => { |
| | | refreshing.value = false |
| | | }) |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <view class="min-h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col"> |
| | | <view class="h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col overflow-hidden"> |
| | | <view class="fixed left-0 top-0 z-0 h-[500rpx] w-full" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" /> |
| | | |
| | | <view class="fixed top-0 left-0 w-full z-50 flex flex-col"> |
| | | <view class="absolute left-0 top-0 z-[-1] w-full overflow-hidden" :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(220) + uni.upx2px(24)) + 'px' }"> |
| | | <view class="fixed top-0 left-0 w-full z-50 flex flex-col pointer-events-none"> |
| | | <view class="absolute left-0 top-0 z-[-1] w-full overflow-hidden" :style="{ height: `${fixedHeaderHeightPx}px` }"> |
| | | <view class="w-full h-[500rpx] absolute top-0 left-0" style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" /> |
| | | </view> |
| | | |
| | | <view class="relative w-full shrink-0" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }"> |
| | | <view class="relative w-full shrink-0 pointer-events-auto" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }"> |
| | | <view class="flex items-center justify-between w-full absolute left-0" :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> |
| | | |
| | | <view class="px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border"> |
| | | <view class="px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border pointer-events-auto"> |
| | | <view class="bg-white rounded-[16rpx] search-card shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full shrink-0 flex flex-col"> |
| | | <view class="flex h-[88rpx] px-[16rpx] mt-[16rpx]"> |
| | | <view v-for="(tab, index) in tabs" :key="tab.title" class="flex-1 flex flex-col items-center justify-center relative" @click="handleTabClick(index)"> |
| | |
| | | <input |
| | | v-model="searchKeyword" |
| | | type="text" |
| | | placeholder="搜索被访人姓名" |
| | | placeholder="搜索访客姓名" |
| | | placeholder-class="text-[#B3B3B3] text-[26rpx]" |
| | | class="w-full text-[26rpx] text-[#333333] h-full" |
| | | confirm-type="search" |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <view :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(220) + uni.upx2px(24)) + 'px' }" class="shrink-0" /> |
| | | <view :style="{ height: `${fixedHeaderHeightPx}px` }" class="shrink-0" /> |
| | | |
| | | <view class="relative z-10 px-[24rpx] flex-1 flex flex-col min-h-0 box-border"> |
| | | <scroll-view scroll-y class="flex-1 min-h-0" :show-scrollbar="false"> |
| | | <scroll-view |
| | | :key="listRenderKey" |
| | | scroll-y |
| | | class="flex-1 min-h-0" |
| | | :show-scrollbar="false" |
| | | :scroll-into-view="scrollIntoViewId" |
| | | :lower-threshold="120" |
| | | refresher-enabled |
| | | :refresher-triggered="refreshing" |
| | | @refresherrefresh="handleScrollRefresh" |
| | | @scroll="handleListScroll" |
| | | @scrolltolower="loadMoreRecords" |
| | | > |
| | | <view class="pb-[24rpx]"> |
| | | <view id="review-list-top-anchor" class="h-[2rpx]" /> |
| | | <view |
| | | v-for="item in records" |
| | | :key="item.key" |
| | |
| | | |
| | | <view class="flex flex-col px-[40rpx] pt-[8rpx]"> |
| | | <view class="flex items-center h-[80rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">被访人:</text> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">访客姓名:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#666666] text-[28rpx]">{{ item.name || '-' }}</text> |
| | | <text class="text-[#666666] text-[28rpx]">{{ item.visitorText || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | <view v-if="item.plateText" class="flex items-center h-[80rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">访客车辆:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#666666] text-[28rpx]">{{ item.plateText }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="flex items-center h-[80rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">单号:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#666666] text-[28rpx]">{{ item.orderNo }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="flex items-center h-[80rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">拜访时间:</text> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">来访时间:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#666666] text-[28rpx]">{{ item.time }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="flex items-center h-[80rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">节点:</text> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">来访事由:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#666666] text-[28rpx]">{{ item.nodeText }}</text> |
| | | <text class="text-[#666666] text-[28rpx]">{{ item.reasonText }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="flex items-center h-[80rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">提交时间:</text> |
| | | <view class="flex-1 h-full flex items-center"> |
| | | <text class="text-[#666666] text-[28rpx]">{{ formatDateTime(item.createTime) }}</text> |
| | | <view class="flex items-center min-h-[80rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[140rpx]">事由说明:</text> |
| | | <view class="flex-1 min-h-[80rpx] flex items-center border-b1 py-[16rpx] box-border"> |
| | | <text class="text-[#666666] text-[28rpx] line-clamp-2 break-all">{{ item.reasonOtherText }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | <view v-else-if="records.length === 0" class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]"> |
| | | 暂无待办记录 |
| | | </view> |
| | | <view v-else-if="loadingMore" class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]"> |
| | | 加载更多中... |
| | | </view> |
| | | <view v-else-if="hasMore" class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]"> |
| | | 上拉加载更多 |
| | | </view> |
| | | <view v-else class="text-center text-[#999999] text-[24rpx] mt-[32rpx] mb-[32rpx]"> |
| | | -最多显示3个月数据- |
| | | </view> |