longz
2026-07-13 5882ba8863d77fdcc168b176c3ffb30e8a20556a
yuyueMiniProgram/src/pages/appointment/index.vue
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from 'vue'
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'
@@ -13,7 +14,7 @@
  MiniMyAppointmentDetailItem,
  MiniMyAppointmentDetailRebookVisitorItem,
} from '@/types/appointment'
import type { MiniVisitReasonItem, MiniVisitedUserItem } from '@/types/lookup'
import type { MiniFieldConfigItem, MiniVisitReasonItem, MiniVisitedUserItem } from '@/types/lookup'
import type { MiniVisitorListItem } from '@/types/visitor'
definePage({
@@ -27,15 +28,20 @@
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 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 {
@@ -76,6 +82,7 @@
})
const visitReasons = ref<MiniVisitReasonItem[]>([])
const fieldConfigs = ref<MiniFieldConfigItem[]>([])
const selectedVisitReason = ref<MiniVisitReasonItem | null>(null)
const reasonPickerValue = ref('')
const visitedOptions = ref<MiniVisitedUserItem[]>([])
@@ -85,13 +92,14 @@
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 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`,
@@ -101,6 +109,10 @@
}
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('')
@@ -108,9 +120,37 @@
  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) {
@@ -118,10 +158,8 @@
}
function focusNextPlateChar(vehicleIndex: number, charIndex: number) {
  nextTick(() => {
    focusedVehicleIndex.value = vehicleIndex
    focusedCharIndex.value = charIndex
  })
  focusedVehicleIndex.value = vehicleIndex
  focusedCharIndex.value = charIndex
}
function normalizePlateSegmentValue(rawValue: string) {
@@ -166,6 +204,10 @@
  if (lastFilledIndex < 7) {
    focusNextPlateChar(vehicleIndex, lastFilledIndex + 1)
  }
}
function handleIntervieweeInput(e: any) {
  formData.interviewee = e?.detail?.value || ''
}
function handlePlateDelete(vehicleIndex: number, charIndex: number) {
@@ -403,11 +445,62 @@
  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() {
@@ -542,6 +635,7 @@
    promiseHtml.value = settings.CONTENT_PARK_NOTICE_HTML || '<p>暂无入园承诺</p>'
    pendingSubmitPayload.value = payload
    showPromisePopup.value = true
    void requestApprovalResultSubscribeAuthorization()
    countdown.value = 5
    startCountdown()
  }
@@ -554,12 +648,19 @@
}
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))
    }
  }
}
@@ -578,7 +679,19 @@
}
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表示选择的是普通来访人员
@@ -586,8 +699,14 @@
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}${formData.vehicles[index]?.driverVisitorId ? `&selectedId=${formData.vehicles[index].driverVisitorId}` : ''}`,
    url: `/pages/choosePerson/index?mode=driver&vehicleIndex=${index}${currentDriverId ? `&selectedId=${currentDriverId}` : ''}${occupiedDriverIds.length ? `&occupiedDriverIds=${occupiedDriverIds.join(',')}` : ''}`,
  })
}
@@ -595,7 +714,7 @@
  selectingDriverForVehicleIndex.value = -1
  const selectedIds = formData.visitors.map(item => item.id).join(',')
  uni.navigateTo({
    url: `/pages/choosePerson/index?mode=pick${selectedIds ? `&selectedIds=${selectedIds}` : ''}`,
    url: `/pages/choosePerson/index?mode=pick${selectedIds ? `&excludedIds=${selectedIds}` : ''}`,
  })
}
@@ -710,6 +829,12 @@
  }
}
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
@@ -732,6 +857,13 @@
    }
    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, '驾驶员')
@@ -746,14 +878,8 @@
      .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,
    }
  nextVisitors.forEach((visitor) => {
    upsertVisitor(visitor, currentDriverIds.has(visitor.id) ? '驾驶员' : undefined)
  })
}
@@ -805,9 +931,26 @@
  }
}
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 = await fetchVisitReasons()
    visitReasons.value = sortVisitReasons(await fetchVisitReasons())
    if (selectedVisitReason.value) {
      const nextReason = visitReasons.value.find(item => item.reasonCode === selectedVisitReason.value?.reasonCode) || null
      selectedVisitReason.value = nextReason
@@ -826,12 +969,25 @@
  }
}
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?.realName === keyword) {
  if (selectedVisitedUser.value?.id && selectedVisitedUser.value?.realName === keyword) {
    return selectedVisitedUser.value
  }
  const users = await searchVisitedUsersByExactName(keyword)
@@ -849,9 +1005,6 @@
  const target = visitReasons.value.find(item => item.reasonName === reasonName) || null
  selectedVisitReason.value = target
  formData.reason = target?.reasonName || ''
  if (!showCustomReasonInput.value) {
    customVisitReason.value = ''
  }
}
function resolveVisitTimeMode() {
@@ -900,31 +1053,42 @@
  }
}
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 = 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('原被访人不可用,请重新选择')
    }
  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) {
@@ -939,9 +1103,7 @@
      warnings.push('原拜访事由不可用,请重新选择')
    }
  }
  customVisitReason.value = detailData.visitReasonCode === VISIT_REASON_CODE_OTHER
    ? detailData.visitReasonOther || ''
    : ''
  customVisitReason.value = detailData.visitReasonOther || ''
  const currentVisitorMap = new Map(currentVisitors.map(item => [item.id, item]))
  const restoredVisitors: AppointmentVisitor[] = []
