| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import { fetchMyAppointmentOrders } from '@/api/appointment' |
| | | import type { MiniAppointmentListItem } from '@/types/appointment' |
| | | |
| | | definePage({ |
| | | name: 'record', |
| | |
| | | }, |
| | | }) |
| | | |
| | | interface RecordTabItem { |
| | | title: string |
| | | status?: string |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | |
| | | |
| | | const searchKeyword = ref('') |
| | | const activeTab = ref(0) |
| | | const loading = ref(false) |
| | | const records = ref<MiniAppointmentListItem[]>([]) |
| | | const page = ref(1) |
| | | const size = 10 |
| | | const total = ref(0) |
| | | const finished = ref(false) |
| | | |
| | | const tabs = [ |
| | | { title: '全部', badge: 0 }, |
| | | { title: '待拜访', badge: 2 }, |
| | | { title: '生效中', badge: 2 }, |
| | | { title: '已失效', badge: 0 }, |
| | | const tabs: RecordTabItem[] = [ |
| | | { title: '全部' }, |
| | | { title: '待拜访', status: 'PENDING_VISIT' }, |
| | | { title: '生效中', status: 'EFFECTIVE' }, |
| | | { title: '已失效', status: 'INVALID' }, |
| | | ] |
| | | |
| | | const records = ref([ |
| | | { |
| | | id: 1, |
| | | status: 'pending', |
| | | statusText: '待拜访', |
| | | interviewee: '员工A(管理部)', |
| | | area: 'xx区域/xx区域', |
| | | time: '2023-10-12 00:00 -2023-10-13 23:59', |
| | | reason: '业务洽谈', |
| | | }, |
| | | { |
| | | id: 2, |
| | | status: 'active', |
| | | statusText: '生效中', |
| | | interviewee: '员工A(管理部)', |
| | | area: 'xx区域/xx区域', |
| | | time: '2023-10-12 00:00 -2023-10-13 23:59', |
| | | reason: '业务洽谈', |
| | | }, |
| | | { |
| | | id: 3, |
| | | status: 'expired', |
| | | statusText: '已失效', |
| | | interviewee: '员工A(管理部)', |
| | | area: 'xx区域/xx区域', |
| | | time: '2023-10-12 00:00 -2023-10-13 23:59', |
| | | reason: '业务洽谈', |
| | | }, |
| | | ]) |
| | | |
| | | function handleBack() { |
| | | router.back() |
| | | } |
| | | |
| | | function handleTabClick(index: number) { |
| | | activeTab.value = 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: '待拜访', |
| | | } |
| | | } |
| | | } |
| | | |
| | | function handleRecordClick(status: string) { |
| | | // 映射 record 状态到 review 页面的 appointmentStatus |
| | | let targetStatus = '' |
| | | switch (status) { |
| | | case 'pending': |
| | | targetStatus = 'reviewing' |
| | | break |
| | | case 'active': |
| | | targetStatus = 'effective' |
| | | break |
| | | case 'expired': |
| | | targetStatus = 'invalid' |
| | | break |
| | | default: |
| | | targetStatus = 'reviewing' |
| | | 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 |
| | | } |
| | | } |
| | | |
| | | function reloadRecords() { |
| | | page.value = 1 |
| | | finished.value = false |
| | | void loadRecords(true) |
| | | } |
| | | |
| | | function handleTabClick(index: number) { |
| | | activeTab.value = index |
| | | reloadRecords() |
| | | } |
| | | |
| | | function handleSearch() { |
| | | reloadRecords() |
| | | } |
| | | |
| | | function handleRecordClick(item: MiniAppointmentListItem) { |
| | | uni.navigateTo({ |
| | | url: `/pages/review/index?status=${targetStatus}` |
| | | url: `/pages/review/index?orderId=${item.id}&orderUuid=${item.uuid}`, |
| | | }) |
| | | } |
| | | |
| | | onShow(() => { |
| | | reloadRecords() |
| | | }) |
| | | |
| | | onReachBottom(() => { |
| | | if (!finished.value) { |
| | | void loadRecords() |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <view class="min-h-screen bg-[#F6FBFF] w-full relative box-border flex flex-col"> |
| | | <!-- 顶部渐变背景(只保留基础底色,具体遮挡由上面 fixed 区域的背景完成) --> |
| | | <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 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(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="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 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 class="absolute left-0 w-full text-center text-[34rpx] font-medium text-white z-0"> |
| | | 预约记录 |
| | | </view> |
| | | <view class="w-[88rpx]"></view> |
| | | <view class="w-[88rpx]" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 搜索和标签栏容器 --> |
| | | <view class="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" /> |
| | | <image src="/static/record/search@2x.png" class="w-[48rpx] h-[48rpx] ml-[16rpx] shrink-0" /> |
| | | <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="index" |
| | | class="flex-1 flex flex-col items-center justify-center relative" @click="handleTabClick(index)"> |
| | | <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]'"> |
| | | <text class="text-[28rpx] transition-colors duration-200" :class="activeTab === index ? 'text-[#0068FF] font-medium' : 'text-[#999999]'"> |
| | | {{ tab.title }} |
| | | </text> |
| | | <view v-if="tab.badge > 0" |
| | | class="absolute top-[-10rpx] right-[-28rpx] bg-[#FF4D4F] text-white text-[20rpx] px-[8rpx] h-[28rpx] rounded-full flex items-center justify-center min-w-[28rpx] box-border"> |
| | | {{ tab.badge }} |
| | | </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 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> |
| | | <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="pb-[24rpx]"> |
| | | <view v-for="item in records" :key="item.id" |
| | | <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.status)"> |
| | | <!-- 顶部彩条 --> |
| | | <view class="w-full h-[12rpx]" :class="{ |
| | | 'bg-[#0068FF]': item.status === 'pending', |
| | | 'bg-[#52C41A]': item.status === 'active', |
| | | 'bg-[#D8D8D8]': item.status === 'expired' |
| | | }"></view> |
| | | @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="{ |
| | | 'bg-[#E6F0FF] text-[#0068FF]': item.status === 'pending', |
| | | 'bg-[#E8F8E3] text-[#52C41A]': item.status === 'active', |
| | | 'bg-[#F5F5F5] text-[#999999]': item.status === 'expired' |
| | | }"> |
| | | <text class="text-[24rpx] font-medium">{{ item.statusText }}</text> |
| | | <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 |
| | | class="mx-[6rpx]">访</text>人:</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ item.interviewee }}</text> |
| | | <!-- 底部边框 (跳过 label 区域) --> |
| | | <view class="absolute bottom-0 left-[140rpx] right-0 h-[2rpx] bg-[#F5F5F5]"></view> |
| | | <text class="text-[#666666] text-[28rpx] w-[140rpx] shrink-0">被访人:</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ item.visitedUserDisplay }}</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.area }}</text> |
| | | <!-- 底部边框 (跳过 label 区域) --> |
| | | <view class="absolute bottom-0 left-[140rpx] right-0 h-[2rpx] bg-[#F5F5F5]"></view> |
| | | <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.time }}</text> |
| | | <!-- 底部边框 (跳过 label 区域) --> |
| | | <view class="absolute bottom-0 left-[140rpx] right-0 h-[2rpx] bg-[#F5F5F5]"></view> |
| | | <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">{{ item.reason }}</text> |
| | | <text class="text-[#333333] text-[28rpx] flex-1 truncate">{{ item.visitReasonText }}</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> |
| | |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .status-border{ |
| | | .status-border { |
| | | border-bottom: 2rpx solid #F5F5F5; |
| | | } |
| | | |
| | | /* 隐藏滚动条 */ |
| | | ::-webkit-scrollbar { |
| | | display: none; |
| | | width: 0 !important; |