hookehuyr

feat(添加参观者): 重构表单界面并添加证件类型选择功能

重构添加参观者页面表单布局,使用卡片式设计提升用户体验
添加证件类型选择器,支持身份证和其他证件类型
移除历史参观者列表功能以简化页面
添加表单验证和温馨提示信息
......@@ -11,9 +11,8 @@ declare module 'vue' {
NutCheckbox: typeof import('@nutui/nutui-taro')['Checkbox']
NutCheckboxGroup: typeof import('@nutui/nutui-taro')['CheckboxGroup']
NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
NutForm: typeof import('@nutui/nutui-taro')['Form']
NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
NutPopup: typeof import('@nutui/nutui-taro')['Popup']
OfflineQrCode: typeof import('./src/components/offlineQrCode.vue')['default']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
......
<!--
* @Date: 2024-01-15 16:35:10
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-06 23:28:40
* @LastEditTime: 2026-01-07 22:54:19
* @FilePath: /xyxBooking-weapp/src/pages/addVisitor/index.vue
* @Description: 添加参观者
-->
<template>
<view class="add-visitor-page">
<nut-form>
<nut-form-item label="姓名">
<nut-input v-model="name" placeholder="请输入参观者姓名" type="text" />
</nut-form-item>
<nut-form-item label="证件类型">
<view style="padding: 20rpx 0;">身份证</view>
</nut-form-item>
<nut-form-item label="证件号">
<nut-input v-model="id_number" placeholder="请输入参观者证件号" type="idcard" />
</nut-form-item>
</nut-form>
<view style="padding: 32rpx;">
<nut-button type="primary" block color="#A67939" @click="save">保存</nut-button>
</view>
<view v-if="visitorList.length" class="history-list">
<view class="title">历史参观者</view>
<view v-for="(item, index) in visitorList" :key="index" class="item">
<view class="info">
<view class="name">{{ item.name }}</view>
<view class="id">{{ formatId(item.id_number) }}</view>
<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 class="action" @tap="delVisitor(item.id)">删除</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 } from 'vue'
import Taro, { useDidShow } from '@tarojs/taro'
import { personListAPI, addPersonAPI, delPersonAPI } from '@/api/index'
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 visitorList = 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) => {
......@@ -51,31 +92,16 @@ const checkIDCard = (idcode) => {
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(idcode);
}
function replaceMiddleCharacters(inputString) {
if (!inputString || inputString.length < 15) {
return inputString;
}
const start = Math.floor((inputString.length - 8) / 2);
const end = start + 8;
const replacement = '*'.repeat(8);
return inputString.substring(0, start) + replacement + inputString.substring(end);
}
const formatId = (id) => replaceMiddleCharacters(id);
const loadList = async () => {
const { code, data } = await personListAPI({});
if (code) {
visitorList.value = data || [];
}
}
const save = async () => {
if (!name.value) {
Taro.showToast({ title: '请输入姓名', icon: 'none' });
return;
}
if (!checkIDCard(id_number.value)) {
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;
}
......@@ -83,7 +109,7 @@ const save = async () => {
Taro.showLoading({ title: '保存中' });
const { code, msg } = await addPersonAPI({
name: name.value,
id_type: 1, // 身份证
id_type: id_type.value,
id_number: id_number.value
});
Taro.hideLoading();
......@@ -92,30 +118,11 @@ const save = async () => {
Taro.showToast({ title: '添加成功' });
name.value = '';
id_number.value = '';
loadList();
// 自动返回上一页? H5 没有自动返回
Taro.navigateBack();
} else {
Taro.showToast({ title: msg || '添加失败', icon: 'none' });
}
}
const delVisitor = async (id) => {
const { confirm } = await Taro.showModal({ title: '提示', content: '确定删除该参观者吗?' });
if (confirm) {
const { code, msg } = await delPersonAPI({ person_id: id });
if (code) {
Taro.showToast({ title: '删除成功' });
loadList();
} else {
Taro.showToast({ title: msg || '删除失败', icon: 'none' });
}
}
}
useDidShow(() => {
loadList();
})
</script>
<style lang="less">
......@@ -124,48 +131,89 @@ useDidShow(() => {
background-color: #F6F6F6;
padding-top: 2rpx;
.history-list {
margin-top: 32rpx;
background-color: #FFF;
.content {
padding: 32rpx;
padding-bottom: 220rpx;
}
.form-card {
background-color: #FFF;
border-radius: 16rpx;
overflow: hidden;
}
.form-row {
display: flex;
align-items: center;
padding: 0 32rpx;
height: 112rpx;
&:not(:last-child) {
border-bottom: 2rpx solid #F2F2F2;
}
.label {
width: 160rpx;
color: #333;
font-size: 30rpx;
}
.title {
font-size: 32rpx;
.field-value {
flex: 1;
text-align: right;
color: #333;
margin-bottom: 32rpx;
border-left: 6rpx solid #A67939;
padding-left: 16rpx;
font-size: 30rpx;
}
.item {
.field-input {
flex: 1;
}
.picker-value {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 0;
border-bottom: 2rpx solid #EEE;
&:last-child {
border-bottom: none;
}
.info {
.name {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.id {
font-size: 29rpx;
color: #999;
}
}
.action {
color: #FF0000;
font-size: 29rpx;
}
justify-content: flex-end;
}
.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>
......