@@ -1000,11 +1162,24 @@
async function buildAppointmentPayload(): Promise<MiniAppointmentCreateRequest> {
  const visitedUser = await resolveVisitedUser()
  if (!selectedVisitReason.value?.reasonCode) {
  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('请选择拜访事由')
  }
  if (showCustomReasonInput.value && !customVisitReason.value.trim()) {
    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('请选择来访人员')
@@ -1013,16 +1188,13 @@
  const visitTimeMode = resolveVisitTimeMode()
  const payload: MiniAppointmentCreateRequest = {
    visitedUserId: visitedUser.id,
    visitReasonCode: selectedVisitReason.value.reasonCode,
    visitReasonCode,
    visitReasonOther,
    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('请选择拜访时间')
@@ -1052,11 +1224,16 @@
    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,
@@ -1088,11 +1265,9 @@
  showPromisePopup.value = false
  isSubmitting.value = true
  try {
    await requestApprovalResultSubscribeAuthorization()
    const orderId = await createAppointment(pendingSubmitPayload.value)
    resetForm()
    const order = await createAppointment(pendingSubmitPayload.value)
    pendingSubmitPayload.value = null
    uni.redirectTo({ url: `/pages/success/index?orderId=${orderId}` })
    uni.redirectTo({ url: `/pages/success/index?orderId=${order.id}&orderUuid=${encodeURIComponent(order.uuid)}` })
  }
  catch (error) {
    uni.showToast({
@@ -1153,14 +1328,30 @@
  },
)
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(() => prefillFromRebookDetail())
  ]).then(async () => {
    if (rebookOrderId.value && rebookOrderUuid.value) {
      await prefillFromRebookDetail()
      return
    }
    await prefillDefaultVisitorForCreate()
  })
})
onUnload(() => {
@@ -1207,15 +1398,24 @@
        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">
          <!-- 被访人 -->
          <wd-input v-model="formData.interviewee" placeholder="请输入要接见谁" align="right" custom-class="custom-cell"
            placeholder-class="custom-placeholder">
            <template #label>
              <view class="inline-flex items-start">
                <text class="text-[28rpx] text-[#333333]">被访人</text>
                <text class="text-[24rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text>
              </view>
            </template>
          </wd-input>
          <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>
@@ -1234,7 +1434,7 @@
          <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 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]' : ''">
@@ -1251,39 +1451,46 @@
            <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="请输入具体来访事由"
              maxlength="15"
              placeholder="请输入事由说明"
              placeholder-class="text-[#B3B3B3]"
            />
          </view>
          <!-- 拜访时间类型切换 -->
          <wd-cell custom-class="custom-cell cell-with-switch">
            <template #title>
              <view class="inline-flex items-start">
                <text class="text-[26rpx] text-[#333333]">拜访时间</text>
                <text class="text-[28rpx] text-[#FF4D4F] ml-[4rpx] mt-[-4rpx]">*</text>
              </view>
            </template>
          <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 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="handleDateTypeChange('single')">
                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"
                <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]',
                    formData.dateType === 'multi' ? 'bg-[#0068FF] text-white' : 'bg-white text-[#0068FF]',
                    !allowMulti ? 'opacity-40' : '',
                  ]"
                  @click="handleDateTypeChange('multi')">
                  @tap.stop="handleDateTypeChange('multi')"
                  @click.stop="handleDateTypeChange('multi')"
                >
                  多日
                </view>
              </view>
            </view>
          </wd-cell>
          </view>
          <!-- 单日时间选择 -->
          <view v-if="formData.dateType === 'single'" class="custom-cell flex items-center justify-between" @click="openTimePicker('single')">
