| | |
| | | <script setup lang="ts"> |
| | | import { fetchMyAppointmentOrders } from '@/api/appointment' |
| | | import type { MiniAppointmentListItem } from '@/types/appointment' |
| | | import { normalizeVisitReasonText } from '@/utils/visitReason' |
| | | |
| | | definePage({ |
| | | name: 'record', |
| | |
| | | status?: string |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | | const navBarHeight = 44 |
| | |
| | | const searchKeyword = ref('') |
| | | const activeTab = ref(0) |
| | | const loading = ref(false) |
| | | const refreshing = ref(false) |
| | | const records = ref<MiniAppointmentListItem[]>([]) |
| | | const page = ref(1) |
| | | const size = 10 |
| | | const total = ref(0) |
| | | const finished = ref(false) |
| | | const statusCountMap = ref<Record<string, number>>({ |
| | | PENDING_VISIT: 0, |
| | | EFFECTIVE: 0, |
| | | }) |
| | | |
| | | const tabs: RecordTabItem[] = [ |
| | | { title: '全部' }, |
| | |
| | | ] |
| | | |
| | | function handleBack() { |
| | | router.back() |
| | | 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 statusTheme(item: MiniAppointmentListItem) { |
| | |
| | | } |
| | | } |
| | | |
| | | async function loadStatusCounts() { |
| | | try { |
| | | const [pendingResult, effectiveResult] = await Promise.all([ |
| | | fetchMyAppointmentOrders({ page: 1, size: 1, status: 'PENDING_VISIT' }), |
| | | fetchMyAppointmentOrders({ page: 1, size: 1, status: 'EFFECTIVE' }), |
| | | ]) |
| | | statusCountMap.value = { |
| | | PENDING_VISIT: pendingResult.total, |
| | | EFFECTIVE: effectiveResult.total, |
| | | } |
| | | } |
| | | catch (error) { |
| | | statusCountMap.value = { |
| | | PENDING_VISIT: 0, |
| | | EFFECTIVE: 0, |
| | | } |
| | | console.warn('load appointment status counts failed', error) |
| | | } |
| | | } |
| | | |
| | | function reloadRecords() { |
| | | page.value = 1 |
| | | finished.value = false |
| | | void loadRecords(true) |
| | | void loadStatusCounts() |
| | | } |
| | | |
| | | function handleTabClick(index: number) { |
| | |
| | | }) |
| | | } |
| | | |
| | | function formatVisitedUserText(item: MiniAppointmentListItem) { |
| | | const name = (item.visitedUserName || '').trim() |
| | | const dept = (item.visitedDeptName || '').trim() |
| | | if (!name) { |
| | | return item.visitedUserDisplay || '-' |
| | | } |
| | | if (!dept) { |
| | | return name |
| | | } |
| | | return `${name}(${dept})` |
| | | } |
| | | |
| | | function formatVisitReasonText(item: MiniAppointmentListItem) { |
| | | return normalizeVisitReasonText(item.visitReasonText) || '-' |
| | | } |
| | | |
| | | function formatVisitReasonOtherText(item: MiniAppointmentListItem) { |
| | | return (item.visitReasonOther || '').trim() || '-' |
| | | } |
| | | |
| | | onShow(() => { |
| | | reloadRecords() |
| | | }) |
| | | |
| | | onReachBottom(() => { |
| | | if (!finished.value) { |
| | | void loadRecords() |
| | | function handlePullRefresh() { |
| | | if (refreshing.value) { |
| | | return |
| | | } |
| | | }) |
| | | refreshing.value = true |
| | | page.value = 1 |
| | | finished.value = false |
| | | void loadRecords(true).finally(() => { |
| | | refreshing.value = false |
| | | }) |
| | | } |
| | | |
| | | function handleScrollToLower() { |
| | | if (finished.value || loading.value) { |
| | | return |
| | | } |
| | | void loadRecords() |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <view class="min-h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col"> |
| | | <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="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 pointer-events-none" 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(288) + uni.upx2px(24)) + 'px' }"> |
| | | <view class="fixed top-0 left-0 w-full flex flex-col record-fixed-head"> |
| | | <view class="absolute left-0 top-0 z-0 w-full overflow-hidden pointer-events-none" :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(288) + uni.upx2px(24)) + '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="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"> |
| | | <view class="relative z-20 w-full shrink-0" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }"> |
| | | <view class="flex items-center justify-between w-full absolute left-0 z-20" :style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }"> |
| | | <view class="record-back-hit flex h-full w-[120rpx] items-center justify-center relative" @tap.stop="handleBack" @click.stop="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> |
| | | |
| | | <view class="px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border"> |
| | | <view class="relative z-10 px-[24rpx] pt-[36rpx] pb-[24rpx] w-full box-border"> |
| | | <view class="bg-white rounded-[16rpx] search-card shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full shrink-0"> |
| | | <view class="flex justify-center pt-[32rpx] pb-[16rpx]"> |
| | | <view class="w-[622rpx] h-[88rpx] rounded-[44rpx] bg-[#FFFFFF] border-[4rpx] border-solid border-[#D4E6FF] box-border flex items-center px-[32rpx]"> |
| | |
| | | <text class="text-[28rpx] transition-colors duration-200" :class="activeTab === index ? 'text-[#0068FF] font-medium' : 'text-[#999999]'"> |
| | | {{ tab.title }} |
| | | </text> |
| | | <view |
| | | v-if="tab.status && statusCountMap[tab.status] > 0" |
| | | class="absolute right-[-28rpx] top-[-14rpx] min-w-[32rpx] h-[32rpx] px-[8rpx] rounded-full bg-[#FF4D4F] flex items-center justify-center box-border" |
| | | > |
| | | <text class="text-white text-[20rpx] leading-none"> |
| | | {{ statusCountMap[tab.status] > 99 ? '99+' : statusCountMap[tab.status] }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <view class="absolute bottom-[16rpx] w-[40rpx] h-[4rpx] rounded-[2rpx] transition-all duration-200" :class="activeTab === index ? 'bg-[#0068FF]' : 'bg-transparent'" /> |
| | | </view> |
| | |
| | | |
| | | <view :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(288) + uni.upx2px(24)) + '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"> |
| | | <view class="relative z-10 mt-[20rpx] 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" |
| | | :lower-threshold="120" |
| | | refresher-enabled |
| | | :refresher-triggered="refreshing" |
| | | @refresherrefresh="handlePullRefresh" |
| | | @scrolltolower="handleScrollToLower" |
| | | > |
| | | <view class="pb-[24rpx]"> |
| | | <view |
| | | v-for="item in records" |
| | |
| | | <view class="flex flex-col px-[40rpx]"> |
| | | <view class="flex items-center h-[96rpx] relative"> |
| | | <text class="text-[#666666] text-[28rpx] w-[140rpx] shrink-0">被访人:</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ item.visitedUserDisplay }}</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ formatVisitedUserText(item) }}</text> |
| | | <view class="absolute bottom-0 left-[140rpx] right-0 h-[2rpx] bg-[#F5F5F5]" /> |
| | | </view> |
| | | <view class="flex items-center h-[96rpx] relative"> |
| | |
| | | </view> |
| | | <view class="flex items-center h-[96rpx]"> |
| | | <text class="text-[#666666] text-[28rpx] w-[140rpx] shrink-0">拜访事由:</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ item.visitReasonText }}</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ formatVisitReasonText(item) }}</text> |
| | | </view> |
| | | <view class="flex items-center min-h-[96rpx]"> |
| | | <text class="text-[#666666] text-[28rpx] w-[140rpx] shrink-0">事由说明:</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 line-clamp-2 break-all">{{ formatVisitReasonOtherText(item) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .record-fixed-head { |
| | | z-index: 9999; |
| | | } |
| | | |
| | | .record-back-hit { |
| | | z-index: 10001; |
| | | } |
| | | |
| | | .search-card { |
| | | width: 702rpx; |
| | | height: 288rpx; |