| | |
| | | <script setup lang="ts"> |
| | | import { reactive } from 'vue' |
| | | import { createVisitor, fetchVisitorDetail, updateVisitor } from '@/api/visitor' |
| | | import { fetchMiniFieldConfigs } from '@/api/lookup' |
| | | import { FIELD_CODES } from '@/constants/fieldCodes' |
| | | import type { MiniFieldConfigItem } from '@/types/lookup' |
| | | import type { MiniVisitorSaveRequest } from '@/types/visitor' |
| | | |
| | | definePage({ |
| | | name: 'addPerson', |
| | | layout: 'default', |
| | | style: { |
| | | navigationStyle: 'custom' |
| | | navigationStyle: 'custom', |
| | | }, |
| | | }) |
| | | |
| | | const router = useRouter() |
| | | |
| | | // 状态栏高度 |
| | | const sysInfo = uni.getSystemInfoSync() |
| | | const statusBarHeight = sysInfo.statusBarHeight || 0 |
| | | const navBarHeight = 44 |
| | | |
| | | const pageMode = ref<'add' | 'edit'>('add') |
| | | const visitorId = ref<number>() |
| | | const visitorUuid = ref('') |
| | | const loading = ref(false) |
| | | const saving = ref(false) |
| | | const fieldConfigs = ref<MiniFieldConfigItem[]>([]) |
| | | |
| | | const formData = reactive({ |
| | | name: '', |
| | | phone: '', |
| | | company: '', |
| | | idCard: '', |
| | | gender: 'male', |
| | | gender: '男' as '男' | '女', |
| | | }) |
| | | |
| | | const errors = reactive({ |
| | |
| | | idCard: '', |
| | | }) |
| | | |
| | | function validate() { |
| | | let isValid = true |
| | | const pageTitle = computed(() => pageMode.value === 'edit' ? '编辑来访人' : '新增来访人') |
| | | |
| | | function findConfig(code: string) { |
| | | return fieldConfigs.value.find(item => item.fieldCode === code) |
| | | } |
| | | |
| | | function isVisible(code: string) { |
| | | const config = findConfig(code) |
| | | return config ? config.visible === 1 : true |
| | | } |
| | | |
| | | function isRequired(code: string) { |
| | | const config = findConfig(code) |
| | | return config ? config.requiredFlag === 1 : false |
| | | } |
| | | |
| | | function resetErrors() { |
| | | errors.name = '' |
| | | errors.phone = '' |
| | | errors.company = '' |
| | | errors.idCard = '' |
| | | } |
| | | |
| | | if (!formData.name) { |
| | | function validate() { |
| | | let isValid = true |
| | | resetErrors() |
| | | |
| | | if (isVisible(FIELD_CODES.VISITOR_NAME) && isRequired(FIELD_CODES.VISITOR_NAME) && !formData.name.trim()) { |
| | | errors.name = '请输入来访人姓名' |
| | | isValid = false |
| | | } |
| | | |
| | | if (!formData.phone) { |
| | | else if (formData.name && formData.name.trim().length > 30) { |
| | | errors.name = '来访人姓名不能超过30个字' |
| | | isValid = false |
| | | } |
| | | |
| | | if (isVisible(FIELD_CODES.MOBILE) && isRequired(FIELD_CODES.MOBILE) && !formData.phone.trim()) { |
| | | errors.phone = '请输入来访人电话' |
| | | isValid = false |
| | | } else if (!/^1[3-9]\d{9}$/.test(formData.phone)) { |
| | | } |
| | | else if (formData.phone && !/^\d{11}$/.test(formData.phone.trim())) { |
| | | errors.phone = '请确保电话号码录入正确!' |
| | | isValid = false |
| | | } |
| | | |
| | | if (!formData.company) { |
| | | |
| | | if (isVisible(FIELD_CODES.COMPANY) && isRequired(FIELD_CODES.COMPANY) && !formData.company.trim()) { |
| | | errors.company = '请输入来访人单位' |
| | | isValid = false |
| | | } |
| | | |
| | | if (!formData.idCard) { |
| | | |
| | | if (isVisible(FIELD_CODES.ID_CARD) && isRequired(FIELD_CODES.ID_CARD) && !formData.idCard.trim()) { |
| | | errors.idCard = '请输入身份证号码' |
| | | isValid = false |
| | | } else if (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(formData.idCard)) { |
| | | } |
| | | else if (formData.idCard && !/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(formData.idCard.trim())) { |
| | | errors.idCard = '请确保身份证号码录入正确!' |
| | | isValid = false |
| | | } |
| | |
| | | return isValid |
| | | } |
| | | |
| | | function handleSave() { |
| | | if (validate()) { |
| | | function buildPayload(): MiniVisitorSaveRequest { |
| | | return { |
| | | visitorName: formData.name.trim(), |
| | | visitorMobile: formData.phone.trim(), |
| | | visitorCompany: formData.company.trim(), |
| | | idCard: formData.idCard.trim(), |
| | | gender: formData.gender, |
| | | } |
| | | } |
| | | |
| | | async function loadFieldConfigs() { |
| | | fieldConfigs.value = await fetchMiniFieldConfigs() |
| | | } |
| | | |
| | | async function loadDetail() { |
| | | if (pageMode.value !== 'edit' || !visitorId.value || !visitorUuid.value) { |
| | | return |
| | | } |
| | | const detail = await fetchVisitorDetail(visitorId.value, visitorUuid.value) |
| | | formData.name = detail.visitorName || '' |
| | | formData.phone = detail.visitorMobile || '' |
| | | formData.company = detail.visitorCompany || '' |
| | | formData.idCard = detail.idCard || '' |
| | | formData.gender = detail.gender === '女' ? '女' : '男' |
| | | } |
| | | |
| | | async function initPage() { |
| | | loading.value = true |
| | | try { |
| | | await loadFieldConfigs() |
| | | await loadDetail() |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '加载来访人信息失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleSave() { |
| | | if (saving.value || !validate()) { |
| | | return |
| | | } |
| | | saving.value = true |
| | | try { |
| | | if (pageMode.value === 'edit' && visitorId.value && visitorUuid.value) { |
| | | await updateVisitor(visitorId.value, visitorUuid.value, buildPayload()) |
| | | } |
| | | else { |
| | | await createVisitor(buildPayload()) |
| | | } |
| | | uni.showToast({ title: '保存成功', icon: 'success' }) |
| | | setTimeout(() => { |
| | | router.back() |
| | | }, 1500) |
| | | }, 300) |
| | | } |
| | | catch (error) { |
| | | uni.showToast({ |
| | | title: error instanceof Error ? error.message : '保存来访人失败', |
| | | icon: 'none', |
| | | }) |
| | | } |
| | | finally { |
| | | saving.value = false |
| | | } |
| | | } |
| | | |
| | | onLoad((options) => { |
| | | visitorId.value = options?.id ? Number(options.id) : undefined |
| | | visitorUuid.value = options?.uuid || '' |
| | | pageMode.value = visitorId.value && visitorUuid.value ? 'edit' : 'add' |
| | | void initPage() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <view class="min-h-screen bg-[#F6FBFF] box-border relative pb-[200rpx]"> |
| | | |
| | | <!-- 自定义导航栏 --> |
| | | <view class="relative z-50 w-full bg-[#F6FBFF]" :style="{ height: (statusBarHeight + navBarHeight) + 'px' }"> |
| | | <view class="flex items-center justify-between w-full absolute left-0" |
| | | :style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }"> |
| | | <view class="flex items-center justify-between w-full absolute left-0" :style="{ top: statusBarHeight + 'px', height: navBarHeight + 'px' }"> |
| | | <view class="flex h-full w-[88rpx] items-center justify-center relative z-10" @click="router.back()"> |
| | | <wd-icon name="arrow-left" size="40rpx" color="#333333" /> |
| | | </view> |
| | | <view class="absolute left-0 w-full text-center text-[34rpx] font-medium text-[#333333] z-0"> |
| | | 选择来访人 |
| | | {{ pageTitle }} |
| | | </view> |
| | | <view class="w-[88rpx]"></view> |
| | | <view class="w-[88rpx]" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 表单区域 --> |
| | | <view class="bg-white px-[64rpx] py-[48rpx]"> |
| | | |
| | | <!-- 来访人姓名 --> |
| | | <view class="py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view v-if="loading" class="px-[64rpx] py-[80rpx] text-center text-[28rpx] text-[#999999]"> |
| | | 加载中... |
| | | </view> |
| | | |
| | | <view v-else class="bg-white px-[64rpx] py-[48rpx]"> |
| | | <view v-if="isVisible(FIELD_CODES.VISITOR_NAME)" class="py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view class="flex flex-col shrink-0 mr-[32rpx]"> |
| | | <view class="flex items-center h-[40rpx]"> |
| | | <text class="text-[28rpx] text-[#000000]">来访人姓名</text> |
| | | <text class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | <text v-if="isRequired(FIELD_CODES.VISITOR_NAME)" class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | </view> |
| | | <text v-if="errors.name" class="text-[20rpx] text-[#F5222D] mt-[8rpx]">{{ errors.name }}</text> |
| | | </view> |
| | | <input |
| | | v-model="formData.name" |
| | | placeholder="请输入来访人姓名" |
| | | placeholder-class="text-[#B3B3B3] text-[28rpx]" |
| | | class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" |
| | | /> |
| | | <input v-model="formData.name" placeholder="请输入来访人姓名" placeholder-class="text-[#B3B3B3] text-[28rpx]" class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" /> |
| | | </view> |
| | | |
| | | <!-- 来访人电话 --> |
| | | <view class=" py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view v-if="isVisible(FIELD_CODES.MOBILE)" class="py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view class="flex flex-col shrink-0 mr-[32rpx]"> |
| | | <view class="flex items-center h-[40rpx]"> |
| | | <text class="text-[28rpx] text-[#000000]">来访人电话</text> |
| | | <text class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | <text v-if="isRequired(FIELD_CODES.MOBILE)" class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | </view> |
| | | <text v-if="errors.phone" class="text-[20rpx] text-[#F5222D] mt-[8rpx]">{{ errors.phone }}</text> |
| | | </view> |
| | | <input |
| | | v-model="formData.phone" |
| | | type="number" |
| | | maxlength="11" |
| | | placeholder="请输入来访人电话" |
| | | placeholder-class="text-[#B3B3B3] text-[28rpx]" |
| | | class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" |
| | | /> |
| | | <input v-model="formData.phone" type="number" maxlength="11" placeholder="请输入来访人电话" placeholder-class="text-[#B3B3B3] text-[28rpx]" class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" /> |
| | | </view> |
| | | |
| | | <!-- 来访人单位 --> |
| | | <view class=" py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view v-if="isVisible(FIELD_CODES.COMPANY)" class="py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view class="flex flex-col shrink-0 mr-[32rpx]"> |
| | | <view class="flex items-center h-[40rpx]"> |
| | | <text class="text-[28rpx] text-[#000000]">来访人单位</text> |
| | | <text class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | <text v-if="isRequired(FIELD_CODES.COMPANY)" class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | </view> |
| | | <text v-if="errors.company" class="text-[20rpx] text-[#F5222D] mt-[8rpx]">{{ errors.company }}</text> |
| | | </view> |
| | | <input |
| | | v-model="formData.company" |
| | | placeholder="请输入来访人单位" |
| | | placeholder-class="text-[#B3B3B3] text-[28rpx]" |
| | | class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" |
| | | /> |
| | | <input v-model="formData.company" placeholder="请输入来访人单位" placeholder-class="text-[#B3B3B3] text-[28rpx]" class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" /> |
| | | </view> |
| | | |
| | | <!-- 身份证号码 --> |
| | | <view class=" py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view v-if="isVisible(FIELD_CODES.ID_CARD)" class="py-[28rpx] flex items-start justify-between border-b1"> |
| | | <view class="flex flex-col shrink-0 mr-[32rpx]"> |
| | | <view class="flex items-center h-[40rpx]"> |
| | | <text class="text-[28rpx] text-[#000000]">身份证号码</text> |
| | | <text class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | <text v-if="isRequired(FIELD_CODES.ID_CARD)" class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | </view> |
| | | <text v-if="errors.idCard" class="text-[20rpx] text-[#F5222D] mt-[8rpx]">{{ errors.idCard }}</text> |
| | | </view> |
| | | <input |
| | | v-model="formData.idCard" |
| | | type="idcard" |
| | | placeholder="请输入身份证号码" |
| | | placeholder-class="text-[#B3B3B3] text-[28rpx]" |
| | | class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" |
| | | /> |
| | | <input v-model="formData.idCard" type="idcard" placeholder="请输入身份证号码" placeholder-class="text-[#B3B3B3] text-[28rpx]" class="text-[28rpx] text-right text-[#333333] flex-1 h-[40rpx] min-h-[40rpx]" /> |
| | | </view> |
| | | |
| | | <!-- 性别 --> |
| | | <view class="py-[28rpx] flex items-start justify-between"> |
| | | <view v-if="isVisible(FIELD_CODES.GENDER)" class="py-[28rpx] flex items-start justify-between"> |
| | | <view class="flex flex-col shrink-0 mr-[32rpx]"> |
| | | <view class="flex items-center h-[40rpx]"> |
| | | <text class="text-[28rpx] text-[#000000]">性别</text> |
| | | <text class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | <text v-if="isRequired(FIELD_CODES.GENDER)" class="text-[28rpx] text-[#F5222D] ml-[4rpx]">*</text> |
| | | </view> |
| | | </view> |
| | | <view class="flex items-center space-x-[48rpx] h-[40rpx]"> |
| | | <view class="flex items-center" @click="formData.gender = 'male'"> |
| | | <view class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center transition-colors duration-200" |
| | | :class="formData.gender === 'male' ? 'border-[#0068FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="formData.gender === 'male'" class="w-[24rp x] h-[24rpx] rounded-full bg-[#0068FF]"></view> |
| | | <view class="flex items-center" @click="formData.gender = '男'"> |
| | | <view class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center transition-colors duration-200" :class="formData.gender === '男' ? 'border-[#0068FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="formData.gender === '男'" class="w-[24rpx] h-[24rpx] rounded-full bg-[#0068FF]" /> |
| | | </view> |
| | | <text class="ml-[12rpx] text-[28rpx] text-[#333333]">男</text> |
| | | </view> |
| | | <view class="flex items-center" @click="formData.gender = 'female'"> |
| | | <view class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center transition-colors duration-200" |
| | | :class="formData.gender === 'female' ? 'border-[#0068FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="formData.gender === 'female'" class="w-[24rpx] h-[24rpx] rounded-full bg-[#0068FF]"></view> |
| | | <view class="flex items-center" @click="formData.gender = '女'"> |
| | | <view class="w-[32rpx] h-[32rpx] rounded-full border-[2rpx] border-solid flex items-center justify-center transition-colors duration-200" :class="formData.gender === '女' ? 'border-[#0068FF]' : 'border-[#CFCFCF]'"> |
| | | <view v-if="formData.gender === '女'" class="w-[24rpx] h-[24rpx] rounded-full bg-[#0068FF]" /> |
| | | </view> |
| | | <text class="ml-[12rpx] text-[28rpx] text-[#333333]">女</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | </view> |
| | | |
| | | <!-- 保存按钮 --> |
| | | <view class="fixed bottom-[94rpx] left-0 w-full px-[32rpx] box-border z-[10]"> |
| | | <view |
| | | class="h-[96rpx] rounded-full bg-[#0068FF] flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)]" |
| | | @click="handleSave" |
| | | > |
| | | <text class="text-[32rpx] text-white font-medium">保存</text> |
| | | <view class="h-[96rpx] rounded-full bg-[#0068FF] flex items-center justify-center shadow-[0_8rpx_24rpx_rgba(0,104,255,0.2)]" @click="handleSave"> |
| | | <text class="text-[32rpx] text-white font-medium">{{ saving ? '保存中...' : '保存' }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | </view> |
| | | </template> |
| | | |