<script setup lang="ts">
|
import { computed, reactive, ref, watch } from 'vue'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { createAppointment, fetchMyAppointmentDetail } from '@/api/appointment'
|
import {
|
fetchMiniFieldConfigs,
|
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 { MiniFieldConfigItem, MiniVisitReasonItem, MiniVisitedUserItem } from '@/types/lookup'
|
import type { MiniVisitorListItem } from '@/types/visitor'
|
|
definePage({
|
name: 'appointment',
|
layout: 'default',
|
style: {
|
navigationStyle: 'custom',
|
},
|
})
|
|
const router = useRouter()
|
const appointmentAssetBase = '/static/appointment'
|
const defaultPlateTag = '新能源'
|
const VISIT_TIME_MODE_SINGLE = 'SINGLE'
|
const VISIT_TIME_MODE_MULTI = 'MULTI'
|
const APPROVAL_RESULT_SUBSCRIBE_TEMPLATE_ID = 'rIJpfN51_q818RTpJAqTe5ssTwWCHnxDM5rpq64uuKM'
|
const TIME_SLOT_LABEL_TO_CODE = {
|
'全天00:00-23:59': 'ALL_DAY',
|
'上午00:00-11:59': 'AM',
|
'下午12:00-23:59': 'PM',
|
} as const
|
const APPOINTMENT_FIELD_CODES = {
|
RESPONDENT: 'respondent',
|
VISIT_REASON: 'visit_reason',
|
VISIT_START_TIME: 'visit_start_time',
|
LICENSE_PLATE: 'license_plate',
|
DRIVER: 'driver',
|
} 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
|
}
|
|
// 状态栏高度
|
const sysInfo = uni.getSystemInfoSync()
|
const statusBarHeight = sysInfo.statusBarHeight || 0
|
const navBarHeight = 44
|
|
// 表单数据
|
const formData = reactive({
|
interviewee: '',
|
reason: '',
|
dateType: 'single', // single 单日 | multi 多日
|
visitTimeSingle: '',
|
visitTimeMultiStart: '',
|
visitTimeMultiEnd: '',
|
vehicleEntry: false,
|
visitors: [] as AppointmentVisitor[],
|
vehicles: [] as AppointmentVehicle[],
|
})
|
|
const visitReasons = ref<MiniVisitReasonItem[]>([])
|
const fieldConfigs = ref<MiniFieldConfigItem[]>([])
|
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 isVisitReasonRequired = computed(() => isFieldRequired(APPOINTMENT_FIELD_CODES.VISIT_REASON, true))
|
const isRespondentRequired = computed(() => isFieldRequired(APPOINTMENT_FIELD_CODES.RESPONDENT, true))
|
const isVisitTimeRequired = computed(() => isFieldRequired(APPOINTMENT_FIELD_CODES.VISIT_START_TIME, true))
|
const showVehicleEntryOption = computed(() =>
|
isFieldVisible(APPOINTMENT_FIELD_CODES.LICENSE_PLATE, true)
|
|| isFieldVisible(APPOINTMENT_FIELD_CODES.DRIVER, true),
|
)
|
const showCustomReasonInput = computed(() => Boolean(selectedVisitReason.value))
|
const showReasonPicker = ref(false)
|
const appointmentIcons = {
|
male: `${appointmentAssetBase}/man.png`,
|
female: `${appointmentAssetBase}/woman.png`,
|
company: `${appointmentAssetBase}/company.png`,
|
delete: `${appointmentAssetBase}/delete.png`,
|
}
|
const focusedVehicleIndex = ref(-1)
|
const focusedCharIndex = ref(-1)
|
const plateKeyboardVehicleIndex = ref(-1)
|
const showPlateKeyboard = ref(false)
|
const plateKeyboardLang = ref<'zh' | 'en'>('zh')
|
const plateKeyboardValue = ref('')
|
const rebookOrderId = ref<number | null>(null)
|
const rebookOrderUuid = ref('')
|
|
function formatDatePart(value: number | string) {
|
return String(value).padStart(2, '0')
|
}
|
|
function findFieldConfig(fieldCode: string) {
|
return fieldConfigs.value.find(item => item.fieldCode === fieldCode)
|
}
|
|
function isFieldRequired(fieldCode: string, defaultValue = false) {
|
const config = findFieldConfig(fieldCode)
|
if (!config) {
|
return defaultValue
|
}
|
return config.requiredFlag === 1
|
}
|
|
function isFieldVisible(fieldCode: string, defaultValue = true) {
|
const config = findFieldConfig(fieldCode)
|
if (!config) {
|
return defaultValue
|
}
|
return config.visible === 1
|
}
|
|
function handlePlateCharClick(vehicleIndex: number, charIndex: number) {
|
focusedVehicleIndex.value = vehicleIndex
|
focusedCharIndex.value = charIndex
|
const vehicle = formData.vehicles[vehicleIndex]
|
if (!vehicle) {
|
return
|
}
|
plateKeyboardVehicleIndex.value = vehicleIndex
|
plateKeyboardValue.value = buildVehiclePlateNo(vehicle)
|
plateKeyboardLang.value = charIndex === 0 ? 'zh' : 'en'
|
showPlateKeyboard.value = true
|
}
|
|
function isPlateInputFocused(vehicleIndex: number, charIndex: number) {
|
return focusedVehicleIndex.value === vehicleIndex && focusedCharIndex.value === charIndex
|
}
|
|
function focusNextPlateChar(vehicleIndex: number, charIndex: number) {
|
focusedVehicleIndex.value = vehicleIndex
|
focusedCharIndex.value = charIndex
|
}
|
|
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
|
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) {
|
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)
|
}
|
}
|
|
function handleIntervieweeInput(e: any) {
|
formData.interviewee = e?.detail?.value || ''
|
}
|
|
function handlePlateDelete(vehicleIndex: number, charIndex: number) {
|
const vehicle = formData.vehicles[vehicleIndex]
|
if (charIndex === 0) {
|
vehicle.plateRegion = ''
|
} else {
|
vehicle.plateCharacters[charIndex - 1] = ''
|
}
|
|
// 自动跳到前一个输入框
|
if (charIndex > 0) {
|
focusNextPlateChar(vehicleIndex, charIndex - 1)
|
}
|
}
|
|
// 提交 loading 状态
|
const isSubmitting = ref(false)
|
const pendingSubmitPayload = ref<MiniAppointmentCreateRequest | null>(null)
|
|
// 拜访时间弹窗控制
|
const showTimePicker = ref(false)
|
const showMultiDatePicker = ref(false)
|
const timeSlotOptions = [
|
{
|
label: '全天00:00-23:59',
|
endHour: 23,
|
endMinute: 59,
|
},
|
{
|
label: '上午00:00-11:59',
|
endHour: 11,
|
endMinute: 59,
|
},
|
{
|
label: '下午12:00-23:59',
|
endHour: 23,
|
endMinute: 59,
|
},
|
]
|
const timeColumns = ref<string[][]>([
|
[], // 年
|
[], // 月
|
[], // 日
|
[], // 时段
|
])
|
|
const timePickerIndex = ref([0, 0, 0, 0])
|
|
// 记录当前弹窗是为哪个字段服务的
|
const activeTimeField = ref<'single' | 'start' | 'end'>('single')
|
const multiDateColumns = ref<string[][]>([
|
[], // 年
|
[], // 月
|
[], // 日
|
])
|
const multiDatePickerIndex = ref([0, 0, 0])
|
const activeMultiDateField = ref<'start' | 'end'>('start')
|
|
const getCurrentDate = () => new Date()
|
|
function getDaysInMonth(year: number, month: number) {
|
return new Date(year, month, 0).getDate()
|
}
|
|
function getAvailableTimeSlots(year: number, month: number, day: number) {
|
const now = getCurrentDate()
|
const isToday = year === now.getFullYear()
|
&& month === now.getMonth() + 1
|
&& day === now.getDate()
|
|
if (!isToday) {
|
return timeSlotOptions.map(option => option.label)
|
}
|
|
const currentMinutes = now.getHours() * 60 + now.getMinutes()
|
return timeSlotOptions
|
.filter(option => option.endHour * 60 + option.endMinute > currentMinutes)
|
.map(option => option.label)
|
}
|
|
function updateTimeColumns(selection?: {
|
year?: number
|
month?: number
|
day?: number
|
period?: string
|
}) {
|
const now = getCurrentDate()
|
const currentYear = now.getFullYear()
|
const currentMonth = now.getMonth() + 1
|
const currentDay = now.getDate()
|
|
const years = Array.from({ length: 11 }, (_, index) => (currentYear + index).toString())
|
const selectedYear = selection?.year && years.includes(selection.year.toString())
|
? selection.year
|
: currentYear
|
|
const monthStart = selectedYear === currentYear ? currentMonth : 1
|
const months = Array.from({ length: 12 - monthStart + 1 }, (_, index) => (monthStart + index).toString())
|
const selectedMonth = selection?.month && months.includes(selection.month.toString())
|
? selection.month
|
: monthStart
|
|
const dayStart = selectedYear === currentYear && selectedMonth === currentMonth ? currentDay : 1
|
const maxDay = getDaysInMonth(selectedYear, selectedMonth)
|
const days = Array.from({ length: maxDay - dayStart + 1 }, (_, index) => (dayStart + index).toString())
|
const selectedDay = selection?.day && days.includes(selection.day.toString())
|
? selection.day
|
: dayStart
|
|
const periods = getAvailableTimeSlots(selectedYear, selectedMonth, selectedDay)
|
const selectedPeriod = selection?.period && periods.includes(selection.period)
|
? selection.period
|
: periods[0] || ''
|
|
// 如果是多日选择,可能不需要时段,这里根据需要决定是否清空 periods
|
// 如果 UI 图中多日不需要时段,可以在这里处理
|
const finalPeriods = activeTimeField.value === 'single' ? periods : periods
|
|
timeColumns.value = [years, months, days, finalPeriods]
|
timePickerIndex.value = [
|
years.findIndex(item => item === selectedYear.toString()),
|
months.findIndex(item => item === selectedMonth.toString()),
|
days.findIndex(item => item === selectedDay.toString()),
|
Math.max(periods.findIndex(item => item === selectedPeriod), 0),
|
]
|
}
|
|
function initTimeColumns() {
|
updateTimeColumns()
|
}
|
|
initTimeColumns()
|
|
function updateMultiDateColumns(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 + index).toString())
|
const selectedYear = selection?.year && years.includes(selection.year.toString())
|
? selection.year
|
: currentYear
|
|
const monthStart = selectedYear === currentYear ? currentMonth : 1
|
const months = Array.from({ length: 12 - monthStart + 1 }, (_, index) => (monthStart + index).toString())
|
const selectedMonth = selection?.month && months.includes(selection.month.toString())
|
? selection.month
|
: monthStart
|
|
const dayStart = selectedYear === currentYear && selectedMonth === currentMonth ? currentDay : 1
|
const maxDay = getDaysInMonth(selectedYear, selectedMonth)
|
const days = Array.from({ length: maxDay - dayStart + 1 }, (_, index) => (dayStart + index).toString())
|
const selectedDay = selection?.day && days.includes(selection.day.toString())
|
? selection.day
|
: dayStart
|
|
multiDateColumns.value = [years, months, days]
|
multiDatePickerIndex.value = [
|
years.findIndex(item => item === selectedYear.toString()),
|
months.findIndex(item => item === selectedMonth.toString()),
|
days.findIndex(item => item === selectedDay.toString()),
|
]
|
}
|
|
function initMultiDateColumns() {
|
updateMultiDateColumns()
|
}
|
|
initMultiDateColumns()
|
|
function handleTimeChange(e: any) {
|
const selectedIndex = e.detail.value as number[]
|
const selection = {
|
year: Number(timeColumns.value[0][selectedIndex[0]]),
|
month: Number(timeColumns.value[1][selectedIndex[1]]),
|
day: Number(timeColumns.value[2][selectedIndex[2]]),
|
period: timeColumns.value[3][selectedIndex[3]],
|
}
|
|
updateTimeColumns(selection)
|
}
|
|
function openTimePicker(field: 'single' | 'start' | 'end' = 'single') {
|
activeTimeField.value = field
|
let timeValue = ''
|
if (field === 'single') {
|
timeValue = formData.visitTimeSingle
|
} else if (field === 'start') {
|
timeValue = formData.visitTimeMultiStart
|
} else {
|
timeValue = formData.visitTimeMultiEnd
|
}
|
|
const [datePart, ...timeParts] = timeValue.split(' ')
|
const [year, month, day] = datePart ? datePart.split('-').map(Number) : []
|
const period = timeParts.join(' ')
|
|
updateTimeColumns({
|
year,
|
month,
|
day,
|
period,
|
})
|
|
showTimePicker.value = true
|
}
|
|
function handleMultiDateChange(e: any) {
|
const selectedIndex = e.detail.value as number[]
|
updateMultiDateColumns({
|
year: Number(multiDateColumns.value[0][selectedIndex[0]]),
|
month: Number(multiDateColumns.value[1][selectedIndex[1]]),
|
day: Number(multiDateColumns.value[2][selectedIndex[2]]),
|
})
|
}
|
|
function openMultiDatePicker(field: 'start' | 'end') {
|
activeMultiDateField.value = field
|
const currentValue = field === 'start' ? formData.visitTimeMultiStart : formData.visitTimeMultiEnd
|
const [datePart] = currentValue.split(' ')
|
const [year, month, day] = datePart ? datePart.split('-').map(Number) : []
|
|
updateMultiDateColumns({
|
year,
|
month,
|
day,
|
})
|
|
showMultiDatePicker.value = true
|
}
|
|
function applyPlateValueToVehicle(vehicle: AppointmentVehicle, plateValue: string) {
|
const normalized = normalizePlateSegmentValue(plateValue).slice(0, 8)
|
const [plateRegion = '', ...rest] = normalized.split('')
|
const chars = rest.slice(0, 7)
|
while (chars.length < 7) {
|
chars.push('')
|
}
|
vehicle.plateRegion = plateRegion
|
vehicle.plateCharacters = chars
|
const nextIndex = Math.min(normalized.length, 7)
|
focusedCharIndex.value = nextIndex
|
}
|
|
function handleUnifiedPlateInput(value: string) {
|
const vehicle = formData.vehicles[plateKeyboardVehicleIndex.value]
|
if (!vehicle) {
|
return
|
}
|
plateKeyboardValue.value = value || ''
|
applyPlateValueToVehicle(vehicle, plateKeyboardValue.value)
|
}
|
|
function handlePlateKeyboardClose() {
|
showPlateKeyboard.value = false
|
}
|
|
watch(plateKeyboardValue, (value) => {
|
if (!showPlateKeyboard.value) {
|
return
|
}
|
handleUnifiedPlateInput(value || '')
|
const nextValue = value || ''
|
if (nextValue.length <= 0) {
|
plateKeyboardLang.value = 'zh'
|
} else if (nextValue.length >= 1) {
|
plateKeyboardLang.value = 'en'
|
}
|
})
|
|
function handleDateTypeChange(type: 'single' | 'multi') {
|
if (type === 'multi' && !allowMulti.value) {
|
uni.showToast({
|
title: '当前仅支持单日预约',
|
icon: 'none',
|
})
|
return
|
}
|
formData.dateType = type
|
}
|
|
function formatMultiDateStartDisplay(value: string) {
|
return value ? `${value} 00:00` : ''
|
}
|
|
function formatMultiDateEndDisplay(value: string) {
|
return value ? `${value} 23:59` : ''
|
}
|
|
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}-${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
|
}
|
|
showMultiDatePicker.value = false
|
}
|
|
function handleTimeConfirm() {
|
const selectedYear = Number(timeColumns.value[0][timePickerIndex.value[0]])
|
const selectedMonth = Number(timeColumns.value[1][timePickerIndex.value[1]])
|
const selectedDay = Number(timeColumns.value[2][timePickerIndex.value[2]])
|
const selectedPeriod = timeColumns.value[3][timePickerIndex.value[3]]
|
const availablePeriods = getAvailableTimeSlots(selectedYear, selectedMonth, selectedDay)
|
|
if (!availablePeriods.includes(selectedPeriod)) {
|
updateTimeColumns({
|
year: selectedYear,
|
month: selectedMonth,
|
day: selectedDay,
|
})
|
return uni.showToast({ title: '不能选择过去的时间', icon: 'none' })
|
}
|
|
const year = timeColumns.value[0][timePickerIndex.value[0]]
|
const month = timeColumns.value[1][timePickerIndex.value[1]]
|
const day = timeColumns.value[2][timePickerIndex.value[2]]
|
const time = timeColumns.value[3][timePickerIndex.value[3]]
|
|
const formattedTime = `${year}-${formatDatePart(month)}-${formatDatePart(day)} ${time}`
|
if (activeTimeField.value === 'single') {
|
formData.visitTimeSingle = formattedTime
|
} else if (activeTimeField.value === 'start') {
|
formData.visitTimeMultiStart = formattedTime
|
} else {
|
formData.visitTimeMultiEnd = formattedTime
|
}
|
showTimePicker.value = false
|
}
|
|
const showPromisePopup = ref(false)
|
const promiseHtml = ref('')
|
const countdown = ref(5)
|
let timer: ReturnType<typeof setInterval> | null = null
|
let visitedSearchTimer: ReturnType<typeof setTimeout> | null = null
|
|
function startCountdown() {
|
if (timer) clearInterval(timer)
|
timer = setInterval(() => {
|
if (countdown.value > 0) {
|
countdown.value--
|
} else {
|
if (timer) clearInterval(timer)
|
}
|
}, 1000)
|
}
|
|
function handlePromiseClose() {
|
showPromisePopup.value = false
|
pendingSubmitPayload.value = null
|
if (timer) {
|
clearInterval(timer)
|
timer = null
|
}
|
}
|
|
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
|
void requestApprovalResultSubscribeAuthorization()
|
countdown.value = 5
|
startCountdown()
|
}
|
catch (error) {
|
uni.showToast({
|
title: error instanceof Error ? error.message : '加载入园承诺失败',
|
icon: 'none',
|
})
|
}
|
}
|
|
function handleVehicleEntryChange(enabled: boolean) {
|
const driverVisitorIds = formData.vehicles
|
.map(vehicle => vehicle.driverVisitorId)
|
.filter((id): id is number => Boolean(id))
|
formData.vehicleEntry = enabled
|
if (enabled && formData.vehicles.length === 0) {
|
formData.vehicles = [createEmptyVehicle()]
|
}
|
if (!enabled) {
|
formData.vehicles = []
|
if (driverVisitorIds.length > 0) {
|
const driverIdSet = new Set(driverVisitorIds)
|
formData.visitors = formData.visitors.filter(visitor => !driverIdSet.has(visitor.id))
|
}
|
}
|
}
|
|
function createEmptyVehicle(): AppointmentVehicle {
|
return {
|
plateRegion: '',
|
plateCharacters: ['', '', '', '', '', '', ''],
|
plateTag: defaultPlateTag,
|
driver: '',
|
driverVisitorId: null,
|
}
|
}
|
|
function handleAddVehicle() {
|
formData.vehicles.push(createEmptyVehicle())
|
}
|
|
function handleDeleteVehicle(index: number) {
|
const targetVehicle = formData.vehicles[index]
|
if (!targetVehicle) {
|
return
|
}
|
const removedDriverId = targetVehicle.driverVisitorId
|
formData.vehicles.splice(index, 1)
|
if (!removedDriverId) {
|
return
|
}
|
const stillUsed = formData.vehicles.some(vehicle => vehicle.driverVisitorId === removedDriverId)
|
if (!stillUsed) {
|
formData.visitors = formData.visitors.filter(visitor => visitor.id !== removedDriverId)
|
}
|
}
|
|
// 记录当前正在选择驾驶员的车辆索引,-1表示选择的是普通来访人员
|
const selectingDriverForVehicleIndex = ref(-1)
|
|
function handleSelectDriver(index: number) {
|
selectingDriverForVehicleIndex.value = index
|
const currentDriverId = formData.vehicles[index]?.driverVisitorId
|
const occupiedDriverIds = formData.vehicles
|
.map(vehicle => vehicle.driverVisitorId)
|
.filter((driverVisitorId): driverVisitorId is number =>
|
Boolean(driverVisitorId) && driverVisitorId !== currentDriverId,
|
)
|
uni.navigateTo({
|
url: `/pages/choosePerson/index?mode=driver&vehicleIndex=${index}${currentDriverId ? `&selectedId=${currentDriverId}` : ''}${occupiedDriverIds.length ? `&occupiedDriverIds=${occupiedDriverIds.join(',')}` : ''}`,
|
})
|
}
|
|
function handleSelectVisitor() {
|
selectingDriverForVehicleIndex.value = -1
|
const selectedIds = formData.visitors.map(item => item.id).join(',')
|
uni.navigateTo({
|
url: `/pages/choosePerson/index?mode=pick${selectedIds ? `&excludedIds=${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)
|
}
|
|
function getVisitorGenderIcon(gender: AppointmentVisitor['gender']) {
|
return gender === 'female' ? appointmentIcons.female : appointmentIcons.male
|
}
|
|
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 isDriverUsedByOtherVehicle(driverVisitorId: number, currentVehicleIndex: number) {
|
return formData.vehicles.some((vehicle, index) =>
|
index !== currentVehicleIndex && vehicle.driverVisitorId === driverVisitorId,
|
)
|
}
|
|
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)
|
if (isDriverUsedByOtherVehicle(nextDriver.id, vehicleIndex)) {
|
uni.showToast({
|
title: '该驾驶员已被其他车辆使用',
|
icon: 'none',
|
})
|
return
|
}
|
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)),
|
)
|
nextVisitors.forEach((visitor) => {
|
upsertVisitor(visitor, 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',
|
})
|
}
|
}
|
|
function sortVisitReasons(reasons: MiniVisitReasonItem[]) {
|
return [...reasons].sort((left, right) => {
|
const leftIsOther = left.reasonCode === 'OTHER' || left.reasonName === '其他'
|
const rightIsOther = right.reasonCode === 'OTHER' || right.reasonName === '其他'
|
if (leftIsOther !== rightIsOther) {
|
return leftIsOther ? 1 : -1
|
}
|
|
const leftOrder = left.sortOrder ?? Number.MAX_SAFE_INTEGER
|
const rightOrder = right.sortOrder ?? Number.MAX_SAFE_INTEGER
|
if (leftOrder !== rightOrder) {
|
return leftOrder - rightOrder
|
}
|
return left.id - right.id
|
})
|
}
|
|
async function loadVisitReasons() {
|
try {
|
visitReasons.value = sortVisitReasons(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 loadFieldConfigs() {
|
try {
|
fieldConfigs.value = await fetchMiniFieldConfigs()
|
if (!showVehicleEntryOption.value) {
|
handleVehicleEntryChange(false)
|
}
|
}
|
catch (error) {
|
fieldConfigs.value = []
|
console.warn('load field configs failed', error)
|
}
|
}
|
|
async function resolveVisitedUser() {
|
const keyword = formData.interviewee.trim()
|
if (!keyword) {
|
throw new Error('请输入被访人')
|
}
|
if (selectedVisitedUser.value?.id && 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 || ''
|
}
|
|
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 prefillDefaultVisitorForCreate() {
|
if (rebookOrderId.value || rebookOrderUuid.value || formData.visitors.length > 0) {
|
return
|
}
|
try {
|
const visitors = await fetchMyVisitors()
|
const defaultVisitor = visitors.find(item => item.isDefault)
|
if (!defaultVisitor) {
|
return
|
}
|
if (defaultVisitor.missingFieldCodes?.length) {
|
uni.showToast({
|
title: '默认来访人信息不完善,请先完善后再预约',
|
icon: 'none',
|
duration: 3000,
|
})
|
return
|
}
|
formData.visitors = [mapVisitorFromCurrentList(defaultVisitor)]
|
}
|
catch (error) {
|
console.warn('prefill default visitor failed', error)
|
}
|
}
|
|
async function applyRebookPrefill(detailData: MiniMyAppointmentDetailItem, currentVisitors: MiniVisitorListItem[]) {
|
const warnings: string[] = []
|
resetForm()
|
|
formData.interviewee = detailData.visitedUserName || ''
|
selectedVisitedUser.value = {
|
id: detailData.visitedUserId,
|
realName: detailData.visitedUserName || '',
|
deptName: detailData.visitedDeptName || '',
|
}
|
visitedOptions.value = []
|
|
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.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()
|
let visitReasonCode = selectedVisitReason.value?.reasonCode || ''
|
if (!visitReasonCode && !isVisitReasonRequired.value) {
|
visitReasonCode = visitReasons.value[0]?.reasonCode || ''
|
if (visitReasonCode) {
|
const fallbackReason = visitReasons.value.find(item => item.reasonCode === visitReasonCode) || null
|
selectedVisitReason.value = fallbackReason
|
formData.reason = fallbackReason?.reasonName || ''
|
}
|
}
|
if (!visitReasonCode) {
|
throw new Error('请选择拜访事由')
|
}
|
const visitReasonOther = customVisitReason.value.trim()
|
if (!visitReasonOther) {
|
throw new Error('请输入事由说明')
|
}
|
if (visitReasonOther.length > 15) {
|
throw new Error('事由说明不能超过15个字')
|
}
|
if (!formData.visitors.length) {
|
throw new Error('请选择来访人员')
|
}
|
|
const visitTimeMode = resolveVisitTimeMode()
|
const payload: MiniAppointmentCreateRequest = {
|
visitedUserId: visitedUser.id,
|
visitReasonCode,
|
visitReasonOther,
|
visitTimeMode,
|
personEntry: true,
|
visitorIds: formData.visitors.map(item => item.id),
|
vehicleEntry: formData.vehicleEntry,
|
}
|
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('请添加进场车辆')
|
}
|
const usedDriverIds = new Set<number>()
|
payload.vehicles = formData.vehicles.map((item) => {
|
const plateNo = buildVehiclePlateNo(item)
|
if (!plateNo || !item.driverVisitorId) {
|
throw new Error('请完善车辆信息')
|
}
|
if (usedDriverIds.has(item.driverVisitorId)) {
|
throw new Error('不同车辆的驾驶员不能相同')
|
}
|
usedDriverIds.add(item.driverVisitorId)
|
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 {
|
const order = await createAppointment(pendingSubmitPayload.value)
|
pendingSubmitPayload.value = null
|
uni.redirectTo({ url: `/pages/success/index?orderId=${order.id}&orderUuid=${encodeURIComponent(order.uuid)}` })
|
}
|
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 = ''
|
}
|
},
|
)
|
|
watch(
|
showVehicleEntryOption,
|
(visible) => {
|
if (!visible) {
|
handleVehicleEntryChange(false)
|
}
|
},
|
)
|
|
onLoad((options) => {
|
uni.$on('selectVisitors', handleVisitorSelection)
|
rebookOrderId.value = options?.rebookOrderId ? Number(options.rebookOrderId) : null
|
rebookOrderUuid.value = options?.rebookOrderUuid ? decodeURIComponent(options.rebookOrderUuid) : ''
|
void Promise.all([
|
loadFieldConfigs(),
|
loadBookingSettings(),
|
loadVisitReasons(),
|
]).then(async () => {
|
if (rebookOrderId.value && rebookOrderUuid.value) {
|
await prefillFromRebookDetail()
|
return
|
}
|
await prefillDefaultVisitorForCreate()
|
})
|
})
|
|
onUnload(() => {
|
uni.$off('selectVisitors', handleVisitorSelection)
|
if (visitedSearchTimer) {
|
clearTimeout(visitedSearchTimer)
|
visitedSearchTimer = null
|
}
|
if (timer) {
|
clearInterval(timer)
|
timer = null
|
}
|
})
|
</script>
|
|
<template>
|
<view class="min-h-screen bg-[#F6FBFF] pb-[240rpx] relative box-border">
|
<!-- 顶部渐变背景 -->
|
<view class="absolute left-0 top-0 z-0 h-[500rpx] w-full"
|
style="background: linear-gradient(180deg, #5398FF 0%, #A9D0FF 50%, #F6FBFF 100%);" />
|
|
<!-- 自定义导航栏 -->
|
<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 h-full w-[88rpx] items-center justify-center relative z-10" @click="router.back()">
|
<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>
|
|
<view class="relative z-10 px-[32rpx] pt-[24rpx]">
|
<!-- 拜访信息模块 -->
|
<view class="mb-[24rpx]">
|
<text class="text-[32rpx] text-[#1A1A1A] font-bold">
|
拜访信息
|
</text>
|
</view>
|
<view
|
class="mb-[48rpx] rounded-[16rpx] bg-white pl-[32rpx] pr-[12rpx] py-[8rpx] shadow-[0px_4px_8px_0px_rgba(6,47,108,0.1608)]">
|
<wd-form :model="formData">
|
<!-- 被访人 -->
|
<view class="custom-cell flex items-center justify-between">
|
<view class="inline-flex items-start shrink-0">
|
<text class="text-[28rpx] text-[#333333]">被访人</text>
|
<text v-if="isRespondentRequired" class="text-[24rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text>
|
</view>
|
<input
|
:value="formData.interviewee"
|
type="text"
|
placeholder="请输入要拜访谁"
|
placeholder-class="text-[#B3B3B3]"
|
class="ml-[32rpx] h-[64rpx] flex-1 text-right text-[28rpx] text-[#333333]"
|
confirm-type="search"
|
:adjust-position="true"
|
:cursor-spacing="120"
|
@input="handleIntervieweeInput"
|
@confirm="handleSearchVisitedUser"
|
/>
|
</view>
|
<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="openReasonPicker">
|
<view class="inline-flex items-start">
|
<text class="text-[28rpx] text-[#333333]">拜访事由</text>
|
<text v-if="isVisitReasonRequired" class="text-[24rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text>
|
</view>
|
<view class="flex items-center justify-end flex-1">
|
<text class="text-[28rpx]" :class="formData.reason ? 'text-[#333333]' : ''">
|
{{ formData.reason || '请选择拜访事由' }}
|
</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="15"
|
placeholder="请输入事由说明"
|
placeholder-class="text-[#B3B3B3]"
|
/>
|
</view>
|
|
<!-- 拜访时间类型切换 -->
|
<view class="custom-cell flex items-center justify-between">
|
<view class="inline-flex items-start">
|
<text class="text-[26rpx] text-[#333333]">拜访时间</text>
|
<text v-if="isVisitTimeRequired" class="text-[28rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text>
|
</view>
|
<view class="flex justify-end">
|
<view
|
class="h-[48rpx] w-[240rpx] flex rounded-full border border-[#0068FF] bg-white box-border overflow-hidden">
|
<view
|
class="flex w-[120rpx] items-center justify-center text-[24rpx] transition-all"
|
:class="[
|
'border-r border-[#0068FF]',
|
formData.dateType === 'single' ? 'bg-[#0068FF] text-white' : 'bg-white text-[#0068FF]',
|
]"
|
@tap.stop="handleDateTypeChange('single')"
|
@click.stop="handleDateTypeChange('single')"
|
>
|
单日
|
</view>
|
<view
|
class="flex w-[120rpx] items-center justify-center text-[24rpx] transition-all"
|
:class="[
|
formData.dateType === 'multi' ? 'bg-[#0068FF] text-white' : 'bg-white text-[#0068FF]',
|
!allowMulti ? 'opacity-40' : '',
|
]"
|
@tap.stop="handleDateTypeChange('multi')"
|
@click.stop="handleDateTypeChange('multi')"
|
>
|
多日
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 单日时间选择 -->
|
<view v-if="formData.dateType === 'single'" class="custom-cell flex items-center justify-between" @click="openTimePicker('single')">
|
<view class="flex items-center justify-end flex-1">
|
<text class="text-[28rpx]" :class="formData.visitTimeSingle ? 'text-[#333333]' : 'text-[#999999]'">
|
{{ formData.visitTimeSingle || '请选择拜访时间' }}
|
</text>
|
<wd-icon name="arrow-right" size="32rpx" color="#999999" custom-class="ml-[8rpx]" />
|
</view>
|
</view>
|
|
<!-- 多日时间选择 -->
|
<template v-if="formData.dateType === 'multi'">
|
<view class="custom-cell flex items-center justify-between" @click="openMultiDatePicker('start')">
|
<text class="text-[28rpx] text-[#333333]">开始时间</text>
|
<view class="flex items-center justify-end flex-1">
|
<text class="text-[28rpx]" :class="formData.visitTimeMultiStart ? 'text-[#333333]' : 'text-[#999999]'">
|
{{ formatMultiDateStartDisplay(formData.visitTimeMultiStart) || '请选择开始时间' }}
|
</text>
|
<wd-icon name="arrow-right" size="32rpx" color="#999999" custom-class="ml-[8rpx]" />
|
</view>
|
</view>
|
<view class="custom-cell flex items-center justify-between" @click="openMultiDatePicker('end')">
|
<text class="text-[28rpx] text-[#333333]">结束时间</text>
|
<view class="flex items-center justify-end flex-1">
|
<text class="text-[28rpx]" :class="formData.visitTimeMultiEnd ? 'text-[#333333]' : 'text-[#999999]'">
|
{{ formatMultiDateEndDisplay(formData.visitTimeMultiEnd) || '请选择结束时间' }}
|
</text>
|
<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="mb-[24rpx] flex items-end">
|
<text class="text-[32rpx] text-[#1A1A1A] font-bold">
|
来访人信息
|
</text>
|
<text class="ml-[16rpx] text-[24rpx] text-[#999999] mb-[4rpx]">
|
选择您及同行人员
|
</text>
|
</view>
|
|
<!-- 进场类型 -->
|
<view class="mb-[24rpx] rounded-[16rpx] bg-white shadow-[0px_4px_8px_0px_rgba(6,47,108,0.1608)]">
|
<view class="px-[32rpx] py-[32rpx] flex items-center justify-between">
|
<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">
|
<view
|
class="flex items-center justify-center w-[28rpx] h-[28rpx] rounded-full border-[2rpx] border-solid transition-colors duration-200"
|
:class="'border-[#CFCFCF]'">
|
<view class="w-[16rpx] h-[16rpx] rounded-full bg-[#CFCFCF]">
|
</view>
|
</view>
|
<text class="ml-[12rpx] text-[28rpx] text-[#999999]">人员进场</text>
|
</view>
|
<view v-if="showVehicleEntryOption" 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.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.vehicleEntry ? 'text-[#333333]' : 'text-[#999999]'">车辆进场</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<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 class="px-[24rpx] py-[16rpx]">
|
<view
|
v-for="(vehicle, index) in formData.vehicles"
|
:key="`${vehicle.plateRegion}-${index}`"
|
class="vehicle-card mb-[16rpx] rounded-[12rpx] bg-[#FAFAFA] px-[24rpx] py-[24rpx]"
|
>
|
<view class="flex items-center justify-between">
|
<text class="text-[26rpx] text-[#333333] font-medium">车牌号</text>
|
<view class="flex items-center">
|
<view class="flex items-center space-x-[8rpx]">
|
<!-- 第一格:省份 -->
|
<view
|
class="plate-box"
|
:class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === 0 }"
|
@click="handlePlateCharClick(index, 0)"
|
>
|
<text class="plate-input-text">{{ vehicle.plateRegion }}</text>
|
</view>
|
<!-- 第二格:字母 -->
|
<view
|
class="plate-box"
|
:class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === 1 }"
|
@click="handlePlateCharClick(index, 1)"
|
>
|
<text class="plate-input-text">{{ vehicle.plateCharacters[0] }}</text>
|
</view>
|
<!-- 圆点 -->
|
<text class="text-[28rpx] text-[#333333] font-bold mx-[4rpx]">·</text>
|
<!-- 后续 5-6 格 -->
|
<view
|
v-for="i in 5"
|
:key="i"
|
class="plate-box"
|
:class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === i + 1 }"
|
@click="handlePlateCharClick(index, i + 1)"
|
>
|
<text class="plate-input-text">{{ vehicle.plateCharacters[i] }}</text>
|
</view>
|
<!-- 新能源位 -->
|
<view
|
class="plate-box plate-box-energy relative"
|
:class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === 7 }"
|
@click="handlePlateCharClick(index, 7)"
|
>
|
<text class="plate-input-text">{{ vehicle.plateCharacters[6] }}</text>
|
<text v-if="!vehicle.plateCharacters[6]" class="energy-placeholder">新能源</text>
|
</view>
|
</view>
|
<image
|
v-if="index > 0"
|
:src="appointmentIcons.delete"
|
class="ml-[20rpx] h-[32rpx] w-[32rpx]"
|
@click="handleDeleteVehicle(index)"
|
/>
|
</view>
|
</view>
|
<view class="mt-[24rpx] flex items-center justify-between" @click="handleSelectDriver(index)">
|
<text class="text-[26rpx] text-[#333333] font-medium">驾驶员</text>
|
<view class="flex items-center">
|
<text class="text-[24rpx]" :class="vehicle.driver ? 'text-[#666666]' : 'text-[#999999]'">
|
{{ vehicle.driver || '请选择驾驶员' }}
|
</text>
|
<wd-icon name="arrow-right" size="32rpx" color="#999999" custom-class="ml-[8rpx]" />
|
</view>
|
</view>
|
</view>
|
<view class="flex justify-center pt-[8rpx]">
|
<view
|
class="h-[64rpx] min-w-[220rpx] flex items-center justify-center rounded-full border border-[#1677FF] border-solid px-[28rpx]"
|
@click="handleAddVehicle"
|
>
|
<view class="flex items-center justify-center w-[28rpx] h-[28rpx] rounded-full border border-[#1677FF] border-solid">
|
<text class="text-[20rpx] text-[#1677FF] leading-none">+</text>
|
</view>
|
<text class="ml-[12rpx] text-[24rpx] text-[#1677FF] font-medium">添加车辆</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="mb-[48rpx] rounded-[12rpx] bg-white shadow-[0px_4px_8px_0px_rgba(6,47,108,0.1608)]">
|
<view class="px-[32rpx] py-[28rpx] flex items-center border-b1">
|
<text class="text-[28rpx] text-[#333333] font-medium">
|
来访人员
|
</text>
|
<text class="ml-[12rpx] text-[24rpx] text-[#FF4D4F]">
|
(必填)
|
</text>
|
</view>
|
|
<view class="px-[24rpx] py-[16rpx]">
|
<view v-if="formData.visitors.length > 0" class="mb-[16rpx]">
|
<view
|
v-for="(visitor, index) in formData.visitors"
|
: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">
|
<text class="w-[96rpx] shrink-0 truncate text-[28rpx] text-[#333333]">
|
{{ visitor.name }}
|
</text>
|
<view class="ml-[12rpx] 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.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 || '-' }}
|
</text>
|
</view>
|
</view>
|
</view>
|
<view class="ml-[8rpx] flex items-center shrink-0">
|
<view
|
v-if="visitor.badge"
|
class="rounded-full border border-[#1677FF] border-solid px-[16rpx] py-[8rpx] text-[24rpx] text-[#1677FF]"
|
>
|
{{ visitor.badge }}
|
</view>
|
<image
|
v-else
|
:src="appointmentIcons.delete"
|
class="h-[32rpx] w-[32rpx]"
|
@click="handleDeleteVisitor(index)"
|
/>
|
</view>
|
</view>
|
</view>
|
|
<view class="flex justify-center pt-[8rpx] pb-[8rpx]">
|
<view
|
class="h-[64rpx] min-w-[240rpx] flex items-center justify-center rounded-full border border-[#1677FF] border-solid px-[28rpx]"
|
@click="handleSelectVisitor"
|
>
|
<view class="flex items-center justify-center w-[28rpx] h-[28rpx] rounded-full border border-[#1677FF] border-solid">
|
<text class="text-[20rpx] text-[#1677FF] leading-none">+</text>
|
</view>
|
<text class="ml-[12rpx] text-[24rpx] text-[#1677FF] font-medium">
|
选择来访人员
|
</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 提交按钮 -->
|
<view class="fixed bottom-[94rpx] left-0 w-full px-[32rpx] box-border z-[10]">
|
<wd-button type="primary" size="large" block custom-class="submit-btn" :loading="isSubmitting"
|
@click="handleSubmit">
|
提交
|
</wd-button>
|
</view>
|
|
<!-- 自定义拜访事由弹窗 -->
|
<wd-popup v-model="showReasonPicker" position="bottom" custom-class="reason-popup" z-index="100">
|
<view class="flex flex-col bg-white rounded-t-[32rpx] overflow-hidden h-[636rpx]">
|
<!-- 弹窗头部 -->
|
<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="showReasonPicker = 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">
|
<wd-picker-view
|
v-model="reasonPickerValue"
|
:columns="reasonColumns"
|
:item-height="48"
|
:columns-height="144"
|
:immediate-change="true"
|
custom-class="custom-picker-view"
|
/>
|
</view>
|
|
<!-- 弹窗底部按钮 -->
|
<view class="px-[32rpx] pb-[48rpx] shrink-0">
|
<wd-button type="primary" size="large" block custom-class="popup-submit-btn"
|
@click="handleReasonConfirm">
|
提交
|
</wd-button>
|
</view>
|
</view>
|
</wd-popup>
|
<!-- 自定义拜访时间弹窗 (单日) -->
|
<wd-popup v-model="showTimePicker" position="bottom" custom-class="time-popup" z-index="100">
|
<view class="flex flex-col bg-white rounded-t-[32rpx] overflow-hidden h-[608rpx]">
|
<!-- 弹窗头部 -->
|
<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="showTimePicker = 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] pt-[20rpx] flex-1 flex flex-col justify-start">
|
<!-- 头部列名 -->
|
<view class="mb-[12rpx] flex items-center 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 v-if="timeColumns[3]?.length > 0" class="w-[204rpx] flex justify-center shrink-0"><text class="bg-[#F5F5F5] rounded-[8rpx] px-[48rpx] py-[8rpx]">时段</text></view>
|
</view>
|
<picker-view
|
class="custom-picker-view time-picker-view"
|
:value="timePickerIndex"
|
:immediate-change="true"
|
indicator-style="height: 48px;"
|
style="width: 100%; height: 144px;"
|
@change="handleTimeChange"
|
>
|
<picker-view-column v-for="(col, index) in timeColumns" :key="index" class="wd-picker-view__column">
|
<view
|
v-for="(item, itemIndex) in col"
|
:key="itemIndex"
|
class="wd-picker-view-column__item flex items-center justify-center h-[48px]"
|
:class="{'wd-picker-view-column__item--active': timePickerIndex[index] === itemIndex}"
|
>
|
{{ item }}
|
</view>
|
</picker-view-column>
|
</picker-view>
|
</view>
|
|
<!-- 弹窗底部按钮 -->
|
<view class="px-[32rpx] pb-[8rpx] shrink-0">
|
<wd-button type="primary" size="large" block custom-class="popup-submit-btn"
|
@click="handleTimeConfirm">
|
提交
|
</wd-button>
|
</view>
|
</view>
|
</wd-popup>
|
|
<wd-popup v-model="showMultiDatePicker" position="bottom" custom-class="multi-date-popup" z-index="100">
|
<view class="flex flex-col bg-white rounded-t-[32rpx] overflow-hidden h-[608rpx]">
|
<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="showMultiDatePicker = 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] pt-[20rpx] flex-1 flex flex-col justify-start">
|
<view class="mb-[12rpx] flex items-center 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 multi-date-picker-view"
|
:value="multiDatePickerIndex"
|
:immediate-change="true"
|
indicator-style="height: 48px;"
|
style="width: 100%; height: 144px;"
|
@change="handleMultiDateChange"
|
>
|
<picker-view-column
|
v-for="(col, index) in multiDateColumns"
|
:key="index"
|
class="wd-picker-view__column"
|
>
|
<view
|
v-for="(item, itemIndex) in col"
|
:key="itemIndex"
|
class="wd-picker-view-column__item flex items-center justify-center h-[48px]"
|
:class="{ 'wd-picker-view-column__item--active': multiDatePickerIndex[index] === itemIndex }"
|
>
|
{{ item }}
|
</view>
|
</picker-view-column>
|
</picker-view>
|
</view>
|
|
<view class="px-[32rpx] pb-[8rpx] shrink-0">
|
<wd-button type="primary" size="large" block custom-class="popup-submit-btn"
|
@click="handleMultiDateConfirm">
|
提交
|
</wd-button>
|
</view>
|
</view>
|
</wd-popup>
|
</view>
|
|
<!-- 入园承诺弹窗 -->
|
<wd-popup v-model="showPromisePopup" position="center" :close-on-click-modal="false" @close="handlePromiseClose" custom-style="background-color: transparent;">
|
<view class="w-[600rpx] h-[868rpx] bg-white rounded-[32rpx] overflow-hidden relative flex flex-col">
|
<!-- 顶部蓝底渐变 -->
|
<view class="absolute top-0 left-0 w-full h-[180rpx] bg-gradient-to-b from-[#D4E8FF] to-transparent z-0"></view>
|
|
<view class="relative z-10 pt-[48rpx] pb-[32rpx] shrink-0 text-center">
|
<text class="text-[36rpx] text-[#333333] font-medium">入园承诺</text>
|
</view>
|
|
<!-- 承诺内容滚动区 -->
|
<scroll-view scroll-y class="relative z-10 px-[40rpx] flex-1 min-h-0 box-border">
|
<rich-text
|
class="promise-html text-[26rpx] text-[#666666] leading-[44rpx] block"
|
:nodes="promiseHtml"
|
/>
|
</scroll-view>
|
|
<!-- 底部按钮区 -->
|
<view class="relative z-10 px-[40rpx] pt-[32rpx] pb-[40rpx] shrink-0">
|
<view
|
class="h-[88rpx] rounded-full flex items-center justify-center transition-colors duration-300"
|
:class="countdown > 0 ? 'bg-[#BDD7FF]' : 'bg-[#0068FF] shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)]'"
|
@click="handlePromiseConfirm"
|
>
|
<text class="text-[32rpx] text-white font-medium">
|
我已知晓 {{ countdown > 0 ? `(${countdown})` : '' }}
|
</text>
|
</view>
|
</view>
|
</view>
|
</wd-popup>
|
|
<wd-keyboard
|
v-model:visible="showPlateKeyboard"
|
v-model:model-value="plateKeyboardValue"
|
mode="car"
|
:maxlength="8"
|
:car-lang="plateKeyboardLang"
|
:auto-switch-lang="true"
|
close-text="完成"
|
delete-text="删除"
|
:hide-on-click-outside="true"
|
@update:car-lang="plateKeyboardLang = $event"
|
@close="handlePlateKeyboardClose"
|
/>
|
</view>
|
</template>
|
|
<style lang="scss" scoped>
|
:deep(.cell-with-switch .wd-cell__value) {
|
flex: 1;
|
}
|
|
.border {
|
border: 2rpx solid #0068FF;
|
box-sizing: border-box;
|
}
|
|
// :deep(.rounded-full) {
|
// width: 28rpx !important;
|
// height: 28rpx !important;
|
// border-radius: 50% !important;
|
// border: 2px solid #0068FF !important;
|
// }
|
|
.border-b1 {
|
border-bottom: 2rpx solid #F5F5F5 !important;
|
}
|
|
.vehicle-card:last-child {
|
margin-bottom: 0;
|
}
|
|
/* 车牌号输入样式 */
|
.plate-box {
|
width: 44rpx;
|
height: 56rpx;
|
background: #F5F5F5;
|
border-radius: 4rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
position: relative;
|
transition: all 0.2s;
|
}
|
|
.plate-box-active::after {
|
content: '';
|
position: absolute;
|
bottom: 0;
|
left: 20%;
|
right: 20%;
|
height: 4rpx;
|
background: #1677FF;
|
border-radius: 2rpx;
|
}
|
|
.plate-input {
|
width: 100%;
|
height: 100%;
|
text-align: center;
|
font-size: 28rpx;
|
color: #333333;
|
font-weight: 600;
|
background: transparent;
|
}
|
|
.plate-input-text {
|
width: 100%;
|
text-align: center;
|
font-size: 28rpx;
|
color: #333333;
|
font-weight: 600;
|
line-height: 56rpx;
|
}
|
|
.plate-box-energy {
|
border: 2rpx solid #B7E5C1;
|
background: #F2FAF3;
|
box-sizing: border-box;
|
}
|
|
.energy-placeholder {
|
position: absolute;
|
bottom: 6rpx;
|
left: 0;
|
right: 0;
|
text-align: center;
|
font-size: 12rpx;
|
color: #6AB77A;
|
transform: scale(0.8);
|
line-height: 1;
|
}
|
|
.plate-box-energy.plate-box-active {
|
border-style: solid;
|
border-color: #6AB77A;
|
}
|
|
.plate-box-energy.plate-box-active::after {
|
background: #6AB77A;
|
}
|
|
.vehicle-card {
|
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;
|
}
|
|
:deep(.wd-cell) {
|
padding-left: unset !important;
|
}
|
|
:deep(.wd-cell__wrapper) {
|
padding-right: unset !important;
|
}
|
|
:deep(.wd-input__inner) {
|
text-align: right !important;
|
}
|
|
:deep(.wd-cell__value) {
|
text-align: right !important;
|
color: #000 !important;
|
}
|
|
:deep(.wd-datetime-picker__placeholder) {
|
color: #000 !important;
|
}
|
|
|
|
|
/* 自定义单元格样式,去掉默认边框,使用自身内边距 */
|
:deep(.custom-cell),
|
.custom-cell {
|
padding: 24rpx 20rpx 24rpx 0 !important;
|
background-color: transparent !important;
|
position: relative;
|
}
|
|
:deep(.custom-cell::after),
|
.custom-cell::after {
|
content: '';
|
position: absolute;
|
bottom: 0;
|
border-bottom: 1px solid #F0F0F0 !important;
|
left: 0 !important;
|
right: 20rpx !important;
|
}
|
|
/* 重置输入框、选择器背景色 */
|
:deep(.wd-input),
|
:deep(.wd-input__inner),
|
:deep(.wd-cell),
|
:deep(.wd-picker) {
|
background-color: transparent !important;
|
}
|
|
/* 最后一个元素去掉下划线 */
|
:deep(.wd-form > .custom-cell:last-child::after),
|
.wd-form>.custom-cell:last-child::after {
|
display: none !important;
|
}
|
|
:deep(.no-label-cell .wd-cell__title) {
|
display: none !important;
|
}
|
|
:deep(.submit-btn) {
|
height: 96rpx !important;
|
border-radius: 48rpx !important;
|
font-size: 32rpx !important;
|
font-weight: 500 !important;
|
background: #0068FF !important;
|
box-shadow: 0 8rpx 24rpx rgba(0, 104, 255, 0.2) !important;
|
}
|
|
/* 自定义拜访事由与时间弹窗样式 */
|
:deep(.reason-popup),
|
:deep(.time-popup),
|
:deep(.multi-date-popup) {
|
border-top-left-radius: 32rpx;
|
border-top-right-radius: 32rpx;
|
overflow: hidden;
|
background-color: transparent !important;
|
}
|
|
:deep(.wd-picker-view-column__item) {
|
color: #B3B3B3 !important;
|
font-size: 28rpx !important;
|
background-color: transparent !important;
|
transition: color 0.08s linear, background-color 0.08s linear;
|
z-index: 10;
|
position: relative;
|
}
|
|
:deep(picker-view-column) {
|
background-color: transparent !important;
|
}
|
|
:deep(.custom-picker-view .wd-picker-view-column__item) {
|
font-size: 28rpx;
|
color: #999999;
|
transition: color 0.08s linear, background-color 0.08s linear;
|
position: relative;
|
z-index: 10;
|
}
|
|
:deep(.wd-picker-view-column__item--active) {
|
color: #0068FF !important;
|
font-weight: 500;
|
font-size: 28rpx !important;
|
background-color: #F4F8FF !important;
|
z-index: 10;
|
}
|
|
/* 选择器中间的选中边框线 */
|
:deep(.custom-picker-view .wd-picker-view__hairline) {
|
border-top: none !important;
|
border-bottom: none !important;
|
background-color: #F4F8FF;
|
border-radius: 8rpx;
|
z-index: -1;
|
top: 50% !important;
|
transform: translateY(-50%) !important;
|
}
|
|
:deep(.custom-picker-view .wd-picker-view-column__item--active) {
|
color: #0068FF !important;
|
font-weight: 500;
|
font-size: 28rpx !important;
|
background-color: #F4F8FF !important;
|
}
|
|
:deep(.reason-popup .wd-picker-view__hairline::before) {
|
content: '';
|
position: absolute;
|
top: 15%;
|
left: 20%;
|
height: 70%;
|
width: 4rpx;
|
background-color: #0068FF;
|
border-radius: 2rpx;
|
display: block !important;
|
}
|
:deep(.reason-popup .wd-picker-view__hairline::after) {
|
content: '';
|
position: absolute;
|
top: 15%;
|
right: 20%;
|
height: 70%;
|
width: 4rpx;
|
background-color: #0068FF;
|
border-radius: 2rpx;
|
display: block !important;
|
}
|
|
/* 去除小程序自带的 indicator 边框 */
|
:deep(.time-picker-view .wx-picker-view-indicator),
|
:deep(.multi-date-picker-view .wx-picker-view-indicator) {
|
z-index: 1 !important;
|
background-color: #F4F8FF !important;
|
border-radius: 8rpx;
|
}
|
:deep(.time-picker-view .wx-picker-view-indicator::before),
|
:deep(.multi-date-picker-view .wx-picker-view-indicator::before) {
|
display: none !important;
|
}
|
:deep(.time-picker-view .wx-picker-view-indicator::after),
|
:deep(.multi-date-picker-view .wx-picker-view-indicator::after) {
|
display: none !important;
|
}
|
:deep(.time-picker-view picker-view-column:nth-child(1)),
|
:deep(.multi-date-picker-view picker-view-column:nth-child(1)) {
|
flex: 1 !important;
|
width: auto;
|
padding: 0 !important;
|
z-index: 10;
|
}
|
:deep(.time-picker-view picker-view-column:nth-child(2)),
|
:deep(.multi-date-picker-view picker-view-column:nth-child(2)) {
|
flex: 1 !important;
|
width: auto;
|
padding: 0 !important;
|
z-index: 10;
|
}
|
:deep(.time-picker-view picker-view-column:nth-child(3)),
|
:deep(.multi-date-picker-view picker-view-column:nth-child(3)) {
|
flex: 1 !important;
|
width: auto;
|
padding: 0 !important;
|
z-index: 10;
|
}
|
:deep(.time-picker-view picker-view-column:nth-child(4)) {
|
flex: 1.5 !important;
|
padding: 0 !important;
|
z-index: 10;
|
background-color: transparent !important;
|
}
|
|
/* 调整选项的宽度 */
|
:deep(.time-picker-view picker-view-column:nth-child(1) .wd-picker-view-column__item),
|
:deep(.time-picker-view picker-view-column:nth-child(2) .wd-picker-view-column__item),
|
:deep(.time-picker-view picker-view-column:nth-child(3) .wd-picker-view-column__item) {
|
width: 100% !important;
|
text-align: center;
|
padding: 0 !important;
|
}
|
:deep(.time-picker-view picker-view-column:nth-child(4) .wd-picker-view-column__item) {
|
white-space: nowrap;
|
font-size: 26rpx !important;
|
transform-origin: center;
|
padding: 0 !important;
|
width: 100% !important;
|
}
|
:deep(.time-picker-view .wd-picker-view-column__item--active),
|
:deep(.multi-date-picker-view .wd-picker-view-column__item--active) {
|
background-color: #F4F8FF !important;
|
}
|
:deep(.time-picker-view .wd-picker-view__column),
|
:deep(.multi-date-picker-view .wd-picker-view__column) {
|
background-color: transparent !important;
|
}
|
:deep(.popup-submit-btn) {
|
height: 96rpx !important;
|
border-radius: 48rpx !important;
|
font-size: 32rpx !important;
|
font-weight: 500 !important;
|
background: #0068FF !important;
|
}
|
</style>
|