@@ -1301,7 +1508,7 @@
              <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]'">
                  {{ formData.visitTimeMultiStart || '请选择开始时间' }}
                  {{ formatMultiDateStartDisplay(formData.visitTimeMultiStart) || '请选择开始时间' }}
                </text>
                <wd-icon name="arrow-right" size="32rpx" color="#999999" custom-class="ml-[8rpx]" />
              </view>
@@ -1310,7 +1517,7 @@
              <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]'">
                  {{ formData.visitTimeMultiEnd || '请选择结束时间' }}
                  {{ formatMultiDateEndDisplay(formData.visitTimeMultiEnd) || '请选择结束时间' }}
                </text>
                <wd-icon name="arrow-right" size="32rpx" color="#999999" custom-class="ml-[8rpx]" />
              </view>
@@ -1343,20 +1550,20 @@
              <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-[#1677FF]'">
                  <view class="w-[16rpx] h-[16rpx] rounded-full bg-[#1677FF]">
                  :class="'border-[#CFCFCF]'">
                  <view class="w-[16rpx] h-[16rpx] rounded-full bg-[#CFCFCF]">
                  </view>
                </view>
                <text class="ml-[12rpx] text-[28rpx] text-[#333333]">人员进场</text>
                <text class="ml-[12rpx] text-[28rpx] text-[#999999]">人员进场</text>
              </view>
              <view class="flex items-center" @click="handleVehicleEntryChange(!formData.vehicleEntry)">
              <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>
                <text class="ml-[12rpx] text-[28rpx]" :class="formData.vehicleEntry ? 'text-[#333333]' : 'text-[#999999]'">接送货</text>
              </view>
            </view>
          </view>
@@ -1383,15 +1590,7 @@
                    :class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === 0 }"
                    @click="handlePlateCharClick(index, 0)"
                  >
                    <input
                      class="plate-input"
                      :value="vehicle.plateRegion"
                      :focus="isPlateInputFocused(index, 0)"
                      maxlength="1"
                      @input="handlePlateInput($event, index, 0)"
                      @focus="handlePlateCharClick(index, 0)"
                      @keydown.delete="handlePlateDelete(index, 0)"
                    />
                    <text class="plate-input-text">{{ vehicle.plateRegion }}</text>
                  </view>
                  <!-- 第二格:字母 -->
                  <view
@@ -1399,15 +1598,7 @@
                    :class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === 1 }"
                    @click="handlePlateCharClick(index, 1)"
                  >
                    <input
                      class="plate-input"
                      :value="vehicle.plateCharacters[0]"
                      :focus="isPlateInputFocused(index, 1)"
                      maxlength="1"
                      @input="handlePlateInput($event, index, 1)"
                      @focus="handlePlateCharClick(index, 1)"
                      @keydown.delete="handlePlateDelete(index, 1)"
                    />
                    <text class="plate-input-text">{{ vehicle.plateCharacters[0] }}</text>
                  </view>
                  <!-- 圆点 -->
                  <text class="text-[28rpx] text-[#333333] font-bold mx-[4rpx]">·</text>
@@ -1419,15 +1610,7 @@
                    :class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === i + 1 }"
                    @click="handlePlateCharClick(index, i + 1)"
                  >
                    <input
                      class="plate-input"
                      :value="vehicle.plateCharacters[i]"
                      :focus="isPlateInputFocused(index, i + 1)"
                      maxlength="1"
                      @input="handlePlateInput($event, index, i + 1)"
                      @focus="handlePlateCharClick(index, i + 1)"
                      @keydown.delete="handlePlateDelete(index, i + 1)"
                    />
                    <text class="plate-input-text">{{ vehicle.plateCharacters[i] }}</text>
                  </view>
                  <!-- 新能源位 -->
                  <view
@@ -1435,15 +1618,7 @@
                    :class="{ 'plate-box-active': focusedVehicleIndex === index && focusedCharIndex === 7 }"
                    @click="handlePlateCharClick(index, 7)"
                  >
                    <input
                      class="plate-input"
                      :value="vehicle.plateCharacters[6]"
                      :focus="isPlateInputFocused(index, 7)"
                      maxlength="1"
                      @input="handlePlateInput($event, index, 7)"
                      @focus="handlePlateCharClick(index, 7)"
                      @keydown.delete="handlePlateDelete(index, 7)"
                    />
                    <text class="plate-input-text">{{ vehicle.plateCharacters[6] }}</text>
                    <text v-if="!vehicle.plateCharacters[6]" class="energy-placeholder">新能源</text>
                  </view>
                </view>
