Files
property-resident-side/property-uniapp-project/pageSubPack/visitor/visitor-add.vue
2026-07-28 17:48:06 +08:00

592 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-invite">
<!-- 标签切换车辆来访/人员来访 -->
<view class="tab-wrap">
<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>
<!-- 表单区域 -->
<view class="form-wrap">
<!-- 房屋信息 -->
<view class="section-title">房屋信息</view>
<view class="form-section">
<view class="form-item">
<view class="label">
<text class="required">*</text>来访地址
</view>
<view class="value" @click="openAddressModal">
{{ form.address }}
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 访客信息 -->
<view class="section-title">访客信息</view>
<view class="form-section">
<view class="form-item">
<view class="label">访客姓名</view>
<input class="input" placeholder="请输入访客姓名" v-model="form.name" />
</view>
<view class="form-item">
<view class="label">
<text class="required">*</text>访客性别
</view>
<view class="value" @click="openGenderModal">
{{ form.gender }}
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
<view class="form-item">
<view class="label">
<text class="required">*</text>访客联系方式
</view>
<input class="input" placeholder="请输入联系方式" v-model="form.phone" type="number" maxlength="11" />
</view>
<!-- 车牌号仅车辆来访显示 -->
<view class="form-item" v-if="activeTab === 'car'">
<view class="label">
<text class="required">*</text>车牌号
</view>
<view class="car-number-wrap">
<input class="car-input" v-for="(item, index) in 8" :key="index"
:value="form.carNumber[index]" @input="(e) => onCarInput(index, e)"
:focus="carFocusIndex === index"
@focus="carFocusIndex = index"
type="text" />
</view>
</view>
<view class="form-item">
<view class="label">
<text class="required">*</text>随行人数
</view>
<input class="input" placeholder="请输入随行人数" v-model="form.peopleNum" type="number" maxlength="2" />
</view>
</view>
<!-- 有效期 -->
<view class="section-title">有效期</view>
<view class="form-section">
<view class="form-item">
<view class="label">
<text class="required">*</text>来访时间
</view>
<view class="value">
<uni-datetime-picker type="datetime" v-model="form.visitTime" @change="handleTimeChange"
:border="false" />
</view>
</view>
<view class="form-item">
<view class="label">
<text class="required">*</text>离开时间
</view>
<view class="value">
<uni-datetime-picker type="datetime" v-model="form.leaveTime" @change="handleTimeChange"
:border="false" />
</view>
</view>
<view class="tips">预计来访时间和预计离开时间间隔不能超过24小时</view>
<view class="tips-2">车辆停留请勿超出有效期需及时驶出小区</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="btn-wrap">
<button class="submit-btn" @click="submitForm">
{{ activeTab === 'car' ? '确认邀请' : '生成通行二维码' }}
</button>
</view>
<!-- 地址选择弹窗 -->
<uni-popup ref="addressModalRef" type="bottom" :mask-click="false">
<view class="modal-wrap">
<view class="modal-title">选择来访地址</view>
<view class="address-list">
<view class="address-item" :class="{ active: item.label === form.address }" v-for="item in addressList"
:key="item.id" @click="selectAddress(item)">
{{ item.label }}
</view>
</view>
<uni-icons type="closeempty" size="20" class="modal-close" @click="closeAddressModal"></uni-icons>
</view>
</uni-popup>
<!-- 性别选择弹窗 -->
<uni-popup ref="genderModalRef" type="bottom" :mask-click="false">
<view class="modal-wrap">
<view class="gender-list">
<view class="gender-item" :class="{ active: item === form.gender }" v-for="item in genderList"
:key="item" @click="selectGender(item)">
{{ item }}
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import {
ref,
reactive
} from 'vue';
import {
onLoad
} from "@dcloudio/uni-app"
import moment from 'moment';
import{getHouseList,addVisitorData} from '../api/api.js'
// 标签激活状态
const activeTab = ref('car');
// 表单数据
const form = reactive({
address: '',
villageId: '',
houseId: '',
name: '',
gender: '',
phone: '',
carNumber:['', '', '', '', '', '', '', ''],
peopleNum: '',
visitTime: '',
leaveTime: ''
});
// 地址列表
const addressList = ref([]);
// 性别列表
const genderList = ref(['男', '女']);
// 弹窗实例
const addressModalRef = ref(null);
const genderModalRef = ref(null);
// 车牌号输入框聚焦索引
const carFocusIndex = ref(0);
// =========方法===========
// 获取房屋列表
const fetchHouseList = async () => {
try {
const res = await getHouseList({ pageNum: 1, pageSize: 10 })
if (res.code === 200) {
addressList.value = (res.rows || []).map(item => ({
...item,
label: `${item.buildingName || ''}${item.unitNo}-${item.floorNo || ''}-${item.houseNo || ''}`.trim()
}))
}
} catch (e) {
uni.showToast({
title:e.msg || '获取房屋列表失败',
icon:'none'
})
// console.error('获取房屋列表失败', e)
}
}
onLoad(() => {
fetchHouseList()
});
// 切换tab
const switchTab = (tab) => {
activeTab.value = tab;
resetForm();
};
// 打开地址弹窗
const openAddressModal = () => {
addressModalRef.value.open();
};
// 关闭地址弹窗
const closeAddressModal = () => {
addressModalRef.value.close();
};
// 选择地址
const selectAddress = (item) => {
form.address = item.label;
form.villageId = item.villageId || '';
form.houseId = item.houseId || '';
closeAddressModal();
};
// 打开性别弹窗
const openGenderModal = () => {
genderModalRef.value.open();
};
// 选择性别
const selectGender = (item) => {
form.gender = item;
genderModalRef.value.close();
};
// 车牌号单格输入
const onCarInput = (index, e) => {
const val = (e.detail && e.detail.value !== undefined) ? e.detail.value : (e.target ? e.target.value : '');
const upperVal = (val || '').toUpperCase().slice(0, 1);
form.carNumber[index] = upperVal;
// 输入一位后自动聚焦到下一个
if (upperVal && index < 7) {
carFocusIndex.value = index + 1;
}
};
// 时间校验
const handleTimeChange = () => {
if (!form.visitTime || !form.leaveTime) return true;
const visitMoment = moment(form.visitTime);
const leaveMoment = moment(form.leaveTime);
// 离开时间不能早于来访时间
if (leaveMoment.isBefore(visitMoment)) {
uni.showToast({
title: '离开时间不能早于来访时间',
icon: 'none'
});
form.leaveTime = visitMoment.clone().add(1, 'hours').format('YYYY-MM-DD HH:mm');
return false;
}
// 不能超过24小时
const diffHours = leaveMoment.diff(visitMoment, 'hours', true);
if (diffHours > 24) {
uni.showToast({
title: '时间间隔不能超过24小时',
icon: 'none'
});
form.leaveTime = visitMoment.clone().add(24, 'hours').format('YYYY-MM-DD HH:mm');
return false;
}
return true;
};
// 提交表单
const submitForm = async () => {
// 1. 必填项校验
if (!form.address) {
uni.showToast({
title: '请选择来访地址',
icon: 'none'
});
return;
}
if (!form.name) {
uni.showToast({
title: '请输入访客姓名',
icon: 'none'
});
return;
}
if (!form.gender) {
uni.showToast({
title: '请选择性别',
icon: 'none'
});
return;
}
if (!form.phone) {
uni.showToast({
title: '请输入访客联系方式',
icon: 'none'
});
return;
}
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
if (activeTab.value === 'car' && form.carNumber.filter(Boolean).join('').length < 7) {
uni.showToast({
title: '请填写完整车牌号',
icon: 'none'
});
return;
}
if (!form.peopleNum || form.peopleNum <= 0) {
uni.showToast({
title: '请输入正确的随行人数',
icon: 'none'
});
return;
}
if (!form.visitTime) {
uni.showToast({
title: '请选择来访时间',
icon: 'none'
});
return;
}
if (!form.leaveTime) {
uni.showToast({
title: '请选择离开时间',
icon: 'none'
});
return;
}
// 2. 时间规则校验
if (!handleTimeChange()) return;
// 3. 构造参数并提交
const villageId = uni.getStorageSync('currentVillageId')
const carNumStr = form.carNumber.filter(Boolean).join('')
const params = {
villageId: villageId,
houseId: form.houseId,
visitorName: form.name,
visitorGender: form.gender === '男' ? '0' : (form.gender === '女' ? '1' : ''),
visitorPhone: form.phone,
carNum: activeTab.value === 'car' ? carNumStr : '',
visitorNum: String(form.peopleNum),
startTime: form.visitTime,
endTime: form.leaveTime,
type: activeTab.value === 'car' ? '0' : '1',
status: '0'
}
uni.showLoading({ title: '提交中...' });
try {
const res = await addVisitorData(params)
uni.hideLoading();
if (res.code === 200) {
uni.showToast({
title: activeTab.value === 'car' ? '邀请成功' : '二维码生成成功',
icon: 'success'
});
setTimeout(() => {
uni.navigateTo({ url: `/pageSubPack/visitor/qrcode-page?qrcodeUrl=${encodeURIComponent(res.data.url)}&villageName=${res.data.villageName}` })
}, 1500);
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' });
}
} catch (e) {
uni.hideLoading();
uni.showToast({ title: e.msg || '网络异常,请重试', icon: 'none' });
}
};
// 重置表单
const resetForm = () => {
form.address = '';
form.villageId = '';
form.houseId = '';
form.name = '';
form.gender = '';
form.phone = '';
form.carNumber = ['', '', '', '', '', '', '', ''];
form.peopleNum = '';
form.visitTime = '';
form.leaveTime = '';
};
</script>
<style scoped>
.visitor-invite {
background-color: #f5f5f5;
min-height: 100vh;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #fff;
}
.nav-title {
font-size: 36rpx;
font-weight: 500;
margin-left: 20rpx;
}
/* 标签切换 */
.tab-wrap {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.tab-item {
/* flex: 1; */
display: flex;
padding: 30rpx;
text-align: center;
font-size: 32rpx;
color: #333;
}
.tab-item.active {
color: #007aff;
/* border-bottom: 4rpx solid #007aff; */
}
/* 表单区域 */
.form-wrap {
padding: 10rpx;
}
.form-section {
background-color: #fff;
margin-bottom: 10rpx;
padding: 20rpx 40rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 500;
/* margin-bottom: 20rpx; */
margin: 30rpx 40rpx 10rpx;
}
.form-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.form-item:last-child {
border-bottom: none;
}
.label {
width: 225rpx;
font-size: 32rpx;
color: #333;
}
.required {
color: red;
margin-right: 4rpx;
}
.value {
flex: 1;
font-size: 30rpx;
color: #666;
display: flex;
justify-content: flex-end;
align-items: center;
}
.input {
flex: 1;
font-size: 30rpx;
color: #666;
text-align: right;
}
/* 车牌号输入 */
.car-number-wrap {
flex: 1;
display: flex;
justify-content: space-between;
}
.car-input {
width: 52rpx;
height: 60rpx;
border: 1rpx solid #eee;
text-align: center;
font-size: 30rpx;
}
/* 提示文字 */
.tips {
font-size: 24rpx;
color: #999;
margin-top: 44rpx;
text-align: center;
}
.tips-2 {
font-size: 24rpx;
color: #999;
margin: 20rpx;
text-align: center;
}
/* 底部按钮 */
.btn-wrap {
padding: 20rpx 40rpx 50rpx;
}
.submit-btn {
background-color: #007aff;
color: #fff;
font-size: 28rpx;
height: 80rpx;
line-height: 80rpx;
border-radius: 8rpx;
}
/* 弹窗样式 */
.modal-wrap {
background-color: #fff;
padding: 30rpx;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
position: relative;
}
.modal-title {
font-size: 32rpx;
text-align: center;
margin-bottom: 30rpx;
}
.modal-close {
position: absolute;
top: 20rpx;
right: 20rpx;
color: #999;
}
.address-list,
.gender-list {
display: flex;
flex-direction: column;
}
.address-item,
.gender-item {
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
text-align: center;
font-size: 30rpx;
margin-bottom: 20rpx;
}
.address-item:last-child,
.gender-item:last-child {
margin-bottom: 0;
}
.address-item.active,
.gender-item.active {
background-color: #007aff;
color: #fff;
}
</style>