| | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue' |
| | | import { fetchMiniFieldConfigs } from '@/api/lookup' |
| | | import { fetchMyVisitors } from '@/api/visitor' |
| | | import { FIELD_CODES } from '@/constants/fieldCodes' |
| | | import type { MiniFieldConfigItem } from '@/types/lookup' |
| | | import type { MiniVisitorListItem } from '@/types/visitor' |
| | | |
| | | definePage({ |
| | | name: 'choosePerson', |
| | |
| | | navigationStyle: 'custom', |
| | | }, |
| | | }) |
| | | |
| | | type PageMode = 'manage' | 'pick' | 'driver' |
| | | |
| | | const router = useRouter() |
| | | const choosePersonAssetBase = '/static/choosePerson' |
| | |
| | | company: '/static/appointment/company.png', |
| | | } |
| | | |
| | | interface Visitor { |
| | | id: number |
| | | name: string |
| | | phone: string |
| | | company: string |
| | | gender: 'male' | 'female' |
| | | const mode = ref<PageMode>('manage') |
| | | const visitors = ref<MiniVisitorListItem[]>([]) |
| | | const loading = ref(false) |
| | | const fieldConfigs = ref<MiniFieldConfigItem[]>([]) |
| | | const selectedId = ref<number | null>(null) |
| | | const selectedIds = ref<number[]>([]) |
| | | const vehicleIndex = ref<number>(-1) |
| | | |
| | | const showName = computed(() => isVisible(FIELD_CODES.VISITOR_NAME)) |
| | | const showGender = computed(() => isVisible(FIELD_CODES.GENDER)) |
| | | const showIdCard = computed(() => isVisible(FIELD_CODES.ID_CARD)) |
| | | const showCompany = computed(() => isVisible(FIELD_CODES.COMPANY)) |
| | | |
| | | function isVisible(code: string) { |
| | | const config = fieldConfigs.value.find(item => item.fieldCode === code) |
| | | return config ? config.visible === 1 : true |
| | | } |
| | | |
| | | // 模拟有数据状态 |
| | | const visitors = ref<Visitor[]>([ |
| | | { id: 1, name: '张三', phone: '5120********258', company: '成都xxxxxx有限公司', gender: 'male' }, |
| | | { id: 2, name: '张三', phone: '5120********258', company: '成都xxxxxx有限公司', gender: 'female' }, |
| | | { id: 3, name: '张三', phone: '5120********258', company: '成都xxxxxx有限公司', gender: 'female' }, |
| | | ]) |
| | | function displayName(item: MiniVisitorListItem) { |
| | | return showName.value ? item.visitorName || '来访人' : '来访人' |
| | | } |
| | | |
| | | const selectedId = ref<number>(1) |
| | | function normalizeGender(gender?: string | null) { |
| | | return gender === '女' ? 'female' : 'male' |
| | | } |
| | | |
| | | function handleSelect(id: number) { |
| | | if (mode.value === 'pick') { |
| | | if (selectedIds.value.includes(id)) { |
| | | selectedIds.value = selectedIds.value.filter(item => item !== id) |
| | | return |
| | | } |
| | | selectedIds.value = [...selectedIds.value, id] |
| | | return |
| | | } |
| | | selectedId.value = id |
| | | } |
| | | |
| | | function handleEdit(id: number) { |
| | | // 跳转到编辑页面 |
| | | uni.showToast({ title: '编辑功能开发中', icon: 'none' }) |
| | | function handleEdit(item: MiniVisitorListItem) { |
| | | uni.navigateTo({ |
| | | url: `/pages/addPerson/index?id=${item.id}&uuid=${item.uuid}`, |
| | | }) |
| | | } |
| | | |
| | | function buildPayload(items: MiniVisitorListItem[]) { |
| | | return items.map(item => ({ |
| | | id: item.id, |
| | | uuid: item.uuid, |
| | | name: item.visitorName, |
| | | phone: '', |
| | | company: item.visitorCompany || '', |
| | | gender: normalizeGender(item.gender), |
| | | idCardMasked: item.idCardMasked || '', |
| | | })) |
| | | } |
| | | |
| | | function handleConfirm() { |
| | | if (visitors.value.length > 0) { |
| | | const selectedVisitor = visitors.value.find(v => v.id === selectedId.value) |
| | | if (selectedVisitor) { |
| | | uni.$emit('acceptVisitor', selectedVisitor) |
| | | } |
| | | uni.showToast({ title: '选择成功', icon: 'success' }) |
| | | setTimeout(() => { |
| | | router.back() |
| | | }, 1000) |
| | | if (mode.value === 'manage') { |
| | | router.back() |
| | | return |
| | | } |
| | | |
| | | const picked = mode.value === 'pick' |
| | | ? visitors.value.filter(item => selectedIds.value.includes(item.id)) |
| | | : visitors.value.filter(item => item.id === selectedId.value) |
| | | |
| | | uni.$emit('selectVisitors', { |
| | | mode: mode.value, |
| | | vehicleIndex: vehicleIndex.value, |
| | | visitors: buildPayload(picked), |
| | | }) |
| | | |
| | | uni.showToast({ title: '选择成功', icon: 'success' }) |
| | | setTimeout(() => { |
| | | router.back() |
| | | }, 300) |
| | | } |
| | | |
| | | function handleBack() { |
| | |
| | | function handleAdd() { |
| | | uni.navigateTo({ url: '/pages/addPerson/index' }) |
| | | } |
| | | |
| | | async function loadVisitors() { |
| | | loading.value = true |
| | | try { |
| | | const [configs, list] = await Promise.all([ |
| | | fetchMiniFieldConfigs(), |
| | | fetchMyVisitors(), |
| | | ]) |
| | | fieldConfigs.value = configs |
| | | visitors.value = list |
| | | |
| | | if (mode.value === 'pick') { |
| | | selectedIds.value = selectedIds.value.filter(id => list.some(item => item.id === id)) |
| | | } |
| | | else { |
| | | selectedId.value = list.find(item => item.id === selectedId.value)?.id ?? list[0]?.id ?? null |
| | | } |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载来访人失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onLoad((options) => { |
| | | const pageMode = options?.mode as PageMode | undefined |
| | | if (pageMode === 'pick' || pageMode === 'driver' || pageMode === 'manage') { |
| | | mode.value = pageMode |
| | | } |
| | | |
| | | vehicleIndex.value = options?.vehicleIndex ? Number(options.vehicleIndex) : -1 |
| | | |
| | | if (mode.value === 'pick' && options?.selectedIds) { |
| | | selectedIds.value = options.selectedIds |
| | | .split(',') |
| | | .map(item => Number(item)) |
| | | .filter(item => !Number.isNaN(item)) |
| | | } |
| | | |
| | | if (mode.value === 'driver' && options?.selectedId) { |
| | | const id = Number(options.selectedId) |
| | | selectedId.value = Number.isNaN(id) ? null : id |
| | | } |
| | | }) |
| | | |
| | | onShow(() => { |
| | | void loadVisitors() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <view class="min-h-screen w-full relative box-border bg-[#F6FBFF] overflow-hidden"> |
| | | <!-- 背景图 --> |
| | | <image |
| | | :src="`${choosePersonAssetBase}/bg@2x.png`" |
| | | <image |
| | | :src="`${choosePersonAssetBase}/bg@2x.png`" |
| | | class="absolute left-0 top-0 w-full z-0" |
| | | mode="widthFix" |
| | | /> |
| | | |
| | | <!-- 自定义导航栏 --> |
| | | <view class="relative z-50 w-full" :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 v-if="visitors.length === 0" class="absolute top-[268rpx] left-0 w-full z-10 flex flex-col items-center"> |
| | | <!-- 缺省图 --> |
| | | <image |
| | | :src="`${choosePersonAssetBase}/noData.png`" |
| | | class="w-[380rpx] h-[380rpx]" |
| | | mode="aspectFit" |
| | | /> |
| | | <!-- 提示文案 --> |
| | | <view v-if="!loading && visitors.length === 0" class="absolute top-[268rpx] left-0 w-full z-10 flex flex-col items-center"> |
| | | <image :src="`${choosePersonAssetBase}/noData.png`" class="w-[380rpx] h-[380rpx]" mode="aspectFit" /> |
| | | <text class="text-[28rpx] text-[#B3B3B3]"> |
| | | 暂无来访人信息,请先添加~ |
| | | </text> |
| | | |
| | | <!-- 立即添加按钮 --> |
| | | <view |
| | | class="mt-[94rpx] w-[486rpx] h-[108rpx] rounded-full bg-[#0068FF] flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)]" |
| | | @click="handleAdd" |
| | | > |
| | | <view class="mt-[94rpx] w-[486rpx] h-[108rpx] rounded-full bg-[#0068FF] flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)]" @click="handleAdd"> |
| | | <image :src="`${choosePersonAssetBase}/add.png`" class="w-[40rpx] h-[40rpx]" /> |
| | | <text class="ml-[12rpx] text-[32rpx] text-white font-medium">立即添加</text> |
| | | </view> |
| | | |
| | | <!-- 返回按钮 --> |
| | | <view |
| | | class="mt-[48rpx] w-[486rpx] h-[108rpx] rounded-full border-[2rpx] border-solid border-[#0068FF] bg-white flex items-center justify-center" |
| | | @click="handleBack" |
| | | > |
| | | <view class="mt-[48rpx] w-[486rpx] h-[108rpx] rounded-full border-[2rpx] border-solid border-[#0068FF] bg-white flex items-center justify-center" @click="handleBack"> |
| | | <text class="text-[32rpx] text-[#0068FF] font-medium">返回</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 有数据时的列表展示 --> |
| | | <view v-else class="relative z-10 px-[24rpx] pb-[200rpx]" > |
| | | <!-- 顶部添加按钮 --> |
| | | <view v-else class="relative z-10 px-[24rpx] pb-[200rpx]"> |
| | | <view class="w-full bg-white/50 rounded-[8rpx] h-[144rpx] flex items-center justify-center shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] mb-[24rpx]" @click="handleAdd"> |
| | | <view class="flex items-center justify-center w-[252rpx] h-[64rpx] rounded-full border-[2rpx] border-solid border-[#0068FF] bg-transparent"> |
| | | <wd-icon name="add-circle" size="28rpx" color="#1677FF" /> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 人员卡片列表 --> |
| | | <view class="w-full bg-white rounded-[8rpx] shadow-[0_4rpx_8rpx_rgba(6,47,108,0.1608)] overflow-hidden"> |
| | | <view |
| | | v-for="(item, index) in visitors" |
| | | <view |
| | | v-for="(item, index) in visitors" |
| | | :key="item.id" |
| | | class="relative flex items-center justify-between px-[24rpx] py-[24rpx]" |
| | | @click="handleSelect(item.id)" |
| | | > |
| | | <!-- 底部分隔线 (最后一个元素不显示) --> |
| | | <view v-if="index < visitors.length - 1" class="absolute bottom-0 right-0 w-[574rpx] h-[2rpx] bg-[#F5F5F5]"></view> |
| | | <view v-if="index < visitors.length - 1" class="absolute bottom-0 right-0 w-[574rpx] h-[2rpx] bg-[#F5F5F5]" /> |
| | | |
| | | <!-- 左侧:单选按钮和名字 --> |
| | | <view class="flex items-center w-[160rpx] shrink-0"> |
| | | <!-- 自定义单选框 --> |
| | | <view class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center transition-colors duration-200 shrink-0" |
| | | :class="selectedId === item.id ? 'border-[#0068FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="selectedId === item.id" class="w-[16rpx] h-[16rpx] rounded-full bg-[#0068FF]"></view> |
| | | <view |
| | | class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center transition-colors duration-200 shrink-0" |
| | | :class="mode === 'pick' |
| | | ? (selectedIds.includes(item.id) ? 'border-[#0068FF]' : 'border-[#CFCFCF]') |
| | | : (selectedId === item.id ? 'border-[#0068FF]' : 'border-[#CFCFCF]')" |
| | | > |
| | | <view v-if="mode === 'pick' ? selectedIds.includes(item.id) : selectedId === item.id" class="w-[16rpx] h-[16rpx] rounded-full bg-[#0068FF]" /> |
| | | </view> |
| | | <text class="ml-[16rpx] text-[28rpx] text-[#333333] font-medium truncate">{{ item.name }}</text> |
| | | <text class="ml-[16rpx] text-[28rpx] text-[#333333] font-medium truncate">{{ displayName(item) }}</text> |
| | | </view> |
| | | |
| | | <!-- 中间:详细信息 --> |
| | | <view class="flex flex-col justify-center flex-1 mx-[20rpx] min-w-0"> |
| | | <view class="flex items-center"> |
| | | <image :src="item.gender === 'female' ? appointmentIcons.female : appointmentIcons.male" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate">{{ item.phone }}</text> |
| | | <view v-if="showGender || showIdCard" class="flex items-center"> |
| | | <image :src="item.gender === '女' ? appointmentIcons.female : appointmentIcons.male" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate"> |
| | | {{ [showGender ? item.gender : '', showIdCard ? item.idCardMasked || '-' : ''].filter(Boolean).join(' ') }} |
| | | </text> |
| | | </view> |
| | | <view class="flex items-center mt-[8rpx]"> |
| | | <view v-if="showCompany" class="flex items-center mt-[8rpx]"> |
| | | <image :src="appointmentIcons.company" class="w-[32rpx] h-[32rpx] shrink-0" /> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate">{{ item.company }}</text> |
| | | <text class="ml-[20rpx] text-[28rpx] text-[#B3B3B3] truncate">{{ item.visitorCompany || '-' }}</text> |
| | | </view> |
| | | <text v-if="item.missingFieldCodes?.length" class="mt-[8rpx] text-[22rpx] text-[#F5222D] truncate"> |
| | | 缺失:{{ item.missingFieldCodes.join('、') }} |
| | | </text> |
| | | </view> |
| | | |
| | | <!-- 右侧:编辑图标 --> |
| | | <view class="w-[40rpx] h-[40rpx] shrink-0 flex items-center justify-center" @click.stop="handleEdit(item.id)"> |
| | | <view class="w-[40rpx] h-[40rpx] shrink-0 flex items-center justify-center" @click.stop="handleEdit(item)"> |
| | | <image :src="`${choosePersonAssetBase}/edit.png`" class="w-[33rpx] h-[33rpx]" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 底部确认按钮 --> |
| | | <view v-if="visitors.length > 0" class="fixed bottom-[48rpx] left-0 w-full px-[32rpx] box-border z-[100]"> |
| | | <view |
| | | class="h-[96rpx] rounded-full bg-[#0068FF] flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)]" |
| | | @click="handleConfirm" |
| | | > |
| | | <view class="h-[96rpx] rounded-full bg-[#0068FF] flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)]" @click="handleConfirm"> |
| | | <text class="text-[32rpx] text-white font-medium">确认</text> |
| | | </view> |
| | | </view> |