index.vue 7.26 KB
<!--
 * @Date: 2024-01-15 16:35:10
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2026-01-20 16:04:01
 * @FilePath: /xyxBooking-weapp/src/pages/addVisitor/index.vue
 * @Description: 添加参观者
-->
<template>
  <view class="add-visitor-page">
    <view class="content">
      <view class="form-card">
        <view class="form-row">
          <view class="label">姓名</view>
          <nut-input
            v-model="name"
            class="field-input"
            placeholder="请输入参观者真实姓名"
            type="text"
            input-align="right"
            :border="false"
          />
        </view>
        <view class="form-row">
          <view class="label">证件类型</view>
          <view class="field-value picker-value" @tap="open_id_type_picker">
            <text>{{ id_type_label }}</text>
            <text class="picker-arrow">›</text>
          </view>
        </view>
        <view class="form-row">
          <view class="label">证件号码</view>
          <nut-input
            v-model="id_number"
            class="field-input"
            placeholder="请输入证件号码"
            :type="id_number_type"
            input-align="right"
            :border="false"
          />
        </view>
      </view>

      <view class="tip">
        <IconFont name="tips" size="14" color="#C7A46D" />
        <text class="tip-text">温馨提示:账号实名认证信息一经填写将无法修改</text>
      </view>
    </view>

    <view class="footer">
      <view class="save-btn" @tap="save">保存</view>
    </view>

    <nut-popup v-model:visible="show_id_type_picker" position="bottom" safe-area-inset-bottom>
      <nut-picker
        v-model="id_type_picker_value"
        :columns="id_type_columns"
        title="选择证件类型"
        @confirm="on_id_type_confirm"
        @cancel="show_id_type_picker = false"
      ></nut-picker>
    </nut-popup>
  </view>
</template>

<script setup>
import { ref, computed } from 'vue'
import Taro from '@tarojs/taro'
import { addPersonAPI } from '@/api/index'
import { IconFont } from '@nutui/icons-vue-taro'

const name = ref('')
const id_number = ref('')
const show_id_type_picker = ref(false)
const id_type_options = [
  { label: '身份证', value: 1 },
  { label: '其他', value: 3 }
]
const id_type = ref(id_type_options[0].value)
const id_type_picker_value = ref([String(id_type.value)])

const id_type_columns = computed(() => {
  return id_type_options.map(item => ({
    text: item.label,
    value: String(item.value)
  }))
})
const id_type_label = computed(() => {
  return (
    id_type_options.find(item => item.value === id_type.value)?.label || id_type_options[0].label
  )
})
const id_number_type = computed(() => (id_type.value === 1 ? 'idcard' : 'text'))

const open_id_type_picker = () => {
  id_type_picker_value.value = [String(id_type.value)]
  show_id_type_picker.value = true
}

const on_id_type_confirm = ({ selectedValue }) => {
  const value = selectedValue?.[0]
  id_type.value = Number(value) || 1
  show_id_type_picker.value = false
}

// 身份证校验
const checkIDCard = idcode => {
  // 1. 基础格式校验 (18位)
  if (
    !idcode ||
    !/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(
      idcode
    )
  ) {
    return false
  }

  // 2. 地区码校验
  const cityMap = {
    11: '北京',
    12: '天津',
    13: '河北',
    14: '山西',
    15: '内蒙古',
    21: '辽宁',
    22: '吉林',
    23: '黑龙江',
    31: '上海',
    32: '江苏',
    33: '浙江',
    34: '安徽',
    35: '福建',
    36: '江西',
    37: '山东',
    41: '河南',
    42: '湖北',
    43: '湖南',
    44: '广东',
    45: '广西',
    46: '海南',
    50: '重庆',
    51: '四川',
    52: '贵州',
    53: '云南',
    54: '西藏',
    61: '陕西',
    62: '甘肃',
    63: '青海',
    64: '宁夏',
    65: '新疆',
    71: '台湾',
    81: '香港',
    82: '澳门',
    91: '国外'
  }
  if (!cityMap[idcode.substr(0, 2)]) {
    return false
  }

  // 3. 出生日期校验
  const birthday = idcode.substr(6, 8)
  const year = parseInt(birthday.substr(0, 4))
  const month = parseInt(birthday.substr(4, 2))
  const day = parseInt(birthday.substr(6, 2))
  const date = new Date(year, month - 1, day)

  if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) {
    return false
  }

  // 校验日期不能超过当前时间
  if (date > new Date()) {
    return false
  }

  // 4. 校验码计算
  // 加权因子
  const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
  // 校验位对应值
  const parity = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']

  let sum = 0
  const codeArr = idcode.split('')
  // 计算加权和
  for (let i = 0; i < 17; i++) {
    sum += codeArr[i] * factor[i]
  }
  // 取模
  const mod = sum % 11
  // 获取校验位
  const last = parity[mod]

  // 对比最后一位 (统一转大写比较)
  return last === codeArr[17].toUpperCase()
}

const save = async () => {
  if (!name.value) {
    Taro.showToast({ title: '请输入姓名', icon: 'none' })
    return
  }
  if (!id_number.value) {
    Taro.showToast({ title: '请输入证件号码', icon: 'none' })
    return
  }
  if (id_type.value === 1 && !checkIDCard(id_number.value)) {
    Taro.showToast({ title: '请输入正确的身份证号', icon: 'none' })
    return
  }

  Taro.showLoading({ title: '保存中' })
  const { code, msg } = await addPersonAPI({
    name: name.value,
    id_type: id_type.value,
    id_number: id_number.value
  })
  Taro.hideLoading()

  if (code) {
    Taro.showToast({ title: '添加成功' })
    name.value = ''
    id_number.value = ''
    Taro.navigateBack()
  } else {
    Taro.showToast({ title: msg || '添加失败', icon: 'none' })
  }
}
</script>

<style lang="less">
.add-visitor-page {
  min-height: 100vh;
  background-color: #f6f6f6;
  padding-top: 2rpx;

  .content {
    padding: 32rpx;
    padding-bottom: 220rpx;
  }

  .form-card {
    background-color: #fff;
    border-radius: 16rpx;
    overflow: hidden;
  }

  .form-row {
    display: flex;
    align-items: center;
    padding-left: 32rpx;
    height: 112rpx;

    &:not(:last-child) {
      border-bottom: 2rpx solid #f2f2f2;
    }

    .label {
      width: 160rpx;
      color: #333;
      font-size: 30rpx;
    }

    .field-value {
      flex: 1;
      text-align: right;
      color: #333;
      font-size: 30rpx;
    }

    .field-input {
      flex: 1;
    }

    .picker-value {
      display: flex;
      align-items: center;
      justify-content: flex-end;
      padding-right: 50rpx;
    }

    .picker-arrow {
      margin-left: 10rpx;
      color: #bbb;
      font-size: 28rpx;
    }
  }

  .tip {
    margin-top: 28rpx;
    display: flex;
    align-items: center;
    color: #c7a46d;
    font-size: 24rpx;

    .tip-text {
      margin-left: 10rpx;
    }
  }

  .footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
    background-color: #f6f6f6;
  }

  .save-btn {
    width: 686rpx;
    height: 96rpx;
    background-color: #a67939;
    border-radius: 12rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 34rpx;
    font-weight: 600;
  }
}
</style>