Files
property-resident-side/property-uniapp-project/pages/visitor/visitor-add.vue
2026-04-18 08:31:55 +08:00

496 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="visitor-add-page">
<!-- Tab 切换车辆来访/人员来访 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: activeTab === 'car' }"
@click="switchTab('car')"
>
车辆来访
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'person' }"
@click="switchTab('person')"
>
人员来访
</view>
</view>
<!-- 表单区域 -->
<scroll-view class="form-scroll" scroll-y>
<view class="form-section">
<uni-forms ref="formRef" :modelValue="formData" :rules="formRules" label-width="105px" label-align="left">
<!-- 来访地址 -->
<uni-forms-item label="来访地址:" name="address" required>
<uni-data-select v-model="formData.address" :localdata="addressList" @change="changeAddress" :clear="false"></uni-data-select>
</uni-forms-item>
<!-- 访客姓名 -->
<uni-forms-item label="访客姓名" name="name" required>
<uni-easyinput type="text" v-model="formData.name" placeholder="请输入姓名" />
</uni-forms-item>
<!-- 访客性别 -->
<uni-forms-item label="访客性别" name="gender" required>
<uni-data-select v-model="formData.gender" :localdata="sexList" @change="changeSex" :clear="false"></uni-data-select>
</uni-forms-item>
<!-- 访客联系方式 -->
<uni-forms-item label="访客联系方式" name="phone" required>
<uni-easyinput
v-model="formData.phone"
placeholder="请输入联系方式"
type="number" />
</uni-forms-item>
<!-- 车牌号仅车辆来访显示 -->
<uni-forms-item
label="车牌号:"
name="carNumber"
required
v-if="activeTab === 'car'"
>
<view class="plate-input-wrap">
<!-- 省份简称选择框 -->
<view
class="plate-item province-item"
@click="showProvinceClick"
>
<text>{{ plateChars[0] || '选' }}</text>
</view>
<!-- 6个字符输入框省份+6=完整车牌 -->
<input
v-for="(char, index) in plateChars.slice(1)"
:key="index"
ref="el => plateInputRefs[index + 1] = el"
class="plate-item char-item"
v-model="plateChars[index + 1]"
maxlength="1"
type="text"
@input="handlePlateInput(index + 1, $event)"
placeholder=""
@keydown="handlePlateKeydown(index + 1, $event)"/>
</view>
</uni-forms-item>
<!-- 随行人数 -->
<uni-forms-item label="随行人数:" name="escortNum">
<uni-easyinput v-model="formData.escortNum" placeholder="请输入随行人数" type="number"/>
</uni-forms-item>
<!-- 来访时间 -->
<uni-forms-item label="来访时间:" name="visitTime" required>
<uni-datetime-picker type="datetime" v-model="formData.visitTime" @change="handleTimeChange" />
</uni-forms-item>
<!-- 离开时间 -->
<uni-forms-item label="离开时间:" name="leaveTime" required>
<uni-datetime-picker type="datetime" v-model="formData.leaveTime" @change="handleTimeChange" />
</uni-forms-item>
</uni-forms>
</view>
<!-- 提示文字 -->
<view class="tips-text">
预计来访时间和预计离开时间间隔不能超过24小时
<view>车辆停留请勿超出有效期需及时驶出小区</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="btn-footer">
<button
class="main-btn"
@click="activeTab === 'car' ? submitInvite() : goToQrcode()"
>
{{ activeTab === 'car' ? '确认邀请' : '生成通行二维码' }}
</button>
</view>
<!-- 省份选择弹窗 -->
<uni-popup v-model="showProvinceSelect" type="center" ref="popup">
<view class="popup-select-wrap">
<text class="popup-title">选择省份简称</text>
<uni-data-select
v-model="plateChars[0]"
:localdata="provinceList"
@confirm="onProvinceConfirm"
@cancel="popup.close()"
placeholder="请选择省份简称"
></uni-data-select>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, watch, nextTick } from 'vue';
// 激活的Tabcar-车辆来访person-人员来访
const activeTab = ref('car');
const popup = ref(null)
// 表单数据
const formData = reactive({
address: '', // 来访地址
name: '', // 访客姓名
gender: '', // 访客性别
phone: '', // 联系方式
carNumber: '', // 车牌号(同步输入的完整车牌)
escortNum: '', // 随行人数
visitTime: '', // 来访时间
leaveTime: '' // 离开时间
});
// 表单Ref用于调用校验方法
const formRef = ref(null);
// 表单校验规则
const formRules = reactive({
address: {
rules: [{ required: true, errorMessage: '请选择来访地址' }]
},
name: {
rules: [{ required: true, errorMessage: '请输入访客姓名' }]
},
gender: {
rules: [{ required: true, errorMessage: '请选择访客性别' }]
},
phone: {
rules: [
{ required: true, errorMessage: '请输入正确的手机号' },
{ pattern: /^1[3-9]\d{9}$/, errorMessage: '请输入正确的手机号' }
]
},
carNumber: {
rules: [
{
required: true,
errorMessage: "请输入车牌号",
},
{
validateFunction: (rule, value, callback) => {
// 仅车辆来访时生效
if (activeTab.value !== 'car') return callback();
// 1. 校验省份是否选择
if (!plateChars.value[0]) {
return callback('请选择省份简称');
}
// 2. 拼接完整车牌号
const fullPlate = plateChars.value.join('').trim();
// 3. 校验车牌号长度(省份+5/6位字符
if (fullPlate.length < 7) {
return callback('请输入完整车牌号(省份+5位字符');
}
if (fullPlate.length > 8) {
return callback('车牌号长度超出限制最多7位');
}
// 4. 同步到formData.carNumber供接口提交
formData.carNumber = fullPlate;
callback();
}
}
]
},
visitTime: {
rules: [{ required: true, errorMessage: '请选择来访时间' }]
},
leaveTime: {
rules: [
{ required: true, errorMessage: '请选择离开时间' },
{
validateFunction: (rule, value, callback) => {
// 1. 空值校验
if (!value) return callback('请选择离开时间');
if (!formData.visitTime) return callback('请先选择来访时间');
// 2. 校验时间格式有效性
const visitTime = new Date(formData.visitTime);
const leaveTime = new Date(value);
if (isNaN(visitTime.getTime()) || isNaN(leaveTime.getTime())) {
return callback('请选择有效的时间');
}
// 3. 校验离开时间不能早于来访时间
if (leaveTime <= visitTime) {
return callback('离开时间不能早于来访时间');
}
// 4. 校验时间间隔≤24小时
const diffHours = (leaveTime.getTime() - visitTime.getTime()) / (1000 * 60 * 60);
if (diffHours > 24) {
return callback('来访和离开时间间隔不能超过24小时');
}
callback();
}
}
]
}
});
// 地址列表
const addressList = ref([
{ text: '桂花园别墅1号楼1层', value: '桂花园别墅1号楼1层' },
{ text: '桂花园别墅2号楼2层', value: '桂花园别墅2号楼2层' }
]);
// 性别列表
const sexList = ref([
{ text: '男', value: '男' },
{ text: '女', value: '女' }
]);
// 省份简称列表(国标规范)
const provinceList = ref([
{ text: '京', value: '京' },
{ text: '沪', value: '沪' },
{ text: '粤', value: '粤' },
{ text: '苏', value: '苏' },
{ text: '浙', value: '浙' },
{ text: '鲁', value: '鲁' },
{ text: '川', value: '川' },
{ text: '渝', value: '渝' },
{ text: '湘', value: '湘' },
{ text: '鄂', value: '鄂' }
]);
// 车牌号逐位存储索引0省份1-6字符 → 共7位兼容普通/新能源车牌)
const plateChars = ref(['', '', '', '', '', '', '','']);
const showProvinceSelect = ref(false); // 省份选择弹窗
const plateInputRefs = ref([]); // 输入框refs用于自动聚焦
// 打开省份弹框
const showProvinceClick = () => {
popup.value.open()
};
// 选择省份后赋值
const onProvinceConfirm = () => {
showProvinceSelect.value = false;
popup.value.close();
// 省份选择后自动聚焦第一个字符输入框
nextTick(() => {
plateInputRefs.value[1]?.focus();
});
};
// 车牌号字符输入处理(优化过滤和自动聚焦)
const handlePlateInput = (index, e) => {
// 过滤非字母数字,转大写(车牌仅支持字母/数字)
let val = e.detail.value.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
// 仅保留1个字符
val = val.slice(-1);
plateChars.value[index] = val;
// 自动聚焦下一个输入框(如果当前有值且不是最后一位)
if (val && index < 7) { // 最后一位是索引6无需再聚焦
nextTick(() => {
plateInputRefs.value[index + 1]?.focus();
});
}
// 输入完成后触发表单校验
if (activeTab.value === 'car') {
formRef.value.validateField('carNumber');
}
};
// 车牌号删除键处理(优化回退逻辑)
const handlePlateKeydown = (index, e) => {
// 监听回退键keyCode 8
if (e.keyCode === 8) {
// 当前输入框为空,聚焦上一个
if (!plateChars.value[index] && index > 1) {
nextTick(() => {
plateInputRefs.value[index - 1]?.focus();
});
} else {
// 清空当前输入框
plateChars.value[index] = '';
}
}
};
// 时间变更时重新校验离开时间
const handleTimeChange = () => {
if (formData.leaveTime) {
formRef.value.validateField('leaveTime');
}
};
// 切换Tab
const switchTab = (tab) => {
activeTab.value = tab;
// 切换Tab后重置表单
if (tab === 'person') {
resetForm();
}
// resetForm();
nextTick(() =>{
formRef.value.clearValidate() // 清空所有校验提示
// formRef.value.validate();
})
};
// 空方法占位
const changeAddress = () => {};
const changeSex = (e) => {};
// 提交前表单校验
const submitForm = async () => {
try {
// 调用uni-forms内置校验
await formRef.value.validate();
return true;
} catch (err) {
return false;
}
};
// 车辆来访:提交确认邀请
const submitInvite = async () => {
const isValid = await submitForm();
if (!isValid) return;
// 模拟接口提交
uni.showToast({
title: '邀请成功',
icon: 'success',
duration: 2000
});
setTimeout(() => {
resetForm();
}, 2000);
};
// 跳转到二维码页面(人员来访)
const goToQrcode = async () => {
const isValid = await submitForm();
if (!isValid) return;
const formStr = encodeURIComponent(JSON.stringify(formData));
uni.navigateTo({
url: `/pages/visitor/qrcode-page?form=${formStr}`
});
};
// 重置表单
const resetForm = () => {
formData.address = '';
formData.name = '';
formData.gender = '';
formData.phone = '';
formData.carNumber = '';
formData.escortNum = '';
formData.visitTime = '';
formData.leaveTime = '';
plateChars.value = ['', '', '', '', '', '', '']; // 重置车牌号输入
};
</script>
<style scoped>
.visitor-add-page {
background-color: #f8f8f8;
min-height: 100vh;
}
/* Tab 切换样式 */
.tab-bar {
display: flex;
background: #fff;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 32rpx;
color: #666;
border-bottom: 4rpx solid transparent;
}
.tab-item.active {
color: #007aff;
border-bottom-color: #007aff;
}
/* 表单样式 */
.form-scroll {
height: calc(100vh - 220rpx);
}
.form-section {
background: #fff;
margin-top: 20rpx;
padding: 20rpx;
}
/* 车牌号逐位输入样式 */
.plate-input-wrap {
flex: 1;
display: flex;
align-items: center;
gap: 4rpx;
}
.plate-item {
width: 52rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
border: 1rpx solid #ddd;
border-radius: 4rpx;
font-size: 28rpx;
color: #333;
}
.province-item {
background-color: #f8f8f8;
}
.char-item {
border: 1px solid #666;
outline: none;
padding: 0;
margin: 0 3rpx;
}
.char-item:focus {
border-color: #007aff;
box-shadow: 0 0 4rpx rgba(0, 122, 255, 0.5);
}
/* 省份选择弹窗样式 */
.popup-select-wrap {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
width: 600rpx;
margin: 0 auto;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
display: block;
text-align: center;
}
/* 提示文字 */
.tips-text {
margin: 20rpx;
font-size: 24rpx;
color: #999;
line-height: 1.6;
}
/* 底部按钮 */
.btn-footer {
padding: 20rpx;
background: #fff;
}
.main-btn {
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
height: 88rpx;
font-size: 32rpx;
}
</style>