| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import { onLoad } from '@dcloudio/uni-app' |
| | | import { fetchVisitRecords } from '@/api/appointment' |
| | | import type { MiniVisitRecordItem } from '@/types/appointment' |
| | | |
| | | definePage({ |
| | | name: 'visitRecord', |
| | |
| | | }, |
| | | }) |
| | | |
| | | const props = withDefaults(defineProps<{ |
| | | title?: string |
| | | futureOnly?: boolean |
| | | }>(), { |
| | | title: '', |
| | | futureOnly: false, |
| | | }) |
| | | |
| | | type VisitRecordCard = { |
| | | id: number |
| | | name: string |
| | | totalPeople: number |
| | | time: string |
| | | createTime: string |
| | | orderNo: string |
| | | } |
| | | |
| | | const router = useRouter() |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | |
| | | |
| | | const searchKeyword = ref('') |
| | | const dateValue = ref('') |
| | | |
| | | const showDatePicker = ref(false) |
| | | const dateColumns = ref<string[][]>([ |
| | | [], // 年 |
| | | [], // 月 |
| | | [], // 日 |
| | | ]) |
| | | const loading = ref(false) |
| | | const records = ref<VisitRecordCard[]>([]) |
| | | const pageTitle = ref(props.title || '来访记录') |
| | | |
| | | const dateColumns = ref<string[][]>([[], [], []]) |
| | | const datePickerIndex = ref([0, 0, 0]) |
| | | |
| | | function getCurrentDate() { |
| | |
| | | return new Date(year, month, 0).getDate() |
| | | } |
| | | |
| | | function updateDateColumns(selection?: { year?: number; month?: number; day?: number }) { |
| | | function updateDateColumns(selection?: { year?: number, month?: number, day?: number }) { |
| | | const now = getCurrentDate() |
| | | const currentYear = now.getFullYear() |
| | | const currentMonth = now.getMonth() + 1 |
| | | const currentDay = now.getDate() |
| | | |
| | | const years = Array.from({ length: 11 }, (_, index) => (currentYear - 5 + index).toString()) |
| | | const selectedYear = selection?.year && years.includes(selection.year.toString()) |
| | | ? selection.year |
| | | : currentYear |
| | | |
| | | const selectedYear = selection?.year && years.includes(selection.year.toString()) ? selection.year : currentYear |
| | | const months = Array.from({ length: 12 }, (_, index) => (1 + index).toString()) |
| | | const selectedMonth = selection?.month && months.includes(selection.month.toString()) |
| | | ? selection.month |
| | | : currentMonth |
| | | |
| | | const selectedMonth = selection?.month && months.includes(selection.month.toString()) ? selection.month : currentMonth |
| | | const maxDay = getDaysInMonth(selectedYear, selectedMonth) |
| | | const days = Array.from({ length: maxDay }, (_, index) => (1 + index).toString()) |
| | | const selectedDay = selection?.day && days.includes(selection.day.toString()) |
| | | ? selection.day |
| | | : currentDay |
| | | const selectedDay = selection?.day && days.includes(selection.day.toString()) ? selection.day : currentDay |
| | | |
| | | dateColumns.value = [years, months, days] |
| | | datePickerIndex.value = [ |
| | |
| | | days.findIndex(item => item === selectedDay.toString()), |
| | | ] |
| | | } |
| | | |
| | | function initDateColumns() { |
| | | updateDateColumns() |
| | | } |
| | | initDateColumns() |
| | | |
| | | function handleDateChange(e: any) { |
| | | const selectedIndex = e.detail.value as number[] |
| | |
| | | const day = dateColumns.value[2][datePickerIndex.value[2]] |
| | | dateValue.value = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` |
| | | showDatePicker.value = false |
| | | void loadRecords() |
| | | } |
| | | |
| | | function openDatePicker() { |
| | | if (dateValue.value) { |
| | | const [year, month, day] = dateValue.value.split('-').map(Number) |
| | | updateDateColumns({ year, month, day }) |
| | | } else { |
| | | } |
| | | else { |
| | | updateDateColumns() |
| | | } |
| | | showDatePicker.value = true |
| | | } |
| | | |
| | | const records = ref([ |
| | | { |
| | | id: 1, |
| | | name: '张三', |
| | | company: 'xxxx有限公司', |
| | | hasCar: true, |
| | | totalPeople: 3, |
| | | carPlate: '苏U123456', |
| | | time: '2023-10-12 00:00至2023-10-13 23:59', |
| | | reason: '业务洽谈', |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: '张三', |
| | | company: 'xxxx有限公司', |
| | | hasCar: false, |
| | | totalPeople: 3, |
| | | carPlate: '苏U123456', |
| | | time: '2023-10-12 00:00至2023-10-13 23:59', |
| | | reason: '业务洽谈', |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: '张三', |
| | | company: 'xxxx有限公司', |
| | | hasCar: false, |
| | | totalPeople: 3, |
| | | carPlate: '苏U123456', |
| | | time: '2023-10-12 00:00至2023-10-13 23:59', |
| | | reason: '业务洽谈', |
| | | }, |
| | | ]) |
| | | |
| | | function handleBack() { |
| | | router.back() |
| | | } |
| | | |
| | | function handleSearch() { |
| | | uni.showToast({ title: '搜索中', icon: 'none' }) |
| | | function buildVisitTime(item: MiniVisitRecordItem) { |
| | | const range = item.visitStartDate && item.visitEndDate && item.visitStartDate !== item.visitEndDate |
| | | ? `${item.visitStartDate} 至 ${item.visitEndDate}` |
| | | : item.visitStartDate || item.visitEndDate || '-' |
| | | const slotMap: Record<string, string> = { |
| | | ALL_DAY: '全天', |
| | | AM: '上午', |
| | | PM: '下午', |
| | | } |
| | | const slotText = item.visitTimeMode === 'SINGLE' && item.visitTimeSlot |
| | | ? ` ${slotMap[item.visitTimeSlot] || item.visitTimeSlot}` |
| | | : '' |
| | | return `${range}${slotText}` |
| | | } |
| | | |
| | | function formatDateTime(value?: string | null) { |
| | | if (!value) { |
| | | return '-' |
| | | } |
| | | return value.replace('T', ' ').slice(0, 16) |
| | | } |
| | | |
| | | function mapRecord(item: MiniVisitRecordItem): VisitRecordCard { |
| | | return { |
| | | id: item.orderId, |
| | | name: item.visitorNames.join('、') || '-', |
| | | totalPeople: item.visitorNames.length, |
| | | time: buildVisitTime(item), |
| | | createTime: item.createTime, |
| | | orderNo: item.orderNo, |
| | | } |
| | | } |
| | | |
| | | async function loadRecords() { |
| | | if (loading.value) { |
| | | return |
| | | } |
| | | loading.value = true |
| | | try { |
| | | const result = await fetchVisitRecords({ |
| | | page: 1, |
| | | size: 50, |
| | | futureOnly: props.futureOnly, |
| | | visitorNameKeyword: searchKeyword.value.trim() || undefined, |
| | | visitDate: dateValue.value || undefined, |
| | | }) |
| | | records.value = result.records.map(mapRecord) |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载来访记录失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleSearch() { |
| | | void loadRecords() |
| | | } |
| | | |
| | | updateDateColumns() |
| | | |
| | | onLoad((options) => { |
| | | pageTitle.value = options?.title |
| | | ? decodeURIComponent(options.title) |
| | | : (props.title || '来访记录') |
| | | }) |
| | | |
| | | onShow(() => { |
| | | 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="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(120)) + '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"> |
| | | 来访记录 |
| | | {{ pageTitle }} |
| | | </view> |
| | | <view class="w-[88rpx]"></view> |
| | | <view class="w-[88rpx]" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 搜索栏 --> |
| | | <view class="px-[28rpx] w-[702rpx] pt-[24rpx] pb-[16rpx] box-border"> |
| | | <view class="px-[28rpx] w-[702rpx] pt-[24rpx] pb-[16rpx] box-border"> |
| | | <view class="bg-white rounded-[16rpx] h-[104rpx] flex items-center px-[24rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] w-full shrink-0 justify-between"> |
| | | <!-- 姓名输入 --> |
| | | <view class="bg-[#F5F5F5] w-[140rpx] px-[40rpx] h-[64rpx] rounded-[44rpx] flex items-center mr-[20rpx]"> |
| | | <input v-model="searchKeyword" type="text" placeholder="姓名关键字" placeholder-class="text-[#B3B3B3] text-[26rpx]" |
| | | class="w-full text-[26rpx] text-[#333333] h-full" /> |
| | | <input |
| | | v-model="searchKeyword" |
| | | type="text" |
| | | placeholder="搜索来访人姓名" |
| | | placeholder-class="text-[#B3B3B3] text-[26rpx]" |
| | | class="w-full text-[26rpx] text-[#333333] h-full" |
| | | confirm-type="search" |
| | | @confirm="handleSearch" |
| | | /> |
| | | </view> |
| | | |
| | | <!-- 日期选择 --> |
| | | <view class="flex items-center mr-[20rpx] bg-[#F5F5F5] w-[220rpx] h-[64rpx] rounded-[44rpx]" @click="openDatePicker"> |
| | | <view class="flex items-center"> |
| | | <text class="text-[28rpx] pl-[40rpx] mr-[20rpx]" :class="dateValue ? 'text-[#0068FF] font-medium' : 'text-[#333333]'">{{ dateValue || '来访日期' }}</text> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 查询按钮 --> |
| | | <view class="w-[156rpx] h-[64rpx] rounded-[32rpx] border-[2rpx] border-solid border-[#0068FF] flex items-center justify-center box-border bg-white" @click="handleSearch"> |
| | | <wd-icon name="search" size="28rpx" color="#0068FF" class="mr-[10rpx]" /> |
| | | <text class="text-[#0068FF] text-[26rpx] ml-[10rpx] font-medium">查询</text> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 占位元素,用于撑开固定的顶部高度 --> |
| | | <view :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(144)) + 'px' }" class="shrink-0"></view> |
| | | <view :style="{ height: (statusBarHeight + navBarHeight + uni.upx2px(144)) + 'px' }" class="shrink-0" /> |
| | | |
| | | <!-- 内容区 --> |
| | | <view class="relative z-10 px-[24rpx] flex-1 flex flex-col min-h-0 box-border pt-[16rpx]"> |
| | | <scroll-view scroll-y class="flex-1 min-h-0" :show-scrollbar="false"> |
| | | <view class="pb-[48rpx]"> |
| | | <view v-for="item in records" :key="item.id" |
| | | class="bg-white rounded-[8rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] mb-[24rpx] px-[40rpx]"> |
| | | |
| | | <view v-for="item in records" :key="item.id" class="bg-white rounded-[8rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] mb-[24rpx] px-[40rpx]"> |
| | | <view class="flex items-center h-[96rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[120rpx] mr-[12rpx]">访客姓名:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#333333] text-[28rpx]">{{ item.name }}</text> |
| | | <text class="text-[#999999] text-[28rpx] ml-[8rpx]">({{ item.company }})</text> |
| | | <view v-if="item.hasCar" class="ml-[16rpx] bg-[#E6F0FF] px-[12rpx] py-[4rpx] rounded-[4rpx]"> |
| | | <text class="text-[#0068FF] text-[22rpx]">车辆</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | </view> |
| | | |
| | | <view class="flex items-center h-[96rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[120rpx] mr-[12rpx]">访客车辆:</text> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[120rpx] mr-[12rpx]">预约单号:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#616161] text-[28rpx]">{{ item.carPlate }}</text> |
| | | <text class="text-[#616161] text-[28rpx]">{{ item.orderNo }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | |
| | | </view> |
| | | |
| | | <view class="flex items-center h-[96rpx]"> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[120rpx] mr-[12rpx]">拜访事由:</text> |
| | | <view class="flex-1 h-full flex items-center border-b1"> |
| | | <text class="text-[#616161] text-[28rpx]">{{ item.reason }}</text> |
| | | <text class="text-[#333333] text-[28rpx] font-bold shrink-0 w-[120rpx] mr-[12rpx]">创建时间:</text> |
| | | <view class="flex-1 h-full flex items-center"> |
| | | <text class="text-[#616161] text-[28rpx]">{{ formatDateTime(item.createTime) }}</text> |
| | | </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]"> |
| | | 暂无{{ pageTitle }} |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | |
| | | <!-- 自定义来访日期弹窗 --> |
| | | <wd-popup v-model="showDatePicker" position="bottom" custom-class="date-popup" z-index="100"> |
| | | <view class="flex flex-col bg-white rounded-t-[32rpx] overflow-hidden h-[830rpx]"> |
| | | <!-- 弹窗头部 --> |
| | | <view class="relative flex items-center justify-center shrink-0 h-[96rpx] border-b border-[#F5F5F5] border-solid"> |
| | | <view class="absolute left-[32rpx] flex items-center justify-center w-[48rpx] h-[48rpx]" |
| | | @click="showDatePicker = false"> |
| | | <view class="absolute left-[32rpx] flex items-center justify-center w-[48rpx] h-[48rpx]" @click="showDatePicker = false"> |
| | | <wd-icon name="close-outline" size="40rpx" color="#333333" /> |
| | | </view> |
| | | <text class="text-[32rpx] text-[#333333] font-medium">选择来访日期</text> |
| | | </view> |
| | | |
| | | <!-- 弹窗内容区 --> |
| | | <view class="px-[32rpx] flex-1 flex flex-col justify-center relative"> |
| | | <!-- 头部列名 --> |
| | | <view class="flex items-center absolute top-[40rpx] left-[32rpx] right-[32rpx] z-10 text-[24rpx] text-[#333333] font-medium"> |
| | | <view class="flex-1 flex justify-center"><text class="bg-[#F5F5F5] rounded-[8rpx] px-[24rpx] py-[8rpx]">年</text></view> |
| | | <view class="flex-1 flex justify-center"><text class="bg-[#F5F5F5] rounded-[8rpx] px-[24rpx] py-[8rpx]">月</text></view> |
| | | <view class="flex-1 flex justify-center"><text class="bg-[#F5F5F5] rounded-[8rpx] px-[24rpx] py-[8rpx]">日</text></view> |
| | | </view> |
| | | <picker-view |
| | | class="custom-picker-view date-picker-view mt-[60rpx]" |
| | | :value="datePickerIndex" |
| | | indicator-style="height: 48px;" |
| | | style="width: 100%; height: 192px;" |
| | | @change="handleDateChange" |
| | | > |
| | | <picker-view-column |
| | | v-for="(col, index) in dateColumns" |
| | | :key="index" |
| | | > |
| | | <view |
| | | v-for="(item, itemIndex) in col" |
| | | :key="itemIndex" |
| | | class="flex items-center justify-center h-[48px]" |
| | | :class="{ 'wd-picker-view-column__item--active': datePickerIndex[index] === itemIndex, 'wd-picker-view-column__item': true }" |
| | | > |
| | | <picker-view class="custom-picker-view date-picker-view mt-[60rpx]" :value="datePickerIndex" indicator-style="height: 48px;" style="width: 100%; height: 192px;" @change="handleDateChange"> |
| | | <picker-view-column v-for="(col, index) in dateColumns" :key="index"> |
| | | <view v-for="(item, itemIndex) in col" :key="itemIndex" class="flex items-center justify-center h-[48px]" :class="{ 'wd-picker-view-column__item--active': datePickerIndex[index] === itemIndex, 'wd-picker-view-column__item': true }"> |
| | | {{ item }} |
| | | </view> |
| | | </picker-view-column> |
| | | </picker-view> |
| | | </view> |
| | | |
| | | <!-- 弹窗底部按钮 --> |
| | | <view class="px-[32rpx] pb-[48rpx] shrink-0"> |
| | | <wd-button type="primary" size="large" block custom-class="popup-submit-btn" |
| | | @click="handleDateConfirm"> |
| | | <wd-button type="primary" size="large" block custom-class="popup-submit-btn" @click="handleDateConfirm"> |
| | | 确认 |
| | | </wd-button> |
| | | </view> |
| | |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .border-b1{ |
| | | .border-b1 { |
| | | border-bottom: 2rpx solid #F5F5F5; |
| | | } |
| | | |
| | | /* 隐藏滚动条 */ |
| | | ::-webkit-scrollbar { |
| | | display: none; |
| | | width: 0 !important; |
| | |
| | | background: transparent; |
| | | } |
| | | |
| | | /* 自定义弹窗样式 */ |
| | | :deep(.date-popup) { |
| | | border-top-left-radius: 32rpx; |
| | | border-top-right-radius: 32rpx; |
| | |
| | | background-color: transparent !important; |
| | | } |
| | | |
| | | |
| | | .wd-picker-view-column__item { |
| | | color: #B3B3B3 !important; |
| | | font-size: 28rpx !important; |
| | | background-color: transparent !important; |
| | | transition: all 0.3s; |
| | | z-index: 10; |
| | | position: relative; |
| | | color: #B3B3B3 !important; |
| | | font-size: 28rpx !important; |
| | | background-color: transparent !important; |
| | | transition: all 0.3s; |
| | | z-index: 10; |
| | | position: relative; |
| | | } |
| | | |
| | | :deep(picker-view-column) { |
| | |
| | | z-index: 10; |
| | | } |
| | | |
| | | /* 选择器中间的选中边框线 */ |
| | | :deep(.custom-picker-view .wx-picker-view-indicator) { |
| | | border-top: none !important; |
| | | border-bottom: none !important; |
| | |
| | | display: none !important; |
| | | } |
| | | |
| | | .custom-picker-view .wd-picker-view-column__item--active { |
| | | color: #0068FF !important; |
| | | font-weight: 500; |
| | | font-size: 28rpx !important; |
| | | background-color: #F4F8FF !important; |
| | | } |
| | | |
| | | :deep(.date-picker-view picker-view-column:nth-child(1)), |
| | | :deep(.date-picker-view picker-view-column:nth-child(2)), |
| | | :deep(.date-picker-view picker-view-column:nth-child(3)) { |
| | |
| | | z-index: 10; |
| | | } |
| | | |
| | | /* 调整选项的宽度 */ |
| | | :deep(.date-picker-view picker-view-column:nth-child(1) .wd-picker-view-column__item), |
| | | :deep(.date-picker-view picker-view-column:nth-child(2) .wd-picker-view-column__item), |
| | | :deep(.date-picker-view picker-view-column:nth-child(3) .wd-picker-view-column__item) { |