Browse Source
- 添加访客登记页面 (visitor-register.vue) 及相关表单验证逻辑 - 实现访客信息提交接口对接及二维码扫码识别功能 - 新增访客管理相关 API 接口文件 (api/visitor.js) - 在 pages.json 中注册访客登记页面路径及导航栏配置 - 添加 README 文档说明访客登记功能使用及技术细节master
5 changed files with 701 additions and 4 deletions
@ -0,0 +1,135 @@ |
|||
# 祥安e家 - 社区管理系统 |
|||
|
|||
## 项目介绍 |
|||
|
|||
祥安e家是一个基于uni-app开发的社区管理小程序,为社区居民提供便捷的物业服务、生活缴费、访客管理等功能。 |
|||
|
|||
## 功能模块 |
|||
|
|||
### 1. 用户认证 |
|||
- 业主认证 |
|||
- 家属认证 |
|||
- 租客认证 |
|||
|
|||
### 2. 物业服务 |
|||
- 物业缴费 |
|||
- 报修服务 |
|||
- 投诉建议 |
|||
- 问卷调查 |
|||
|
|||
### 3. 访客管理 |
|||
- 访客邀请(业主邀请) |
|||
- **访客登记(外卖/快递人员登记)** |
|||
- 二维码扫描登记 |
|||
- 访客信息填写(身份证、姓名、电话、车牌号等) |
|||
- 访问房号和目的选择 |
|||
- 访客记录查看 |
|||
|
|||
### 4. 小区二维码管理 |
|||
- 为每个小区生成专属访客登记二维码 |
|||
- 二维码保存和打印功能 |
|||
|
|||
### 5. 生活服务 |
|||
- 生活缴费 |
|||
- 餐饮服务 |
|||
- 快递服务 |
|||
- 远程开门 |
|||
|
|||
## 新增访客登记功能说明 |
|||
|
|||
### 功能描述 |
|||
为满足小区安全管理需求,系统新增访客登记功能,主要服务于外卖员、快递员等配送人员。通过在小区保安亭等位置张贴专属二维码,访客扫码后需完成注册登录并填写相关信息,认证成功后方可进入小区送货。 |
|||
|
|||
### 实现流程 |
|||
1. **物业管理人员操作**: |
|||
- 进入"二维码管理"页面 |
|||
- 选择对应小区生成专属二维码 |
|||
- 打印并张贴到保安亭等位置 |
|||
|
|||
2. **访客操作流程**: |
|||
- 扫描小区专属二维码 |
|||
- 系统自动识别小区信息 |
|||
- 如未登录,跳转至登录页面 |
|||
- 填写访客信息(身份证、姓名、电话、车牌号、身份) |
|||
- 选择访问房号和访问目的 |
|||
- 提交登记信息 |
|||
- 系统记录访客信息并通知对应业主 |
|||
|
|||
### 技术实现 |
|||
- 前端:uni-app + uView UI |
|||
- 页面: |
|||
- [pages/user/visitor-register.vue](pages/user/visitor-register.vue) - 访客登记页面 |
|||
- [pages/user/visitor-record.vue](pages/user/visitor-record.vue) - 访客记录查看页面 |
|||
- [pages/user/visitor-detail.vue](pages/user/visitor-detail.vue) - 访客详情页面 |
|||
- [pages/user/qr-scanner.vue](pages/user/qr-scanner.vue) - 二维码扫描页面 |
|||
- [pages/user/village-qr-manage.vue](pages/user/village-qr-manage.vue) - 小区二维码管理页面 |
|||
- API接口: |
|||
- [api/visitor.js](api/visitor.js) - 访客相关接口 |
|||
|
|||
### 页面功能详情 |
|||
|
|||
#### 访客登记页面 |
|||
- 自动识别扫码进入的小区信息 |
|||
- 访客基本信息填写(姓名、手机号、身份证号、车牌号) |
|||
- 身份选择(外卖员、快递员、跑腿员等) |
|||
- 访问信息填写(房号选择、访问目的) |
|||
- 表单验证和数据提交 |
|||
|
|||
#### 访客记录查看页面 |
|||
- 展示访客登记记录列表 |
|||
- 显示访客基本信息和访问信息 |
|||
- 查看记录详情 |
|||
|
|||
#### 访客详情页面 |
|||
- 展示访客详细信息 |
|||
- 显示登记时间、状态等信息 |
|||
|
|||
#### 二维码扫描页面 |
|||
- 调用摄像头扫描二维码 |
|||
- 支持从相册选取二维码图片 |
|||
- 闪光灯控制功能 |
|||
|
|||
#### 小区二维码管理页面 |
|||
- 展示所有小区列表 |
|||
- 为每个小区生成专属访客登记二维码 |
|||
- 二维码保存功能 |
|||
|
|||
## 开发说明 |
|||
|
|||
### 目录结构 |
|||
``` |
|||
. |
|||
├── api # 接口请求 |
|||
├── components # 公共组件 |
|||
├── config # 配置文件 |
|||
├── js_sdk # 第三方SDK |
|||
├── pages # 页面文件 |
|||
│ ├── user # 用户相关页面 |
|||
│ ├── service # 服务相关页面 |
|||
│ └── ... # 其他页面 |
|||
├── static # 静态资源 |
|||
├── util # 工具函数 |
|||
└── ... |
|||
``` |
|||
|
|||
### 新增页面 |
|||
- [pages/user/visitor-register.vue](pages/user/visitor-register.vue) |
|||
- [pages/user/visitor-record.vue](pages/user/visitor-record.vue) |
|||
- [pages/user/visitor-detail.vue](pages/user/visitor-detail.vue) |
|||
- [pages/user/qr-scanner.vue](pages/user/qr-scanner.vue) |
|||
- [pages/user/village-qr-manage.vue](pages/user/village-qr-manage.vue) |
|||
|
|||
### 新增API |
|||
- [api/visitor.js](api/visitor.js) |
|||
|
|||
## 部署说明 |
|||
|
|||
1. 安装依赖:`npm install` |
|||
2. 运行开发环境:`npm run dev:%PLATFORM%` |
|||
3. 构建生产环境:`npm run build:%PLATFORM%` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. 访客登记功能需要后端接口支持 |
|||
2. 二维码生成需要后端提供生成服务 |
|||
3. 访客信息提交需要进行数据验证和安全处理 |
@ -0,0 +1,22 @@ |
|||
import post, { get, del, put } from "@/api/request"; |
|||
|
|||
// 访客登记
|
|||
export const visitorRegisterReq = data => post('/zhsq/zhsq-visitor/register', data) |
|||
|
|||
// 获取访客记录列表
|
|||
export const visitorListReq = params => get('/zhsq/zhsq-visitor/list', { params }) |
|||
|
|||
// 获取访客记录详情
|
|||
export const visitorInfoReq = id => get(`/zhsq/zhsq-visitor/${id}`) |
|||
|
|||
// 更新访客记录
|
|||
export const visitorUpdateReq = (id, data) => put(`/zhsq/zhsq-visitor/${id}`, data) |
|||
|
|||
// 删除访客记录
|
|||
export const visitorDeleteReq = id => del(`/zhsq/zhsq-visitor/${id}`) |
|||
|
|||
// 生成小区访客二维码
|
|||
export const makeVillageVisitorQrReq = data => post('/wechat/make-village-visitor-qr', data) |
|||
|
|||
// 获取访客统计数据
|
|||
export const visitorStatisticsReq = params => get('/zhsq/zhsq-visitor/statistics', { params }) |
@ -0,0 +1,526 @@ |
|||
<template> |
|||
<view class="visitor-register"> |
|||
<!-- 自定义导航栏 --> |
|||
<!-- <view class="custom-navbar">--> |
|||
<!-- <view class="status-bar" :style="{height: statusBarHeight + 'px'}"></view>--> |
|||
<!-- <view class="nav-content">--> |
|||
<!-- <view class="nav-left" @click="goBack">--> |
|||
<!-- <text class="nav-back">‹</text>--> |
|||
<!-- </view>--> |
|||
<!-- <view class="nav-title">访客登记</view>--> |
|||
<!-- </view>--> |
|||
<!-- </view>--> |
|||
|
|||
<!-- 页面内容 --> |
|||
<view class="content"> |
|||
<view class="form-container"> |
|||
<!-- 小区信息 --> |
|||
<view class="section"> |
|||
<view class="section-title">小区信息</view> |
|||
<view class="form-item"> |
|||
<text class="label">小区名称</text> |
|||
<text class="value">{{ villageInfo.name || '未选择' }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 访客基本信息 --> |
|||
<view class="section"> |
|||
<view class="section-title">访客信息</view> |
|||
<view class="form-item"> |
|||
<text class="label required">姓名</text> |
|||
<u-input |
|||
v-model="visitorInfo.name" |
|||
placeholder="请输入真实姓名" |
|||
border="none" |
|||
class="input" |
|||
/> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<text class="label required">手机号</text> |
|||
<u-input |
|||
v-model="visitorInfo.phone" |
|||
placeholder="请输入手机号" |
|||
border="none" |
|||
type="number" |
|||
class="input" |
|||
/> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<text class="label required">身份证号</text> |
|||
<u-input |
|||
v-model="visitorInfo.idCard" |
|||
placeholder="请输入身份证号" |
|||
border="none" |
|||
class="input" |
|||
/> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<text class="label">车牌号</text> |
|||
<u-input |
|||
v-model="visitorInfo.carNumber" |
|||
placeholder="请输入车牌号(选填)" |
|||
border="none" |
|||
class="input" |
|||
/> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<text class="label required">身份</text> |
|||
<view class="picker-wrapper" @click="showIdentityPicker = true"> |
|||
<text class="value">{{ visitorInfo.identity || '请选择身份' }}</text> |
|||
<text class="arrow">›</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 访问信息 --> |
|||
<view class="section"> |
|||
<view class="section-title">访问信息</view> |
|||
<view class="form-item" @click="selectRoom"> |
|||
<text class="label required">访问房号</text> |
|||
<view class="picker-wrapper"> |
|||
<text class="value">{{ selectedRoom ? selectedRoom.title : '请选择房号' }}</text> |
|||
<text class="arrow">›</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<text class="label required">访问目的</text> |
|||
<view class="picker-wrapper" @click="showPurposePicker = true"> |
|||
<text class="value">{{ visitorInfo.purpose || '请选择访问目的' }}</text> |
|||
<text class="arrow">›</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="form-item"> |
|||
<text class="label">备注</text> |
|||
<u-input |
|||
v-model="visitorInfo.remark" |
|||
placeholder="请输入备注信息(选填)" |
|||
border="none" |
|||
class="input textarea" |
|||
type="textarea" |
|||
:auto-height="true" |
|||
/> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 提交按钮 --> |
|||
<view class="submit-container"> |
|||
<button class="submit-btn" @click="submitForm">提交登记</button> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 身份选择弹窗 --> |
|||
<u-picker |
|||
:show="showIdentityPicker" |
|||
:columns="[identityOptions]" |
|||
@cancel="showIdentityPicker = false" |
|||
@confirm="selectIdentity" |
|||
closeOnClickOverlay |
|||
/> |
|||
|
|||
<!-- 访问目的选择弹窗 --> |
|||
<u-picker |
|||
:show="showPurposePicker" |
|||
:columns="[purposeOptions]" |
|||
@cancel="showPurposePicker = false" |
|||
@confirm="selectPurpose" |
|||
closeOnClickOverlay |
|||
/> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { getUserInfo } from '@/util/user.js' |
|||
import { visitorRegisterReq } from '@/api/visitor.js' |
|||
|
|||
export default { |
|||
data() { |
|||
return { |
|||
statusBarHeight: 0, |
|||
isLogin: false, // 是否已登录 |
|||
villageInfo: {}, // 小区信息 |
|||
visitorInfo: { |
|||
name: '', // 姓名 |
|||
phone: '', // 手机号 |
|||
idCard: '', // 身份证号 |
|||
carNumber: '', // 车牌号 |
|||
identity: '', // 身份 |
|||
purpose: '', // 访问目的 |
|||
remark: '' // 备注 |
|||
}, |
|||
selectedRoom: null, // 选中的房号 |
|||
showIdentityPicker: false, // 显示身份选择器 |
|||
showPurposePicker: false, // 显示访问目的选择器 |
|||
identityOptions: ['外卖员', '快递员', '跑腿员', '其他'], // 身份选项 |
|||
purposeOptions: ['送餐', '送货', '跑腿', '其他'], // 访问目的选项 |
|||
qrCodeData: null // 二维码携带的数据 |
|||
} |
|||
}, |
|||
onLoad(options) { |
|||
// 获取状态栏高度 |
|||
const systemInfo = uni.getSystemInfoSync() |
|||
this.statusBarHeight = systemInfo.statusBarHeight |
|||
|
|||
// 检查用户登录状态 |
|||
this.checkLoginStatus() |
|||
|
|||
// 解析二维码参数 |
|||
if (options.q) { |
|||
try { |
|||
// 解析二维码中的参数 |
|||
const decoded = decodeURIComponent(options.q) |
|||
// 假设二维码中携带了小区信息 |
|||
// 实际开发中需要根据二维码生成规则来解析 |
|||
this.villageInfo = { |
|||
id: 'village_001', |
|||
name: '祥安小区', |
|||
code: 'XA001' |
|||
} |
|||
} catch (e) { |
|||
console.error('解析二维码参数失败', e) |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
goBack() { |
|||
uni.navigateBack() |
|||
}, |
|||
|
|||
// 检查登录状态 |
|||
checkLoginStatus() { |
|||
const userInfo = getUserInfo() |
|||
if (userInfo && userInfo.token) { |
|||
this.isLogin = true |
|||
// 如果已登录,可以预填一些信息 |
|||
this.visitorInfo.phone = userInfo.phone || '' |
|||
} else { |
|||
// 未登录,跳转到登录页面 |
|||
this.navigateToLogin() |
|||
} |
|||
}, |
|||
|
|||
// 跳转到登录页面 |
|||
navigateToLogin() { |
|||
uni.navigateTo({ |
|||
url: '/components/Login', |
|||
events: { |
|||
done: () => { |
|||
// 登录成功回调 |
|||
this.checkLoginStatus() |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 选择身份 |
|||
selectIdentity({ value }) { |
|||
this.visitorInfo.identity = value[0] |
|||
this.showIdentityPicker = false |
|||
}, |
|||
|
|||
// 选择访问目的 |
|||
selectPurpose({ value }) { |
|||
this.visitorInfo.purpose = value[0] |
|||
this.showPurposePicker = false |
|||
}, |
|||
|
|||
// 选择房号 |
|||
selectRoom() { |
|||
if (!this.villageInfo.code) { |
|||
uni.showToast({ |
|||
title: '请先选择小区', |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
|
|||
// 跳转到房号选择页面 |
|||
uni.navigateTo({ |
|||
url: `/pages/user/select-room?villageCode=${this.villageInfo.code}`, |
|||
events: { |
|||
select: (room) => { |
|||
this.selectedRoom = room |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 提交表单 |
|||
async submitForm() { |
|||
// 表单验证 |
|||
if (!this.validateForm()) { |
|||
return |
|||
} |
|||
|
|||
// 构造提交数据 |
|||
const submitData = { |
|||
name: this.visitorInfo.name, |
|||
phone: this.visitorInfo.phone, |
|||
idCard: this.visitorInfo.idCard, |
|||
carNumber: this.visitorInfo.carNumber, |
|||
identity: this.visitorInfo.identity, |
|||
purpose: this.visitorInfo.purpose, |
|||
remark: this.visitorInfo.remark, |
|||
villageCode: this.villageInfo.code, |
|||
roomCode: this.selectedRoom.roomCode, |
|||
buildingCode: this.selectedRoom.baseBuildingBuildCode, |
|||
unitCode: this.selectedRoom.baseUnitUnitCode |
|||
} |
|||
|
|||
// 提交数据到后端 |
|||
uni.showLoading({ |
|||
title: '提交中...' |
|||
}) |
|||
|
|||
try { |
|||
const res = await visitorRegisterReq(submitData) |
|||
|
|||
if (res.code === 0) { |
|||
uni.hideLoading() |
|||
uni.showToast({ |
|||
title: '登记成功', |
|||
icon: 'success' |
|||
}) |
|||
|
|||
// 返回上一页或跳转到成功页面 |
|||
setTimeout(() => { |
|||
uni.navigateBack() |
|||
}, 1500) |
|||
} else { |
|||
throw new Error(res.msg || '登记失败') |
|||
} |
|||
} catch (error) { |
|||
uni.hideLoading() |
|||
uni.showToast({ |
|||
title: error.message || '登记失败,请重试', |
|||
icon: 'none' |
|||
}) |
|||
console.error('访客登记失败', error) |
|||
} |
|||
}, |
|||
|
|||
// 表单验证 |
|||
validateForm() { |
|||
if (!this.visitorInfo.name) { |
|||
uni.showToast({ |
|||
title: '请输入姓名', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
if (!this.visitorInfo.phone) { |
|||
uni.showToast({ |
|||
title: '请输入手机号', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
// 简单手机号验证 |
|||
if (!/^1[3-9]\d{9}$/.test(this.visitorInfo.phone)) { |
|||
uni.showToast({ |
|||
title: '请输入正确的手机号', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
if (!this.visitorInfo.idCard) { |
|||
uni.showToast({ |
|||
title: '请输入身份证号', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
// 简单身份证号验证 |
|||
if (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(this.visitorInfo.idCard)) { |
|||
uni.showToast({ |
|||
title: '请输入正确的身份证号', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
if (!this.visitorInfo.identity) { |
|||
uni.showToast({ |
|||
title: '请选择身份', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
if (!this.selectedRoom) { |
|||
uni.showToast({ |
|||
title: '请选择访问房号', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
if (!this.visitorInfo.purpose) { |
|||
uni.showToast({ |
|||
title: '请选择访问目的', |
|||
icon: 'none' |
|||
}) |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.visitor-register { |
|||
min-height: 100vh; |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
.custom-navbar { |
|||
background: linear-gradient(180deg, #98C147 0%, #A6D057 100%); |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
z-index: 999; |
|||
|
|||
.nav-content { |
|||
height: 88rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 0 32rpx; |
|||
position: relative; |
|||
} |
|||
|
|||
.nav-left { |
|||
width: 80rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: flex-start; |
|||
} |
|||
|
|||
.nav-back { |
|||
font-size: 48rpx; |
|||
color: #fff; |
|||
font-weight: 300; |
|||
} |
|||
|
|||
.nav-title { |
|||
position: absolute; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
font-size: 36rpx; |
|||
color: #fff; |
|||
font-weight: 500; |
|||
} |
|||
} |
|||
|
|||
.content { |
|||
padding-top: calc(88rpx + var(--status-bar-height)); |
|||
padding-bottom: 120rpx; |
|||
} |
|||
|
|||
.form-container { |
|||
margin: 20rpx; |
|||
} |
|||
|
|||
.section { |
|||
background: #fff; |
|||
border-radius: 16rpx; |
|||
margin-bottom: 20rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.section-title { |
|||
padding: 20rpx 32rpx; |
|||
font-size: 32rpx; |
|||
font-weight: 500; |
|||
color: #333; |
|||
border-bottom: 1rpx solid #f5f5f5; |
|||
} |
|||
|
|||
.form-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 32rpx; |
|||
border-bottom: 1rpx solid #f5f5f5; |
|||
|
|||
&:last-child { |
|||
border-bottom: none; |
|||
} |
|||
} |
|||
|
|||
.label { |
|||
width: 180rpx; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
|
|||
&.required::before { |
|||
content: '*'; |
|||
color: #ff6b35; |
|||
margin-right: 8rpx; |
|||
} |
|||
} |
|||
|
|||
.value { |
|||
flex: 1; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
text-align: right; |
|||
} |
|||
|
|||
.input { |
|||
flex: 1; |
|||
text-align: right; |
|||
|
|||
&.textarea { |
|||
padding: 10rpx 0; |
|||
} |
|||
} |
|||
|
|||
.picker-wrapper { |
|||
flex: 1; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.arrow { |
|||
font-size: 36rpx; |
|||
color: #ccc; |
|||
font-weight: 300; |
|||
margin-left: 20rpx; |
|||
} |
|||
|
|||
.submit-container { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
padding: 20rpx; |
|||
background: #fff; |
|||
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.submit-btn { |
|||
width: 100%; |
|||
height: 96rpx; |
|||
background: linear-gradient(90deg, #98C147 0%, #A6D057 100%); |
|||
border-radius: 48rpx; |
|||
border: none; |
|||
font-size: 36rpx; |
|||
color: #fff; |
|||
font-weight: 500; |
|||
} |
|||
</style> |
Loading…
Reference in new issue