<script setup lang="ts">
|
import { fetchMyAppointmentOrders } from '@/api/appointment'
|
import type { MiniAppointmentListItem } from '@/types/appointment'
|
import { normalizeVisitReasonText } from '@/utils/visitReason'
|
|
definePage({
|
name: 'record',
|
layout: 'default',
|
style: {
|
navigationStyle: 'custom',
|
},
|
})
|
|
interface RecordTabItem {
|
title: string
|
status?: string
|
}
|
|
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: '全部' },
|
{ title: '待拜访', status: 'PENDING_VISIT' },
|
{ title: '生效中', status: 'EFFECTIVE' },
|
{ title: '已失效', status: 'INVALID' },
|
]
|
|
function handleBack() {
|
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) {
|
switch (item.displayStatus) {
|
case 'EFFECTIVE':
|
return {
|
bar: 'bg-[#52C41A]',
|
tag: 'bg-[#E8F8E3] text-[#52C41A]',
|
text: '生效中',
|
}
|
case 'INVALID':
|
return {
|
bar: 'bg-[#D8D8D8]',
|
tag: 'bg-[#F5F5F5] text-[#999999]',
|
text: item.status === 'CANCELLED' ? '已取消' : '已失效',
|
}
|
default:
|
return {
|
bar: 'bg-[#0068FF]',
|
tag: 'bg-[#E6F0FF] text-[#0068FF]',
|
text: '待拜访',
|
}
|
}
|
}
|
|
async function loadRecords(reset = false) {
|
if (loading.value) {
|
return
|
}
|
loading.value = true
|
try {
|
const nextPage = reset ? 1 : page.value
|
const result = await fetchMyAppointmentOrders({
|
page: nextPage,
|
size,
|
status: tabs[activeTab.value]?.status,
|
keyword: searchKeyword.value.trim() || undefined,
|
})
|
records.value = reset ? result.records : [...records.value, ...result.records]
|
page.value = nextPage + 1
|
total.value = result.total
|
finished.value = records.value.length >= result.total || result.records.length < size
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '加载预约记录失败',
|
icon: 'none',
|
})
|
}
|
finally {
|
loading.value = false
|
}
|
}
|
|
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) {
|
activeTab.value = index
|
reloadRecords()
|
}
|
|
function handleSearch() {
|
reloadRecords()
|
}
|
|
function handleRecordClick(item: MiniAppointmentListItem) {
|
uni.navigateTo({
|
url: `/pages/review/index?orderId=${item.id}&orderUuid=${item.uuid}`,
|
})
|
}
|
|
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()
|
})
|
|
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="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 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 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 class="w-[88rpx]" />
|
</view>
|
</view>
|
|
<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]">
|
<input
|
v-model="searchKeyword"
|
type="text"
|
placeholder="搜索"
|
placeholder-class="text-[#B3B3B3] text-[28rpx]"
|
class="flex-1 text-[28rpx] text-[#333333] h-full"
|
confirm-type="search"
|
@confirm="handleSearch"
|
/>
|
<image src="/static/record/search@2x.png" class="w-[48rpx] h-[48rpx] ml-[16rpx] shrink-0" @click="handleSearch" />
|
</view>
|
</view>
|
|
<view class="flex h-[88rpx] px-[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)"
|
>
|
<view class="flex items-center relative">
|
<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>
|
</view>
|
</view>
|
</view>
|
|
<view :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(36) + uni.upx2px(288) + uni.upx2px(24)) + 'px' }" class="shrink-0" />
|
|
<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"
|
:key="item.id"
|
class="bg-white rounded-[16rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] mb-[24rpx] overflow-hidden"
|
@click="handleRecordClick(item)"
|
>
|
<view class="w-full h-[12rpx]" :class="statusTheme(item).bar" />
|
|
<view class="pt-[28rpx] pb-[16rpx]">
|
<view class="flex justify-between items-center pb-[24rpx] px-[40rpx] status-border">
|
<view class="px-[16rpx] py-[4rpx] rounded-[4rpx]" :class="statusTheme(item).tag">
|
<text class="text-[24rpx] font-medium">{{ statusTheme(item).text }}</text>
|
</view>
|
<wd-icon name="arrow-right" size="32rpx" color="#999999" />
|
</view>
|
|
<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">{{ 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">
|
<text class="text-[#666666] text-[28rpx] w-[140rpx] shrink-0">访问区域:</text>
|
<text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ item.accessAreaText || '-' }}</text>
|
<view class="absolute bottom-0 left-[140rpx] right-0 h-[2rpx] bg-[#F5F5F5]" />
|
</view>
|
<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.visitTimeText }}</text>
|
<view class="absolute bottom-0 left-[140rpx] right-0 h-[2rpx] bg-[#F5F5F5]" />
|
</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">{{ 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>
|
</view>
|
|
<view v-if="loading" class="text-center text-[#999999] text-[24rpx] py-[24rpx]">
|
加载中...
|
</view>
|
<view v-else-if="records.length === 0" class="text-center text-[#999999] text-[24rpx] py-[80rpx]">
|
暂无预约记录
|
</view>
|
<view v-else-if="finished" class="text-center text-[#999999] text-[24rpx] py-[24rpx]">
|
共 {{ total }} 条记录
|
</view>
|
</view>
|
</scroll-view>
|
</view>
|
</view>
|
</template>
|
|
<style scoped>
|
.record-fixed-head {
|
z-index: 9999;
|
}
|
|
.record-back-hit {
|
z-index: 10001;
|
}
|
|
.search-card {
|
width: 702rpx;
|
height: 288rpx;
|
border-radius: 8rpx;
|
opacity: 1;
|
background: linear-gradient(180deg, #DEF4FF 0%, #FFFFFF 37%);
|
box-sizing: border-box;
|
border: 2rpx solid;
|
border-image: linear-gradient(180deg, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%) 2;
|
box-shadow: 0px 4px 8px 0px rgba(6, 47, 108, 0.1608);
|
overflow: hidden;
|
}
|
|
.status-border {
|
border-bottom: 2rpx solid #F5F5F5;
|
}
|
|
::-webkit-scrollbar {
|
display: none;
|
width: 0 !important;
|
height: 0 !important;
|
-webkit-appearance: none;
|
background: transparent;
|
}
|
</style>
|