| | |
| | | <script setup lang="ts"> |
| | | import { nextTick, reactive, ref } from 'vue' |
| | | import { computed, nextTick, reactive, ref, watch } from 'vue' |
| | | import { onLoad, onUnload } from '@dcloudio/uni-app' |
| | | import { createAppointment, fetchMyAppointmentDetail } from '@/api/appointment' |
| | | import { |
| | | fetchVisitReasons, |
| | | searchVisitedUsersByExactName, |
| | | } from '@/api/lookup' |
| | | import { fetchSystemSettings } from '@/api/systemSettings' |
| | | import { fetchMyVisitors } from '@/api/visitor' |
| | | import type { |
| | | MiniAppointmentCreateRequest, |
| | | MiniMyAppointmentDetailItem, |
| | | MiniMyAppointmentDetailRebookVisitorItem, |
| | | } from '@/types/appointment' |
| | | import type { MiniVisitReasonItem, MiniVisitedUserItem } from '@/types/lookup' |
| | | import type { MiniVisitorListItem } from '@/types/visitor' |
| | | |
| | | definePage({ |
| | | name: 'appointment', |
| | |
| | | const router = useRouter() |
| | | const appointmentAssetBase = '/static/appointment' |
| | | const defaultPlateTag = '新能源' |
| | | const VISIT_REASON_CODE_OTHER = 'OTHER' |
| | | const VISIT_REASON_NAME_OTHER = '其他' |
| | | const VISIT_TIME_MODE_SINGLE = 'SINGLE' |
| | | const VISIT_TIME_MODE_MULTI = 'MULTI' |
| | | const APPROVAL_RESULT_SUBSCRIBE_TEMPLATE_ID = '4FQovCBNFM7rth42AWHTusHajO788eT9VpplSN4ZG2s' |
| | | const TIME_SLOT_LABEL_TO_CODE = { |
| | | '全天00:00-23:59': 'ALL_DAY', |
| | | '上午00:00-11:59': 'AM', |
| | | '下午12:00-23:59': 'PM', |
| | | } as const |
| | | |
| | | interface AppointmentVehicle { |
| | | plateRegion: string |
| | | plateCharacters: string[] |
| | | plateTag: string |
| | | driver: string |
| | | driverVisitorId?: number | null |
| | | } |
| | | |
| | | interface AppointmentVisitor { |
| | | id: number |
| | | uuid?: string |
| | | name: string |
| | | phone: string |
| | | company: string |
| | | gender: 'male' | 'female' |
| | | idCardMasked?: string |
| | | badge?: string |
| | | } |
| | | |
| | |
| | | visitTimeSingle: '', |
| | | visitTimeMultiStart: '', |
| | | visitTimeMultiEnd: '', |
| | | entryType: 'person', // person 人员 | vehicle 车辆 |
| | | vehicleEntry: false, |
| | | visitors: [] as AppointmentVisitor[], |
| | | vehicles: [] as AppointmentVehicle[], |
| | | }) |
| | | |
| | | // UI 选项 |
| | | const reasonColumns = ['会议', '拜访', '邀约', '合作', '其他'] |
| | | const visitReasons = ref<MiniVisitReasonItem[]>([]) |
| | | const selectedVisitReason = ref<MiniVisitReasonItem | null>(null) |
| | | const reasonPickerValue = ref('') |
| | | const visitedOptions = ref<MiniVisitedUserItem[]>([]) |
| | | const selectedVisitedUser = ref<MiniVisitedUserItem | null>(null) |
| | | const customVisitReason = ref('') |
| | | const bookingRangeMode = ref<typeof VISIT_TIME_MODE_SINGLE | typeof VISIT_TIME_MODE_MULTI>(VISIT_TIME_MODE_SINGLE) |
| | | const maxConsecutiveDays = ref(3) |
| | | const reasonColumns = computed(() => visitReasons.value.map(item => item.reasonName)) |
| | | const allowMulti = computed(() => bookingRangeMode.value === VISIT_TIME_MODE_MULTI) |
| | | const showCustomReasonInput = computed(() => { |
| | | if (!selectedVisitReason.value) { |
| | | return false |
| | | } |
| | | return selectedVisitReason.value.reasonCode === VISIT_REASON_CODE_OTHER |
| | | || selectedVisitReason.value.reasonName === VISIT_REASON_NAME_OTHER |
| | | }) |
| | | const showReasonPicker = ref(false) |
| | | const appointmentIcons = { |
| | | male: `${appointmentAssetBase}/man.png`, |
| | |
| | | company: `${appointmentAssetBase}/company.png`, |
| | | delete: `${appointmentAssetBase}/delete.png`, |
| | | } |
| | | const mockDrivers = [ |
| | | '张琪-5120********258', |
| | | '李林-138****5621', |
| | | '王敏-136****2018', |
| | | ] |
| | | const focusedVehicleIndex = ref(-1) |
| | | const focusedCharIndex = ref(-1) |
| | | const rebookOrderId = ref<number | null>(null) |
| | | const rebookOrderUuid = ref('') |
| | | |
| | | function formatDatePart(value: number | string) { |
| | | return String(value).padStart(2, '0') |
| | | } |
| | | |
| | | function handlePlateCharClick(vehicleIndex: number, charIndex: number) { |
| | | focusedVehicleIndex.value = vehicleIndex |
| | |
| | | }) |
| | | } |
| | | |
| | | function handlePlateInput(e: any, vehicleIndex: number, charIndex: number) { |
| | | const value = e.detail.value.toUpperCase().trim() |
| | | const vehicle = formData.vehicles[vehicleIndex] |
| | | |
| | | function normalizePlateSegmentValue(rawValue: string) { |
| | | return (rawValue || '') |
| | | .replace(/\s+/g, '') |
| | | .replace(/·/g, '') |
| | | .toUpperCase() |
| | | } |
| | | |
| | | function setPlateChar(vehicle: AppointmentVehicle, charIndex: number, value: string) { |
| | | if (charIndex === 0) { |
| | | vehicle.plateRegion = value.slice(-1) |
| | | } else { |
| | | // 字符位,只取最后一个字符 |
| | | vehicle.plateCharacters[charIndex - 1] = value.slice(-1) |
| | | vehicle.plateRegion = value |
| | | return |
| | | } |
| | | vehicle.plateCharacters[charIndex - 1] = value |
| | | } |
| | | |
| | | function handlePlateInput(e: any, vehicleIndex: number, charIndex: number) { |
| | | const value = normalizePlateSegmentValue(e.detail.value) |
| | | const vehicle = formData.vehicles[vehicleIndex] |
| | | |
| | | if (!vehicle) { |
| | | return |
| | | } |
| | | |
| | | // 自动跳到下一个输入框 |
| | | if (value.length > 0 && charIndex < 7) { |
| | | focusNextPlateChar(vehicleIndex, charIndex + 1) |
| | | if (!value) { |
| | | setPlateChar(vehicle, charIndex, '') |
| | | return |
| | | } |
| | | |
| | | const chars = value.split('') |
| | | let lastFilledIndex = charIndex |
| | | chars.forEach((char, offset) => { |
| | | const nextIndex = charIndex + offset |
| | | if (nextIndex > 7) { |
| | | return |
| | | } |
| | | setPlateChar(vehicle, nextIndex, char) |
| | | lastFilledIndex = nextIndex |
| | | }) |
| | | |
| | | if (lastFilledIndex < 7) { |
| | | focusNextPlateChar(vehicleIndex, lastFilledIndex + 1) |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | const mockVehicles: AppointmentVehicle[] = [ |
| | | { |
| | | plateRegion: '', |
| | | plateCharacters: ['', '', '', '', '', '', ''], |
| | | plateTag: defaultPlateTag, |
| | | driver: '张琪-5120********258', |
| | | }, |
| | | ] |
| | | const mockVisitors: AppointmentVisitor[] = [ |
| | | { |
| | | name: '张三', |
| | | phone: '5120********258', |
| | | company: '成都XXXX有限公司', |
| | | gender: 'male', |
| | | badge: '驾驶员', |
| | | }, |
| | | { |
| | | name: '张三', |
| | | phone: '5120********258', |
| | | company: '成都有限公司', |
| | | gender: 'female', |
| | | }, |
| | | ] |
| | | |
| | | // 提交 loading 状态 |
| | | const isSubmitting = ref(false) |
| | | const pendingSubmitPayload = ref<MiniAppointmentCreateRequest | null>(null) |
| | | |
| | | // 拜访时间弹窗控制 |
| | | const showTimePicker = ref(false) |
| | |
| | | showMultiDatePicker.value = true |
| | | } |
| | | |
| | | function handleDateTypeChange(type: 'single' | 'multi') { |
| | | if (type === 'multi' && !allowMulti.value) { |
| | | return |
| | | } |
| | | formData.dateType = type |
| | | } |
| | | |
| | | function openReasonPicker() { |
| | | if (!visitReasons.value.length) { |
| | | uni.showToast({ |
| | | title: '暂无可选拜访事由', |
| | | icon: 'none', |
| | | }) |
| | | return |
| | | } |
| | | reasonPickerValue.value = formData.reason || visitReasons.value[0]?.reasonName || '' |
| | | showReasonPicker.value = true |
| | | } |
| | | |
| | | function handleReasonConfirm() { |
| | | const nextReasonName = reasonPickerValue.value || visitReasons.value[0]?.reasonName || '' |
| | | selectReasonByName(nextReasonName) |
| | | showReasonPicker.value = false |
| | | } |
| | | |
| | | async function handleSearchVisitedUser() { |
| | | const keyword = formData.interviewee.trim() |
| | | if (!keyword) { |
| | | visitedOptions.value = [] |
| | | selectedVisitedUser.value = null |
| | | return |
| | | } |
| | | try { |
| | | visitedOptions.value = await searchVisitedUsersByExactName(keyword) |
| | | } |
| | | catch (error) { |
| | | visitedOptions.value = [] |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '搜索被访人失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | } |
| | | |
| | | function selectVisitedUser(item: MiniVisitedUserItem) { |
| | | selectedVisitedUser.value = item |
| | | formData.interviewee = item.realName |
| | | visitedOptions.value = [] |
| | | } |
| | | |
| | | function handleMultiDateConfirm() { |
| | | const year = multiDateColumns.value[0][multiDatePickerIndex.value[0]] |
| | | const month = multiDateColumns.value[1][multiDatePickerIndex.value[1]] |
| | | const day = multiDateColumns.value[2][multiDatePickerIndex.value[2]] |
| | | const formattedDate = `${year}-${month}-${day}` |
| | | const formattedDate = `${year}-${formatDatePart(month)}-${formatDatePart(day)}` |
| | | |
| | | if (activeMultiDateField.value === 'start') { |
| | | formData.visitTimeMultiStart = formattedDate |
| | | if (formData.visitTimeMultiEnd && !isValidMultiDateRange(formData.visitTimeMultiStart, formData.visitTimeMultiEnd)) { |
| | | formData.visitTimeMultiEnd = '' |
| | | } |
| | | } else { |
| | | const nextStart = formData.visitTimeMultiStart |
| | | if (nextStart && !isValidMultiDateRange(nextStart, formattedDate)) { |
| | | return uni.showToast({ |
| | | title: `结束日期最长可选${maxConsecutiveDays.value}天内`, |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | formData.visitTimeMultiEnd = formattedDate |
| | | } |
| | | |
| | |
| | | const day = timeColumns.value[2][timePickerIndex.value[2]] |
| | | const time = timeColumns.value[3][timePickerIndex.value[3]] |
| | | |
| | | const formattedTime = `${year}-${month}-${day} ${time}` |
| | | const formattedTime = `${year}-${formatDatePart(month)}-${formatDatePart(day)} ${time}` |
| | | if (activeTimeField.value === 'single') { |
| | | formData.visitTimeSingle = formattedTime |
| | | } else if (activeTimeField.value === 'start') { |
| | |
| | | } |
| | | |
| | | const showPromisePopup = ref(false) |
| | | const promiseHtml = ref('') |
| | | const countdown = ref(5) |
| | | let timer: ReturnType<typeof setInterval> | null = null |
| | | |
| | | // 校验与提交 |
| | | function handleSubmit() { |
| | | if (!formData.interviewee || !formData.reason || |
| | | (formData.dateType === 'single' && !formData.visitTimeSingle) || |
| | | (formData.dateType === 'multi' && (!formData.visitTimeMultiStart || !formData.visitTimeMultiEnd)) || |
| | | formData.visitors.length === 0) { |
| | | return uni.showToast({ title: '请填写完整必填信息', icon: 'none' }) |
| | | } |
| | | |
| | | showPromisePopup.value = true |
| | | countdown.value = 5 |
| | | startCountdown() |
| | | } |
| | | let visitedSearchTimer: ReturnType<typeof setTimeout> | null = null |
| | | |
| | | function startCountdown() { |
| | | if (timer) clearInterval(timer) |
| | |
| | | }, 1000) |
| | | } |
| | | |
| | | function handlePromiseConfirm() { |
| | | if (countdown.value > 0) return |
| | | |
| | | showPromisePopup.value = false |
| | | isSubmitting.value = true |
| | | setTimeout(() => { |
| | | isSubmitting.value = false |
| | | uni.redirectTo({ url: '/pages/success/index' }) |
| | | }, 1000) |
| | | } |
| | | |
| | | function handlePromiseClose() { |
| | | showPromisePopup.value = false |
| | | if (timer) clearInterval(timer) |
| | | pendingSubmitPayload.value = null |
| | | if (timer) { |
| | | clearInterval(timer) |
| | | timer = null |
| | | } |
| | | } |
| | | |
| | | function handleEntryTypeChange(type: 'person' | 'vehicle') { |
| | | formData.entryType = type |
| | | if (type === 'vehicle' && formData.vehicles.length === 0) { |
| | | async function openParkNotice(payload: MiniAppointmentCreateRequest) { |
| | | try { |
| | | const settings = await fetchSystemSettings('CONTENT_PARK_NOTICE_HTML') |
| | | promiseHtml.value = settings.CONTENT_PARK_NOTICE_HTML || '<p>暂无入园承诺</p>' |
| | | pendingSubmitPayload.value = payload |
| | | showPromisePopup.value = true |
| | | countdown.value = 5 |
| | | startCountdown() |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载入园承诺失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | } |
| | | |
| | | function handleVehicleEntryChange(enabled: boolean) { |
| | | formData.vehicleEntry = enabled |
| | | if (enabled && formData.vehicles.length === 0) { |
| | | formData.vehicles = [createEmptyVehicle()] |
| | | } |
| | | if (!enabled) { |
| | | formData.vehicles = [] |
| | | } |
| | | } |
| | | |
| | |
| | | plateCharacters: ['', '', '', '', '', '', ''], |
| | | plateTag: defaultPlateTag, |
| | | driver: '', |
| | | driverVisitorId: null, |
| | | } |
| | | } |
| | | |
| | |
| | | function handleSelectDriver(index: number) { |
| | | selectingDriverForVehicleIndex.value = index |
| | | uni.navigateTo({ |
| | | url: '/pages/choosePerson/index' |
| | | url: `/pages/choosePerson/index?mode=driver&vehicleIndex=${index}${formData.vehicles[index]?.driverVisitorId ? `&selectedId=${formData.vehicles[index].driverVisitorId}` : ''}`, |
| | | }) |
| | | } |
| | | |
| | | function handleSelectVisitor() { |
| | | selectingDriverForVehicleIndex.value = -1 |
| | | const selectedIds = formData.visitors.map(item => item.id).join(',') |
| | | uni.navigateTo({ |
| | | url: '/pages/choosePerson/index' |
| | | url: `/pages/choosePerson/index?mode=pick${selectedIds ? `&selectedIds=${selectedIds}` : ''}`, |
| | | }) |
| | | } |
| | | |
| | | function handleDeleteVisitor(index: number) { |
| | | const target = formData.visitors[index] |
| | | if (!target) { |
| | | return |
| | | } |
| | | const usedByVehicle = formData.vehicles.some(vehicle => vehicle.driverVisitorId === target.id) |
| | | if (usedByVehicle) { |
| | | uni.showToast({ title: '该来访人已作为驾驶员使用,请先删除对应车辆或更换驾驶员', icon: 'none' }) |
| | | return |
| | | } |
| | | formData.visitors.splice(index, 1) |
| | | } |
| | | |
| | |
| | | return gender === 'female' ? appointmentIcons.female : appointmentIcons.male |
| | | } |
| | | |
| | | onLoad(() => { |
| | | uni.$on('acceptVisitor', (visitor: any) => { |
| | | // 正在选择驾驶员 |
| | | if (selectingDriverForVehicleIndex.value > -1) { |
| | | const vehicleIndex = selectingDriverForVehicleIndex.value |
| | | // 设置车辆的驾驶员信息,格式:姓名-手机号(为了保持与原有 UI 的展示兼容,你可以修改这部分展示逻辑) |
| | | formData.vehicles[vehicleIndex].driver = `${visitor.name}-${visitor.phone}` |
| | | |
| | | // 检查下方来访人员中是否已有该人员 |
| | | const exists = formData.visitors.find(v => v.phone === visitor.phone && v.name === visitor.name) |
| | | if (exists) { |
| | | // 如果存在,打上司机标签 |
| | | exists.badge = '驾驶员' |
| | | } else { |
| | | // 如果不存在,添加该人员并打上司机标签 |
| | | formData.visitors.push({ |
| | | name: visitor.name, |
| | | phone: visitor.phone, |
| | | company: visitor.company, |
| | | gender: visitor.gender, |
| | | badge: '驾驶员', |
| | | }) |
| | | } |
| | | |
| | | // 重置选择状态 |
| | | selectingDriverForVehicleIndex.value = -1 |
| | | } else { |
| | | // 普通来访人员选择 |
| | | const exists = formData.visitors.find(v => v.phone === visitor.phone && v.name === visitor.name) |
| | | if (!exists) { |
| | | formData.visitors.push({ |
| | | name: visitor.name, |
| | | phone: visitor.phone, |
| | | company: visitor.company, |
| | | gender: visitor.gender, |
| | | }) |
| | | } |
| | | function mapVisitorFromSelection(visitor: { |
| | | id: number |
| | | uuid?: string |
| | | name: string |
| | | phone?: string |
| | | company?: string |
| | | gender?: 'male' | 'female' |
| | | idCardMasked?: string |
| | | }): AppointmentVisitor { |
| | | return { |
| | | id: visitor.id, |
| | | uuid: visitor.uuid, |
| | | name: visitor.name, |
| | | phone: visitor.phone || '', |
| | | company: visitor.company || '', |
| | | gender: visitor.gender || 'male', |
| | | idCardMasked: visitor.idCardMasked || '', |
| | | } |
| | | } |
| | | |
| | | function normalizeVisitorGender(gender?: string | null): AppointmentVisitor['gender'] { |
| | | return gender === '女' || gender === 'FEMALE' ? 'female' : 'male' |
| | | } |
| | | |
| | | function mapVisitorFromCurrentList( |
| | | visitor: MiniVisitorListItem, |
| | | snapshot?: MiniMyAppointmentDetailRebookVisitorItem, |
| | | ): AppointmentVisitor { |
| | | return { |
| | | id: visitor.id, |
| | | uuid: visitor.uuid, |
| | | name: visitor.visitorName || snapshot?.name || '来访人', |
| | | phone: snapshot?.mobileMasked || '', |
| | | company: visitor.visitorCompany || snapshot?.companyName || '', |
| | | gender: normalizeVisitorGender(snapshot?.gender || visitor.gender), |
| | | idCardMasked: snapshot?.idCardMasked || '', |
| | | } |
| | | } |
| | | |
| | | function buildVehicleDriverLabel(driver: AppointmentVisitor, fallbackName?: string | null) { |
| | | return `${driver.name || fallbackName || '驾驶员'}${driver.phone ? `-${driver.phone}` : ''}` |
| | | } |
| | | |
| | | function parsePlateNo(plateNo?: string | null) { |
| | | const normalized = (plateNo || '').trim().toUpperCase() |
| | | if (!normalized) { |
| | | return createEmptyVehicle() |
| | | } |
| | | const [plateRegion = '', ...rest] = normalized.split('') |
| | | const characters = rest.slice(0, 7) |
| | | while (characters.length < 7) { |
| | | characters.push('') |
| | | } |
| | | return { |
| | | plateRegion, |
| | | plateCharacters: characters, |
| | | plateTag: defaultPlateTag, |
| | | driver: '', |
| | | driverVisitorId: null, |
| | | } satisfies AppointmentVehicle |
| | | } |
| | | |
| | | function upsertVisitor(nextVisitor: AppointmentVisitor, badge?: string) { |
| | | const exists = formData.visitors.find(item => item.id === nextVisitor.id) |
| | | if (exists) { |
| | | exists.name = nextVisitor.name |
| | | exists.phone = nextVisitor.phone |
| | | exists.company = nextVisitor.company |
| | | exists.gender = nextVisitor.gender |
| | | exists.uuid = nextVisitor.uuid |
| | | exists.idCardMasked = nextVisitor.idCardMasked |
| | | exists.badge = badge ?? exists.badge |
| | | return |
| | | } |
| | | formData.visitors.push({ |
| | | ...nextVisitor, |
| | | badge, |
| | | }) |
| | | } |
| | | |
| | | function removeDriverBadgeIfUnused(visitorId?: number | null) { |
| | | if (!visitorId) { |
| | | return |
| | | } |
| | | const stillDriver = formData.vehicles.some(vehicle => vehicle.driverVisitorId === visitorId) |
| | | if (stillDriver) { |
| | | return |
| | | } |
| | | const visitor = formData.visitors.find(item => item.id === visitorId) |
| | | if (visitor?.badge === '驾驶员') { |
| | | delete visitor.badge |
| | | } |
| | | } |
| | | |
| | | function handleVisitorSelection(payload: { |
| | | mode?: 'pick' | 'driver' |
| | | vehicleIndex?: number |
| | | visitors?: Array<{ |
| | | id: number |
| | | uuid?: string |
| | | name: string |
| | | phone?: string |
| | | company?: string |
| | | gender?: 'male' | 'female' |
| | | idCardMasked?: string |
| | | }> |
| | | }) { |
| | | const picked = payload.visitors || [] |
| | | if (payload.mode === 'driver') { |
| | | const vehicleIndex = payload.vehicleIndex ?? selectingDriverForVehicleIndex.value |
| | | const pickedDriver = picked[0] |
| | | if (vehicleIndex < 0 || !pickedDriver || !formData.vehicles[vehicleIndex]) { |
| | | return |
| | | } |
| | | const previousDriverId = formData.vehicles[vehicleIndex].driverVisitorId |
| | | const nextDriver = mapVisitorFromSelection(pickedDriver) |
| | | formData.vehicles[vehicleIndex].driver = `${nextDriver.name}${nextDriver.phone ? `-${nextDriver.phone}` : ''}` |
| | | formData.vehicles[vehicleIndex].driverVisitorId = nextDriver.id |
| | | upsertVisitor(nextDriver, '驾驶员') |
| | | removeDriverBadgeIfUnused(previousDriverId) |
| | | selectingDriverForVehicleIndex.value = -1 |
| | | return |
| | | } |
| | | |
| | | const nextVisitors = picked.map(mapVisitorFromSelection) |
| | | const currentDriverIds = new Set( |
| | | formData.vehicles |
| | | .map(vehicle => vehicle.driverVisitorId) |
| | | .filter((id): id is number => Boolean(id)), |
| | | ) |
| | | |
| | | formData.visitors = nextVisitors.map((visitor) => { |
| | | const existing = formData.visitors.find(item => item.id === visitor.id) |
| | | return { |
| | | ...existing, |
| | | ...visitor, |
| | | badge: currentDriverIds.has(visitor.id) ? '驾驶员' : undefined, |
| | | } |
| | | }) |
| | | } |
| | | |
| | | function parseDate(value: string) { |
| | | return new Date(`${value}T00:00:00`) |
| | | } |
| | | |
| | | function diffDaysInclusive(start: string, end: string) { |
| | | const startDate = parseDate(start) |
| | | const endDate = parseDate(end) |
| | | const diff = endDate.getTime() - startDate.getTime() |
| | | return Math.floor(diff / (24 * 60 * 60 * 1000)) + 1 |
| | | } |
| | | |
| | | function isValidMultiDateRange(start: string, end: string) { |
| | | if (!start || !end) { |
| | | return true |
| | | } |
| | | if (parseDate(end).getTime() < parseDate(start).getTime()) { |
| | | return false |
| | | } |
| | | return diffDaysInclusive(start, end) <= maxConsecutiveDays.value |
| | | } |
| | | |
| | | async function loadBookingSettings() { |
| | | try { |
| | | const result = await fetchSystemSettings('BOOKING_RANGE_MODE,BOOKING_MAX_CONSECUTIVE_DAYS') |
| | | bookingRangeMode.value |
| | | = result.BOOKING_RANGE_MODE?.trim().toUpperCase() === VISIT_TIME_MODE_MULTI |
| | | ? VISIT_TIME_MODE_MULTI |
| | | : VISIT_TIME_MODE_SINGLE |
| | | |
| | | const parsedDays = Number.parseInt(result.BOOKING_MAX_CONSECUTIVE_DAYS?.trim() || '', 10) |
| | | maxConsecutiveDays.value = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : 3 |
| | | |
| | | if (!allowMulti.value && formData.dateType === 'multi') { |
| | | formData.dateType = 'single' |
| | | formData.visitTimeMultiStart = '' |
| | | formData.visitTimeMultiEnd = '' |
| | | } |
| | | } |
| | | catch (error) { |
| | | bookingRangeMode.value = VISIT_TIME_MODE_SINGLE |
| | | maxConsecutiveDays.value = 3 |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载预约配置失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | } |
| | | |
| | | async function loadVisitReasons() { |
| | | try { |
| | | visitReasons.value = await fetchVisitReasons() |
| | | if (selectedVisitReason.value) { |
| | | const nextReason = visitReasons.value.find(item => item.reasonCode === selectedVisitReason.value?.reasonCode) || null |
| | | selectedVisitReason.value = nextReason |
| | | formData.reason = nextReason?.reasonName || '' |
| | | } |
| | | if (!reasonPickerValue.value) { |
| | | reasonPickerValue.value = formData.reason || visitReasons.value[0]?.reasonName || '' |
| | | } |
| | | } |
| | | catch (error) { |
| | | visitReasons.value = [] |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载来访事由失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | } |
| | | |
| | | async function resolveVisitedUser() { |
| | | const keyword = formData.interviewee.trim() |
| | | if (!keyword) { |
| | | throw new Error('请输入被访人') |
| | | } |
| | | if (selectedVisitedUser.value?.realName === keyword) { |
| | | return selectedVisitedUser.value |
| | | } |
| | | const users = await searchVisitedUsersByExactName(keyword) |
| | | if (!users.length) { |
| | | throw new Error('未匹配到被访人') |
| | | } |
| | | if (users.length > 1) { |
| | | throw new Error('存在多个同名被访人,请输入更准确姓名') |
| | | } |
| | | selectedVisitedUser.value = users[0] |
| | | return users[0] |
| | | } |
| | | |
| | | function selectReasonByName(reasonName: string) { |
| | | const target = visitReasons.value.find(item => item.reasonName === reasonName) || null |
| | | selectedVisitReason.value = target |
| | | formData.reason = target?.reasonName || '' |
| | | if (!showCustomReasonInput.value) { |
| | | customVisitReason.value = '' |
| | | } |
| | | } |
| | | |
| | | function resolveVisitTimeMode() { |
| | | return formData.dateType === 'multi' ? VISIT_TIME_MODE_MULTI : VISIT_TIME_MODE_SINGLE |
| | | } |
| | | |
| | | function buildVehiclePlateNo(vehicle: AppointmentVehicle) { |
| | | return `${vehicle.plateRegion}${vehicle.plateCharacters.join('')}`.trim().toUpperCase() |
| | | } |
| | | |
| | | function resetForm() { |
| | | formData.interviewee = '' |
| | | formData.reason = '' |
| | | formData.dateType = 'single' |
| | | formData.visitTimeSingle = '' |
| | | formData.visitTimeMultiStart = '' |
| | | formData.visitTimeMultiEnd = '' |
| | | formData.vehicleEntry = false |
| | | formData.visitors = [] |
| | | formData.vehicles = [] |
| | | visitedOptions.value = [] |
| | | selectedVisitReason.value = null |
| | | selectedVisitedUser.value = null |
| | | customVisitReason.value = '' |
| | | selectingDriverForVehicleIndex.value = -1 |
| | | } |
| | | |
| | | async function prefillFromRebookDetail() { |
| | | if (!rebookOrderId.value || !rebookOrderUuid.value) { |
| | | return |
| | | } |
| | | try { |
| | | const [detailData, currentVisitors] = await Promise.all([ |
| | | fetchMyAppointmentDetail(rebookOrderId.value, rebookOrderUuid.value), |
| | | fetchMyVisitors(), |
| | | ]) |
| | | await applyRebookPrefill(detailData, currentVisitors) |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载再次预约数据失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | } |
| | | } |
| | | |
| | | async function applyRebookPrefill(detailData: MiniMyAppointmentDetailItem, currentVisitors: MiniVisitorListItem[]) { |
| | | const warnings: string[] = [] |
| | | resetForm() |
| | | |
| | | formData.interviewee = detailData.visitedUserName || '' |
| | | selectedVisitedUser.value = null |
| | | visitedOptions.value = [] |
| | | |
| | | const visitedKeyword = detailData.visitedUserName?.trim() |
| | | if (visitedKeyword) { |
| | | try { |
| | | const users = await searchVisitedUsersByExactName(visitedKeyword) |
| | | const matched = users.find(item => item.id === detailData.visitedUserId) |
| | | if (matched) { |
| | | selectedVisitedUser.value = matched |
| | | formData.interviewee = matched.realName |
| | | } |
| | | else { |
| | | warnings.push('原被访人不可用,请重新选择') |
| | | } |
| | | } |
| | | catch { |
| | | warnings.push('原被访人不可用,请重新选择') |
| | | } |
| | | } |
| | | |
| | | const reasonByCode = visitReasons.value.find(item => item.reasonCode === detailData.visitReasonCode) || null |
| | | if (reasonByCode) { |
| | | selectedVisitReason.value = reasonByCode |
| | | formData.reason = reasonByCode.reasonName |
| | | reasonPickerValue.value = reasonByCode.reasonName |
| | | } |
| | | else { |
| | | selectedVisitReason.value = null |
| | | formData.reason = '' |
| | | if (detailData.visitReasonCode) { |
| | | warnings.push('原拜访事由不可用,请重新选择') |
| | | } |
| | | } |
| | | customVisitReason.value = detailData.visitReasonCode === VISIT_REASON_CODE_OTHER |
| | | ? detailData.visitReasonOther || '' |
| | | : '' |
| | | |
| | | const currentVisitorMap = new Map(currentVisitors.map(item => [item.id, item])) |
| | | const restoredVisitors: AppointmentVisitor[] = [] |
| | | const missingVisitors: string[] = [] |
| | | for (const item of detailData.rebookVisitors || []) { |
| | | const current = currentVisitorMap.get(item.visitorId) |
| | | if (!current) { |
| | | missingVisitors.push(item.name || '来访人') |
| | | continue |
| | | } |
| | | restoredVisitors.push(mapVisitorFromCurrentList(current, item)) |
| | | } |
| | | formData.visitors = restoredVisitors |
| | | |
| | | if (missingVisitors.length > 0) { |
| | | warnings.push(`已跳过${missingVisitors.length}位失效来访人`) |
| | | } |
| | | |
| | | formData.vehicleEntry = Boolean(detailData.vehicleEntry) |
| | | const restoredVisitorMap = new Map(formData.visitors.map(item => [item.id, item])) |
| | | const vehicleWarnings: string[] = [] |
| | | formData.vehicles = (detailData.rebookVehicles || []).map((item) => { |
| | | const vehicle = parsePlateNo(item.plateNo) |
| | | const driver = item.driverVisitorId ? restoredVisitorMap.get(item.driverVisitorId) : undefined |
| | | if (driver) { |
| | | vehicle.driverVisitorId = driver.id |
| | | vehicle.driver = buildVehicleDriverLabel(driver, item.driverName) |
| | | driver.badge = '驾驶员' |
| | | } |
| | | else if (item.driverVisitorId) { |
| | | vehicle.driver = item.driverName || '' |
| | | vehicleWarnings.push(item.plateNo || '车辆') |
| | | } |
| | | return vehicle |
| | | }) |
| | | if (formData.vehicleEntry && formData.vehicles.length === 0) { |
| | | formData.vehicles = [createEmptyVehicle()] |
| | | } |
| | | if (vehicleWarnings.length > 0) { |
| | | warnings.push(`部分车辆驾驶员失效,请重新选择驾驶员`) |
| | | } |
| | | |
| | | formData.dateType = 'single' |
| | | formData.visitTimeSingle = '' |
| | | formData.visitTimeMultiStart = '' |
| | | formData.visitTimeMultiEnd = '' |
| | | |
| | | if (warnings.length > 0) { |
| | | uni.showToast({ |
| | | title: warnings.join(';'), |
| | | icon: 'none', |
| | | duration: 3000, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | async function buildAppointmentPayload(): Promise<MiniAppointmentCreateRequest> { |
| | | const visitedUser = await resolveVisitedUser() |
| | | if (!selectedVisitReason.value?.reasonCode) { |
| | | throw new Error('请选择拜访事由') |
| | | } |
| | | if (showCustomReasonInput.value && !customVisitReason.value.trim()) { |
| | | throw new Error('请输入具体来访事由') |
| | | } |
| | | if (!formData.visitors.length) { |
| | | throw new Error('请选择来访人员') |
| | | } |
| | | |
| | | const visitTimeMode = resolveVisitTimeMode() |
| | | const payload: MiniAppointmentCreateRequest = { |
| | | visitedUserId: visitedUser.id, |
| | | visitReasonCode: selectedVisitReason.value.reasonCode, |
| | | visitTimeMode, |
| | | personEntry: true, |
| | | visitorIds: formData.visitors.map(item => item.id), |
| | | vehicleEntry: formData.vehicleEntry, |
| | | } |
| | | if (showCustomReasonInput.value) { |
| | | payload.visitReasonOther = customVisitReason.value.trim() |
| | | } |
| | | |
| | | if (visitTimeMode === VISIT_TIME_MODE_SINGLE) { |
| | | if (!formData.visitTimeSingle) { |
| | | throw new Error('请选择拜访时间') |
| | | } |
| | | const [visitDate, visitTimeLabel = ''] = formData.visitTimeSingle.split(' ') |
| | | const visitTimeSlot = TIME_SLOT_LABEL_TO_CODE[visitTimeLabel as keyof typeof TIME_SLOT_LABEL_TO_CODE] |
| | | if (!visitDate || !visitTimeSlot) { |
| | | throw new Error('请选择有效的单日拜访时间') |
| | | } |
| | | payload.visitDate = visitDate |
| | | payload.visitTimeSlot = visitTimeSlot |
| | | } else { |
| | | if (!allowMulti.value) { |
| | | throw new Error('当前预约配置不支持多日') |
| | | } |
| | | if (!formData.visitTimeMultiStart || !formData.visitTimeMultiEnd) { |
| | | throw new Error('请选择开始时间和结束时间') |
| | | } |
| | | if (!isValidMultiDateRange(formData.visitTimeMultiStart, formData.visitTimeMultiEnd)) { |
| | | throw new Error(`结束日期最长可选${maxConsecutiveDays.value}天内`) |
| | | } |
| | | payload.visitStartDate = formData.visitTimeMultiStart |
| | | payload.visitEndDate = formData.visitTimeMultiEnd |
| | | } |
| | | |
| | | if (payload.vehicleEntry) { |
| | | if (!formData.vehicles.length) { |
| | | throw new Error('请添加进场车辆') |
| | | } |
| | | payload.vehicles = formData.vehicles.map((item) => { |
| | | const plateNo = buildVehiclePlateNo(item) |
| | | if (!plateNo || !item.driverVisitorId) { |
| | | throw new Error('请完善车辆信息') |
| | | } |
| | | return { |
| | | plateNo, |
| | | driverVisitorId: item.driverVisitorId, |
| | | } |
| | | }) |
| | | } |
| | | |
| | | return payload |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | try { |
| | | const payload = await buildAppointmentPayload() |
| | | await openParkNotice(payload) |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '请填写完整必填信息', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | } |
| | | |
| | | async function handlePromiseConfirm() { |
| | | if (countdown.value > 0 || !pendingSubmitPayload.value) { |
| | | return |
| | | } |
| | | |
| | | showPromisePopup.value = false |
| | | isSubmitting.value = true |
| | | try { |
| | | await requestApprovalResultSubscribeAuthorization() |
| | | const orderId = await createAppointment(pendingSubmitPayload.value) |
| | | resetForm() |
| | | pendingSubmitPayload.value = null |
| | | uni.redirectTo({ url: `/pages/success/index?orderId=${orderId}` }) |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '提交预约失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | isSubmitting.value = false |
| | | } |
| | | } |
| | | |
| | | async function requestApprovalResultSubscribeAuthorization() { |
| | | // #ifdef MP-WEIXIN |
| | | try { |
| | | await new Promise<void>((resolve) => { |
| | | uni.requestSubscribeMessage({ |
| | | tmplIds: [APPROVAL_RESULT_SUBSCRIBE_TEMPLATE_ID], |
| | | complete: () => resolve(), |
| | | }) |
| | | }) |
| | | } |
| | | catch (error) { |
| | | console.warn('requestSubscribeMessage failed', error) |
| | | } |
| | | // #endif |
| | | } |
| | | |
| | | watch( |
| | | () => formData.interviewee, |
| | | (keyword) => { |
| | | if (visitedSearchTimer) { |
| | | clearTimeout(visitedSearchTimer) |
| | | visitedSearchTimer = null |
| | | } |
| | | const trimmedKeyword = keyword.trim() |
| | | if (!trimmedKeyword) { |
| | | visitedOptions.value = [] |
| | | selectedVisitedUser.value = null |
| | | return |
| | | } |
| | | if (selectedVisitedUser.value?.realName === trimmedKeyword) { |
| | | return |
| | | } |
| | | selectedVisitedUser.value = null |
| | | visitedSearchTimer = setTimeout(() => { |
| | | void handleSearchVisitedUser() |
| | | }, 300) |
| | | }, |
| | | ) |
| | | |
| | | watch( |
| | | () => showCustomReasonInput.value, |
| | | (visible) => { |
| | | if (!visible) { |
| | | customVisitReason.value = '' |
| | | } |
| | | }, |
| | | ) |
| | | |
| | | onLoad((options) => { |
| | | uni.$on('selectVisitors', handleVisitorSelection) |
| | | rebookOrderId.value = options?.rebookOrderId ? Number(options.rebookOrderId) : null |
| | | rebookOrderUuid.value = options?.rebookOrderUuid ? decodeURIComponent(options.rebookOrderUuid) : '' |
| | | void Promise.all([ |
| | | loadBookingSettings(), |
| | | loadVisitReasons(), |
| | | ]).then(() => prefillFromRebookDetail()) |
| | | }) |
| | | |
| | | onUnload(() => { |
| | | uni.$off('acceptVisitor') |
| | | uni.$off('selectVisitors', handleVisitorSelection) |
| | | if (visitedSearchTimer) { |
| | | clearTimeout(visitedSearchTimer) |
| | | visitedSearchTimer = null |
| | | } |
| | | if (timer) { |
| | | clearInterval(timer) |
| | | timer = null |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | |
| | | </view> |
| | | </template> |
| | | </wd-input> |
| | | <view v-if="selectedVisitedUser" class="px-[20rpx] pb-[16rpx] text-right text-[22rpx] text-[#999999]"> |
| | | 已选:{{ selectedVisitedUser.realName }}{{ selectedVisitedUser.deptName ? ` / ${selectedVisitedUser.deptName}` : '' }} |
| | | </view> |
| | | <view v-if="visitedOptions.length" class="px-[20rpx] pb-[16rpx]"> |
| | | <view |
| | | v-for="item in visitedOptions" |
| | | :key="item.id" |
| | | class="mb-[12rpx] rounded-[12rpx] bg-[#F5F8FF] px-[24rpx] py-[18rpx] text-right text-[24rpx] text-[#333333]" |
| | | @click="selectVisitedUser(item)" |
| | | > |
| | | {{ item.realName }}{{ item.deptName ? ` / ${item.deptName}` : '' }} |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 拜访事由 --> |
| | | <view class="custom-cell flex items-center justify-between" @click="showReasonPicker = true"> |
| | | <view class="custom-cell flex items-center justify-between" @click="openReasonPicker"> |
| | | <view class="inline-flex items-start"> |
| | | <text class="text-[28rpx] text-[#333333]">拜访事由</text> |
| | | <text class="text-[24rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text> |
| | |
| | | </text> |
| | | <wd-icon name="arrow-right" size="32rpx" color="#999999" custom-class="ml-[8rpx]" /> |
| | | </view> |
| | | </view> |
| | | <view v-if="showCustomReasonInput" class="custom-cell"> |
| | | <view class="mb-[20rpx] inline-flex items-start"> |
| | | <text class="text-[28rpx] text-[#333333]">事由说明</text> |
| | | <text class="text-[24rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text> |
| | | </view> |
| | | <textarea |
| | | v-model.trim="customVisitReason" |
| | | class="min-h-[160rpx] w-full rounded-[16rpx] bg-[#F8FAFD] px-[24rpx] py-[20rpx] text-[26rpx] text-[#333333] box-border" |
| | | maxlength="100" |
| | | placeholder="请输入具体来访事由" |
| | | placeholder-class="text-[#B3B3B3]" |
| | | /> |
| | | </view> |
| | | |
| | | <!-- 拜访时间类型切换 --> |
| | |
| | | class="h-[48rpx] w-[240rpx] flex rounded-full bg-white box-border border border-[#0068FF] overflow-hidden"> |
| | | <view class="flex w-[120rpx] items-center justify-center text-[24rpx] transition-all" |
| | | :class="formData.dateType === 'single' ? 'bg-[#0068FF] text-white' : ' border bg-transparent text-[#0068FF] border-r border-[#0068FF]'" |
| | | @click="formData.dateType = 'single'"> |
| | | @click="handleDateTypeChange('single')"> |
| | | 单日 |
| | | </view> |
| | | <view class="flex w-[120rpx] items-center justify-center text-[24rpx] transition-all" |
| | | :class="formData.dateType === 'multi' ? 'bg-[#0068FF] text-white' : ' border bg-transparent text-[#0068FF]'" |
| | | @click="formData.dateType = 'multi'"> |
| | | :class="[ |
| | | formData.dateType === 'multi' ? 'bg-[#0068FF] text-white' : 'border bg-transparent text-[#0068FF]', |
| | | !allowMulti ? 'opacity-40' : '', |
| | | ]" |
| | | @click="handleDateTypeChange('multi')"> |
| | | 多日 |
| | | </view> |
| | | </view> |
| | |
| | | <wd-icon name="arrow-right" size="32rpx" color="#999999" custom-class="ml-[8rpx]" /> |
| | | </view> |
| | | </view> |
| | | <view class="px-[20rpx] py-[16rpx] text-right text-[22rpx] text-[#999999]"> |
| | | 结束日期最长可选 {{ maxConsecutiveDays }} 天内 |
| | | </view> |
| | | </template> |
| | | </wd-form> |
| | | </view> |
| | |
| | | <view class="inline-flex items-start"> |
| | | <text class="text-[28rpx] text-[#333333]">进场类型</text> |
| | | </view> |
| | | <view class="flex justify-end items-center h-full"> |
| | | <view class="flex items-center gap-[32rpx]"> |
| | | <view class="flex items-center" @click="handleEntryTypeChange('person')"> |
| | | <view class="flex justify-end items-center h-full"> |
| | | <view class="flex items-center gap-[32rpx]"> |
| | | <view class="flex items-center"> |
| | | <view |
| | | class="flex items-center justify-center w-[28rpx] h-[28rpx] rounded-full border-[2rpx] border-solid transition-colors duration-200" |
| | | :class="formData.entryType === 'person' ? 'border-[#1677FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="formData.entryType === 'person'" class="w-[16rpx] h-[16rpx] rounded-full bg-[#1677FF]"> |
| | | :class="'border-[#1677FF]'"> |
| | | <view class="w-[16rpx] h-[16rpx] rounded-full bg-[#1677FF]"> |
| | | </view> |
| | | </view> |
| | | <text class="ml-[12rpx] text-[28rpx]" :class="formData.entryType === 'person' ? 'text-[#333333]' : 'text-[#999999]'">人员进场</text> |
| | | <text class="ml-[12rpx] text-[28rpx] text-[#333333]">人员进场</text> |
| | | </view> |
| | | <view class="flex items-center" @click="handleEntryTypeChange('vehicle')"> |
| | | <view class="flex items-center" @click="handleVehicleEntryChange(!formData.vehicleEntry)"> |
| | | <view |
| | | class="flex items-center justify-center w-[28rpx] h-[28rpx] rounded-full border-[2rpx] border-solid transition-colors duration-200" |
| | | :class="formData.entryType === 'vehicle' ? 'border-[#1677FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="formData.entryType === 'vehicle'" class="w-[16rpx] h-[16rpx] rounded-full bg-[#1677FF]"> |
| | | :class="formData.vehicleEntry ? 'border-[#1677FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="formData.vehicleEntry" class="w-[16rpx] h-[16rpx] rounded-full bg-[#1677FF]"> |
| | | </view> |
| | | </view> |
| | | <text class="ml-[12rpx] text-[28rpx]" :class="formData.entryType === 'vehicle' ? 'text-[#333333]' : 'text-[#999999]'">车辆进场</text> |
| | | <text class="ml-[12rpx] text-[28rpx]" :class="formData.vehicleEntry ? 'text-[#333333]' : 'text-[#999999]'">车辆进场</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="formData.entryType === 'vehicle'" class="mb-[24rpx] rounded-[16rpx] bg-white shadow-[0px_4px_8px_0px_rgba(6,47,108,0.1608)]"> |
| | | <view v-if="formData.vehicleEntry" class="mb-[24rpx] rounded-[16rpx] bg-white shadow-[0px_4px_8px_0px_rgba(6,47,108,0.1608)]"> |
| | | <view class="px-[32rpx] py-[28rpx] border-b1"> |
| | | <text class="text-[28rpx] text-[#333333] font-medium">进场车辆</text> |
| | | </view> |
| | |
| | | <view v-if="formData.visitors.length > 0" class="mb-[16rpx]"> |
| | | <view |
| | | v-for="(visitor, index) in formData.visitors" |
| | | :key="`${visitor.name}-${index}`" |
| | | :key="`${visitor.id}-${index}`" |
| | | class="visitor-row flex items-center justify-between px-[8rpx] py-[20rpx]" |
| | | > |
| | | <view class="flex items-center min-w-0 flex-1"> |
| | |
| | | <view class="flex items-center min-w-0"> |
| | | <image :src="getVisitorGenderIcon(visitor.gender)" class="h-[32rpx] w-[32rpx] shrink-0" /> |
| | | <text class="ml-[10rpx] truncate text-[24rpx] text-[#999999]"> |
| | | {{ visitor.phone }} |
| | | {{ visitor.phone || visitor.idCardMasked || '-' }} |
| | | </text> |
| | | </view> |
| | | <view class="mt-[8rpx] flex items-center min-w-0"> |
| | | <image :src="appointmentIcons.company" class="h-[32rpx] w-[32rpx] shrink-0" /> |
| | | <text class="ml-[10rpx] truncate text-[24rpx] text-[#B3B3B3]"> |
| | | {{ visitor.company }} |
| | | {{ visitor.company || '-' }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | |
| | | <!-- 弹窗内容区 (选择器) --> |
| | | <view class="px-[32rpx] flex-1 flex flex-col justify-center"> |
| | | <wd-picker-view |
| | | v-model="formData.reason" |
| | | v-model="reasonPickerValue" |
| | | :columns="reasonColumns" |
| | | :item-height="48" |
| | | :columns-height="144" |
| | |
| | | <!-- 弹窗底部按钮 --> |
| | | <view class="px-[32rpx] pb-[48rpx] shrink-0"> |
| | | <wd-button type="primary" size="large" block custom-class="popup-submit-btn" |
| | | @click="showReasonPicker = false"> |
| | | @click="handleReasonConfirm"> |
| | | 提交 |
| | | </wd-button> |
| | | </view> |
| | |
| | | |
| | | <!-- 承诺内容滚动区 --> |
| | | <scroll-view scroll-y class="relative z-10 px-[40rpx] flex-1 min-h-0 box-border"> |
| | | <text class="text-[26rpx] text-[#666666] leading-[44rpx] block text-justify"> |
| | | 确认入园承诺,内容后台配置(小程序显示5秒倒计时后才能进行下一步) |
| | | 确认入园承诺,内容后台配置(小程序显示5秒倒。计时后才能进行下一步)确认入园承诺,内容后台配置(小程序显示5秒倒计时后才能进行下一步)确认入园承诺,内容后台配置(小程序显示5秒倒计时后才能进行下一步) |
| | | 确认入园承诺,内容后台配置(小程序显示5秒倒计时后才能进行下一步) |
| | | 确认入园承诺,内容后台配置(小程序显示5秒倒。计时后才能进行下一步)确认入园承诺,内容后台配置(小程序显示5秒倒。计时后才能进行下一步) |
| | | 确认入园承诺,内容后台配置(小程序显示5秒倒。计时后才能进行下一步) |
| | | </text> |
| | | <rich-text |
| | | class="promise-html text-[26rpx] text-[#666666] leading-[44rpx] block" |
| | | :nodes="promiseHtml" |
| | | /> |
| | | </scroll-view> |
| | | |
| | | <!-- 底部按钮区 --> |
| | |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); |
| | | } |
| | | |
| | | .promise-html { |
| | | word-break: break-word; |
| | | } |
| | | |
| | | .visitor-row + .visitor-row { |
| | | border-top: 1px solid #F0F0F0; |
| | | } |