@@ -1497,14 +1672,14 @@
              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-[72rpx] shrink-0 text-[28rpx] text-[#333333]">
                <text class="w-[96rpx] shrink-0 truncate text-[28rpx] text-[#333333]">
                  {{ visitor.name }}
                </text>
                <view class="ml-[20rpx] min-w-0 flex-1">
                <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.phone || visitor.idCardMasked || '-' }}
                      {{ visitor.idCardMasked || '-' }}
                    </text>
                  </view>
                  <view class="mt-[8rpx] flex items-center min-w-0">
@@ -1515,10 +1690,10 @@
                  </view>
                </view>
              </view>
              <view class="ml-[16rpx] flex items-center shrink-0">
              <view class="ml-[8rpx] flex items-center shrink-0">
                <view
                  v-if="visitor.badge"
                  class="rounded-full border border-[#1677FF] border-solid px-[20rpx] py-[8rpx] text-[24rpx] text-[#1677FF]"
                  class="rounded-full border border-[#1677FF] border-solid px-[16rpx] py-[8rpx] text-[24rpx] text-[#1677FF]"
                >
                  {{ visitor.badge }}
                </view>
@@ -1575,6 +1750,7 @@
              :columns="reasonColumns"
              :item-height="48"
              :columns-height="144"
              :immediate-change="true"
              custom-class="custom-picker-view"
            />
          </view>
@@ -1590,7 +1766,7 @@
      </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-[830rpx]">
        <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]"
@@ -1601,19 +1777,20 @@
          </view>
          <!-- 弹窗内容区 -->
          <view class="px-[32rpx] flex-1 flex flex-col justify-center relative">
          <view class="px-[32rpx] pt-[20rpx] flex-1 flex flex-col justify-start">
            <!-- 头部列名 -->
            <view class="flex items-center absolute top-[40rpx] left-[32rpx] right-[32rpx] z-10 text-[24rpx] text-[#333333] font-medium">
            <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 mt-[60rpx]"
              class="custom-picker-view time-picker-view"
              :value="timePickerIndex"
              :immediate-change="true"
              indicator-style="height: 48px;"
              style="width: 100%; height: 192px;"
              style="width: 100%; height: 144px;"
              @change="handleTimeChange"
            >
              <picker-view-column v-for="(col, index) in timeColumns" :key="index" class="wd-picker-view__column">
@@ -1630,7 +1807,7 @@
          </view>
          <!-- 弹窗底部按钮 -->
          <view class="px-[32rpx] pb-[48rpx] shrink-0">
          <view class="px-[32rpx] pb-[8rpx] shrink-0">
            <wd-button type="primary" size="large" block custom-class="popup-submit-btn"
              @click="handleTimeConfirm">
              提交
@@ -1640,7 +1817,7 @@
      </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-[830rpx]">
        <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">
@@ -1649,17 +1826,18 @@
            <text class="text-[32rpx] text-[#333333] font-medium">选择时间</text>
          </view>
          <view class="px-[32rpx] flex-1 flex flex-col justify-center relative">
            <view class="flex items-center absolute top-[40rpx] left-[32rpx] right-[32rpx] z-10 text-[24rpx] text-[#333333] font-medium">
          <view class="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 mt-[60rpx]"
              class="custom-picker-view multi-date-picker-view"
              :value="multiDatePickerIndex"
              :immediate-change="true"
              indicator-style="height: 48px;"
              style="width: 100%; height: 192px;"
              style="width: 100%; height: 144px;"
              @change="handleMultiDateChange"
            >
              <picker-view-column
@@ -1679,7 +1857,7 @@
            </picker-view>
          </view>
          <view class="px-[32rpx] pb-[48rpx] shrink-0">
          <view class="px-[32rpx] pb-[8rpx] shrink-0">
            <wd-button type="primary" size="large" block custom-class="popup-submit-btn"
              @click="handleMultiDateConfirm">
              提交
@@ -1721,6 +1899,20 @@
        </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>
@@ -1781,6 +1973,15 @@
  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 {
@@ -1905,7 +2106,7 @@
   color: #B3B3B3 !important;
   font-size: 28rpx !important;
   background-color: transparent !important;
   transition: all 0.3s;
   transition: color 0.08s linear, background-color 0.08s linear;
   z-index: 10;
   position: relative;
}
@@ -1917,7 +2118,7 @@
:deep(.custom-picker-view .wd-picker-view-column__item) {
  font-size: 28rpx;
  color: #999999;
  transition: all 0.3s;
  transition: color 0.08s linear, background-color 0.08s linear;
  position: relative;
  z-index: 10;
}