index.vue 7.79 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>