longz
2026-04-27 25df88d0ab9d7444ed1d162b9efa1259ac2f6f66
yuyueMiniProgram/src/pages/addPerson/index.vue
@@ -1,27 +1,37 @@
<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({
@@ -31,35 +41,61 @@
  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
  }
@@ -67,145 +103,177 @@
  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>