feat: 添加人员通行模块一键开门、密码开门、访客管理、我的访客、访客设置UI

This commit is contained in:
魏少阳 2025-01-17 17:49:55 +08:00
parent 0bcbca8d6a
commit 347181ed31
17 changed files with 1278 additions and 3611 deletions

View File

@ -122,8 +122,6 @@
"@uni-helper/vite-plugin-uni-pages": "0.2.20",
"@uni-helper/vite-plugin-uni-platform": "^0.0.4",
"@unocss/preset-legacy-compat": "^0.59.4",
"@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/runtime-core": "^3.5.13",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.20",

3394
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -206,6 +206,13 @@
},
"needLogin": false
},
{
"path": "pages/mine/mine",
"type": "page",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/notification/notification",
"type": "page"
@ -278,7 +285,39 @@
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personnel-passage/one-click-open-door/one-click-open-door",
"type": "page",
"layout": "default",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personnel-passage/password-open-door/password-open-door",
"type": "page",
"layout": "default",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personnel-passage/traffic-record/traffic-record",
"type": "page",
"layout": "default",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/personnel-passage/visitor-manage/visitor-manage-tab",
"type": "page",
"layout": "default",
"style": {
"navigationStyle": "custom"
}
}
],
"subPackages": []
}
}

View File

@ -134,28 +134,32 @@
{
id: 302,
icon: 'https://file.hikmall.com/prod/image/885eb6ac104e4a76941e67eb738142ec.png',
name: '一键开门'
name: '一键开门',
url: '/pages/personnel-passage/one-click-open-door/one-click-open-door'
},
{
id: 400,
icon: 'https://file.hikmall.com/prod/image/701d5a7031fa4b4290838c4562f99a0b.png',
name: '密码开门'
name: '密码开门',
url: '/pages/personnel-passage/password-open-door/password-open-door'
},
{
id: 299,
icon: 'https://file.hikmall.com/prod/image/771a717eb8d74f5bba71db68c9605a0f.png',
name: '通行记录',
url: '/pages/personnel-passage/traffic-correlation'
url: '/pages/personnel-passage/traffic-record/traffic-record'
},
{
id: 387,
icon: 'https://file.hikmall.com/prod/image/9752516f054342d3ac310a0fdb32b654.png',
name: '访客管理'
name: '访客管理',
url: '/pages/personnel-passage/visitor-manage/visitor-manage-tab'
},
{
id: 399,
icon: 'https://file.hikmall.com/prod/image/4dbe971d6fac465cb4fed956defd678c.png',
name: '我的访客'
name: '我的访客',
url: '/pages/personnel-passage/visitor-manage/visitor-manage-tab'
},
{
id: 388,

View File

@ -1,8 +1,7 @@
<route lang="json5" type="home">
<route lang="json5">
{
style: {
navigationStyle: 'custom',
navigationBarTitleText: '首页'
navigationStyle: 'custom'
}
}
</route>

View File

@ -111,31 +111,6 @@
</view>
</view>
<!-- 底部导航 -->
<view class="tab-bar">
<view class="tab-item active">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/635392a7f5e04e75bb657b6cc6e2abc8.png"
/>
<text>通行权限</text>
</view>
<view class="tab-item" @click="navigateTo('access-control')">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/d1d07b3125f841848c3c3eb94509b2ae.png"
/>
<text>门禁控制</text>
</view>
<view class="tab-item" @click="navigateTo('traffic-record')">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/885eb6ac104e4a76941e67eb738142ec.png"
/>
<text>通行记录</text>
</view>
</view>
<!-- 根据当前组件加载 -->
<component :is="currentComponent" />
</template>
@ -352,36 +327,4 @@
}
}
}
.tab-bar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: space-around;
padding: 8px 0;
background-color: #fff;
border-top: 1px solid #f5f5f5;
.tab-item {
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
.tab-icon {
width: 24px;
height: 24px;
}
text {
font-size: 12px;
}
&.active {
color: #2b5cff;
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<route lang="json5">
{
type: 'page',
layout: 'default',
style: {
navigationStyle: 'custom'
}
@ -42,60 +42,34 @@
</view>
<!-- 设备列表 -->
<view class="device-list">
<view class="device-item">
<text class="offline-tag">离线</text>
<view class="device-info">
<image
class="device-icon"
src="/static/images/icon_one_key_door_key.png"
mode="aspectFit"
/>
<text class="device-name">DS-K(FU6004429)</text>
</view>
<view class="unlock-btn">
<image class="unlock-icon" src="/static/images/icon_unlock.png" mode="aspectFit" />
</view>
<view class="device-list"></view>
<view class="device-item">
<text class="offline-tag">离线</text>
<view class="device-info">
<image
class="device-icon"
src="/static/images/icon_one_key_door_key.png"
mode="aspectFit"
/>
<text class="device-name">DS-K(FU6004429)</text>
</view>
<view class="device-item">
<text class="offline-tag">离线</text>
<view class="device-info">
<image
class="device-icon"
src="/static/images/icon_one_key_door_key.png"
mode="aspectFit"
/>
<text class="device-name">DS-K(L40959329)</text>
</view>
<view class="unlock-btn">
<image class="unlock-icon" src="/static/images/icon_unlock.png" mode="aspectFit" />
</view>
<view class="unlock-btn">
<image class="unlock-icon" src="/static/images/icon_unlock.png" mode="aspectFit" />
</view>
</view>
<!-- 底部导航 -->
<view class="tab-bar">
<view class="tab-item">
<view class="device-item">
<text class="offline-tag">离线</text>
<view class="device-info">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/635392a7f5e04e75bb657b6cc6e2abc8.png"
class="device-icon"
src="/static/images/icon_one_key_door_key.png"
mode="aspectFit"
/>
<text>通行权限</text>
<text class="device-name">DS-K(L40959329)</text>
</view>
<view class="tab-item active">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/d1d07b3125f841848c3c3eb94509b2ae.png"
/>
<text>门禁控制</text>
</view>
<view class="tab-item">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/885eb6ac104e4a76941e67eb738142ec.png"
/>
<text>通行记录</text>
<view class="unlock-btn">
<image class="unlock-icon" src="/static/images/icon_unlock.png" mode="aspectFit" />
</view>
</view>
</view>
@ -320,36 +294,4 @@
}
}
}
.tab-bar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: space-around;
padding: 8px 0;
background-color: #fff;
border-top: 1px solid #f5f5f5;
.tab-item {
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
.tab-icon {
width: 24px;
height: 24px;
}
text {
font-size: 12px;
}
&.active {
color: #2b5cff;
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<route lang="json5">
{
layout: 'default',
style: {
navigationStyle: 'custom'
}
}
</route>
<template>
<TopNavigation title="一键开门" />
<view class="container">
<view class="search-box">
<image class="search-icon" src="/static/images/icon_search.png" mode="aspectFit" />
<input type="text" placeholder="输入门禁点名称搜索" placeholder-class="placeholder" />
</view>
<view class="device-list">
<view class="device-item">
<view class="device-left">
<text class="device-name">DS-K(FU6004429)</text>
<text class="favorite"> 收藏</text>
</view>
<view class="device-actions">
<view class="action-btn">
<image class="action-icon" src="/static/images/icon_table_menu.png" mode="aspectFit" />
</view>
<view class="action-btn">
<image class="action-icon" src="/static/images/icon_table_menu.png" mode="aspectFit" />
</view>
</view>
</view>
<view class="device-item">
<view class="device-left">
<text class="device-name">DS-K(L40959329)</text>
<text class="favorite"> 收藏</text>
</view>
<view class="device-actions">
<view class="action-btn">
<image class="action-icon" src="/static/images/icon_table_menu.png" mode="aspectFit" />
</view>
<view class="action-btn">
<image class="action-icon" src="/static/images/icon_table_menu.png" mode="aspectFit" />
</view>
</view>
</view>
</view>
<view class="bottom-tip">
<text>设备离线无法开门可使用蓝牙开门</text>
</view>
</view>
</template>
<script setup>
import TopNavigation from '@/components/TopNavigation/TopNavigation.vue'
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
padding: 0;
background-color: #f5f6fa;
}
.search-box {
display: flex;
align-items: center;
padding: 8px 12px;
margin: 16px;
background-color: #fff;
border-radius: 8px;
.search-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
input {
flex: 1;
font-size: 14px;
}
.placeholder {
color: #999;
}
}
.device-list {
padding: 0 12px;
.device-item {
display: flex;
justify-content: space-between;
padding: 16px 20px;
margin: 0 4px 12px;
background-color: #fff;
border-radius: 8px;
.device-left {
display: flex;
flex-direction: column;
gap: 4px;
.device-name {
font-size: 16px;
color: #333;
}
.favorite {
font-size: 14px;
color: #999;
}
}
.device-actions {
display: flex;
gap: 12px;
margin-right: -4px;
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background-color: #f5f5f5;
border-radius: 50%;
.action-icon {
width: 20px;
height: 20px;
}
}
}
}
}
.bottom-tip {
padding: 16px;
font-size: 12px;
color: #999;
text-align: center;
}
</style>

View File

@ -0,0 +1,146 @@
<route lang="json5">
{
layout: 'default',
style: {
navigationStyle: 'custom'
}
}
</route>
<template>
<TopNavigation title="密码开门" />
<view class="container">
<view class="password-list">
<view class="password-card">
<view class="help-icon">
<text>通行帮助</text>
<image src="/static/images/personnelPassage/icon_tip.png" mode="aspectFit" />
</view>
<view class="password-display">
<text class="password">742523</text>
<image class="eye-icon" src="/static/images/icon_eye.png" mode="aspectFit" />
</view>
<view class="password-tip">*请妥善保管密码勿泄露给他人</view>
<view class="door-range">
<text>门禁范围</text>
<image src="/static/images/icon_white_right.png" mode="aspectFit" />
</view>
</view>
</view>
<view class="bottom-button">
<button class="refresh-btn" @click="refreshPassword">重新生成密码</button>
</view>
</view>
</template>
<script setup>
import TopNavigation from '@/components/TopNavigation/TopNavigation.vue'
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f5f6fa;
}
.password-list {
padding: 16px;
}
.password-card {
position: relative;
margin-right: 0;
color: #fff;
background: linear-gradient(135deg, #4080ff 0%, #2b5cff 100%);
border-radius: 12px;
.help-icon {
display: flex;
gap: 4px;
align-items: center;
justify-content: flex-end;
padding: 15px 15px 15px 0;
font-size: 14px;
text {
font-size: 12px;
opacity: 0.8;
}
image {
width: 16px;
height: 16px;
}
}
.password-display {
display: flex;
align-items: center;
justify-content: center;
margin-top: 25px;
margin-bottom: 5px;
.password {
font-size: 40px;
font-weight: bold;
letter-spacing: 4px;
}
.eye-icon {
width: 24px;
height: 24px;
}
}
.password-tip {
margin-bottom: 32px;
font-size: 12px;
text-align: center;
opacity: 0.8;
}
.door-range {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
font-size: 14px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
text {
font-size: 12px;
}
image {
width: 16px;
height: 16px;
}
}
}
.bottom-button {
position: fixed;
right: 0;
bottom: 0;
left: 0;
padding: 16px;
.refresh-btn {
width: 100%;
height: 44px;
font-size: 16px;
line-height: 44px;
color: #fff;
background-color: #2b5cff;
border: none;
border-radius: 8px;
}
}
</style>

View File

@ -17,9 +17,6 @@
<view v-else-if="curIndex == 1">
<AccessControl />
</view>
<view v-else-if="curIndex == 2">
<TrafficRecord />
</view>
</scroll-view>
</view>
<CustomTabBar :list="list" :default-index="0" @change="change"></CustomTabBar>
@ -29,10 +26,9 @@
// import { onLoad } from '@dcloudio/uni-app'
import AccessAuthority from './AccessAuthority/AccessAuthority.vue'
import AccessControl from './AccessControl/AccessControl.vue'
import TrafficRecord from './TrafficRecord/TrafficRecord.vue'
import { TabBarItem } from '@/typings'
const pages = [AccessAuthority, AccessControl, TrafficRecord]
const titleTab = ['通行权限', '门禁控制', '通行记录']
// const pages = [AccessAuthority, AccessControl]
const titleTab = ['通行权限', '门禁控制']
const list = ref<Array<TabBarItem>>([
{
title: '通行权限',
@ -41,10 +37,6 @@
{
title: '门禁控制',
icon: 'notification'
},
{
title: '通行记录',
icon: 'setting'
}
])

View File

@ -1,6 +1,6 @@
<route lang="json5">
{
type: 'page',
layout: 'default',
style: {
navigationStyle: 'custom'
}
@ -8,6 +8,7 @@
</route>
<template>
<TopNavigation title="通行记录"></TopNavigation>
<view class="record-container">
<!-- 快捷功能区 -->
<view class="quick-actions">
@ -80,32 +81,6 @@
</view>
</view>
</view>
<!-- 底部导航 -->
<view class="tab-bar">
<view class="tab-item">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/635392a7f5e04e75bb657b6cc6e2abc8.png"
/>
<text>通行权限</text>
</view>
<view class="tab-item">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/d1d07b3125f841848c3c3eb94509b2ae.png"
/>
<text>门禁控制</text>
</view>
<view class="tab-item active">
<image
class="tab-icon"
src="https://file.hikmall.com/prod/image/885eb6ac104e4a76941e67eb738142ec.png"
/>
<text>通行记录</text>
</view>
</view>
<!-- </view>-->
</view>
</template>
@ -318,36 +293,4 @@
}
}
}
.tab-bar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: space-around;
padding: 8px 0;
background-color: #fff;
border-top: 1px solid #f5f5f5;
.tab-item {
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
.tab-icon {
width: 24px;
height: 24px;
}
text {
font-size: 12px;
}
&.active {
color: #2b5cff;
}
}
}
</style>

View File

@ -0,0 +1,239 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationStyle: 'custom'
}
}
</route>
<template>
<view class="container">
<!-- 功能区域 -->
<view class="function-area">
<view class="function-item">
<image
class="icon"
src="https://file.hikmall.com/prod/image/635392a7f5e04e75bb657b6cc6e2abc8.png"
mode="aspectFit"
/>
<view class="text-wrapper">
<text class="title">访客码</text>
<text class="desc">分享刷码通行</text>
</view>
</view>
<view class="function-item">
<image
class="icon"
src="https://file.hikmall.com/prod/image/d1d07b3125f841848c3c3eb94509b2ae.png"
mode="aspectFit"
/>
<view class="text-wrapper">
<text class="title">新建邀约</text>
<text class="desc">短信邀约访客</text>
</view>
</view>
</view>
<!-- 访客列表筛选 -->
<view class="visitor-filter">
<text
v-for="(filter, index) in filters"
:key="index"
:class="['filter-item', { active: currentFilter === index }]"
@click="switchFilter(index)"
>
{{ filter }}
</text>
</view>
<!-- 记录列表 -->
<view class="record-list">
<view v-for="(record, index) in currentRecords" :key="index" class="record-item">
<view class="record-number-wrapper">
<text class="record-number">{{ record.number }}</text>
<text class="visit-time">预约到访时间{{ record.time }}</text>
</view>
<text class="status">{{ record.status }}</text>
</view>
</view>
<text class="data-tip">仅展示近6个月数据</text>
</view>
</template>
<script setup>
import TopNavigation from '@/components/TopNavigation/TopNavigation.vue'
import { ref, computed } from 'vue'
const filters = ref(['全部', '已到访', '待来访', '更多筛选'])
const currentFilter = ref(0)
// 访
const records = ref([
{
number: '3',
time: '今天 10:57',
status: '已失效',
type: 'all'
},
{
number: '12',
time: '昨天 18:07',
status: '已失效',
type: 'error'
},
{
number: '123',
time: '2024/12/17 10:17',
status: '已失效',
type: 'all'
}
])
//
const currentRecords = computed(() => {
switch (currentFilter.value) {
case 0: //
return records.value
case 1: // 访
return records.value.filter(record => record.type === 'visited')
case 2: // 访
return records.value.filter(record => record.type === 'pending')
case 3: //
return records.value
default:
return records.value
}
})
const switchFilter = index => {
currentFilter.value = index
}
</script>
<style scoped lang="scss">
.container {
min-height: 100vh;
// padding: 0 16px;
background-color: #f5f6fa;
}
.function-area {
display: flex;
gap: 12px;
padding: 16px;
margin: 12px 0;
background: #fff;
border-radius: 12px;
.function-item {
display: flex;
flex: 1;
gap: 12px;
align-items: center;
margin-right: 16px;
margin-left: 16px;
// padding: 8px 12px;
// background: #f8f9fc;
// border-radius: 8px;
.icon {
flex-shrink: 0;
width: 32px;
height: 32px;
}
.text-wrapper {
display: flex;
flex-direction: column;
gap: 2px;
.title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.desc {
font-size: 12px;
color: #999;
}
}
}
}
.visitor-filter {
display: flex;
gap: 16px;
padding: 12px 16px;
margin-right: 16px;
margin-left: 16px;
background: #fff;
border-radius: 12px 12px 0 0;
.filter-item {
padding-bottom: 4px;
font-size: 14px;
color: #666;
&.active {
color: #2b5cff;
border-bottom: 2px solid #2b5cff;
}
}
}
.record-list {
margin-right: 16px;
margin-left: 16px;
background: #fff;
border-radius: 0 0 12px 12px;
.record-item {
position: relative;
display: flex;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
.record-number-wrapper {
display: flex;
flex-direction: column;
gap: 4px;
.record-number {
font-size: 24px;
font-weight: 500;
line-height: 1;
color: #333;
}
.visit-time {
font-size: 12px;
color: #999;
white-space: nowrap;
}
}
.status {
position: absolute;
top: 16px;
right: 16px;
padding: 2px 8px;
font-size: 12px;
color: #999;
background: #f5f5f5;
border-radius: 10px;
}
}
}
.data-tip {
display: block;
padding-bottom: 16px;
margin-top: 16px;
font-size: 12px;
color: #999;
text-align: center;
}
</style>

View File

@ -0,0 +1,348 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationStyle: 'custom'
}
}
</route>
<template>
<view class="container">
<!-- 顶部标签页 -->
<view class="tab-header">
<text
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: currentTab === index }]"
@click="switchTab(index)"
>
{{ tab.name }}
</text>
</view>
<!-- 主要内容区域 -->
<view class="content">
<image class="illustration" :src="tabs[currentTab].image" mode="aspectFit" />
<text class="tip-text">{{ tabs[currentTab].tipText }}</text>
<button class="invite-btn">
<image src="/static/images/icon_arrow_right_white.png" mode="aspectFit" />
{{ tabs[currentTab].buttonText }}
</button>
</view>
<!-- 访客记录 -->
<view class="visitor-record">
<view class="record-header">
<view class="header-left">
<image src="/static/images/icon_record.png" mode="aspectFit" />
<text>访客记录</text>
</view>
<view class="header-right">
<text>一键下发</text>
<image src="/static/images/icon_send.png" mode="aspectFit" />
</view>
</view>
<!-- 记录筛选 -->
<view class="record-filter">
<text
v-for="(filter, index) in filters"
:key="index"
:class="['filter-item', { active: currentFilter === index }]"
@click="switchFilter(index)"
>
{{ filter.name }}
</text>
</view>
<!-- 记录列表 -->
<view class="record-list">
<view v-for="(record, index) in currentRecords" :key="index" class="record-item">
<view class="record-number-wrapper">
<text class="record-number">{{ record.number }}</text>
<text class="visit-time">预约到访时间{{ record.time }}</text>
</view>
<text class="status">{{ record.status }}</text>
</view>
</view>
<text class="data-tip">仅展示近6个月数据</text>
</view>
</view>
</template>
<script setup>
import TopNavigation from '@/components/TopNavigation/TopNavigation.vue'
import { ref, computed } from 'vue'
const currentTab = ref(0)
const tabs = ref([
{
name: '访客自助预约',
image: '/static/images/bg_no_device.webp',
tipText: '邀请访客自助填写预约信息',
buttonText: '去邀请'
},
{
name: '我要邀约访客',
image: '/static/images/bg_no_device.webp',
tipText: '填写访客信息,短信邀请访客来访',
buttonText: '去填写'
},
{
name: '访客签到',
image: '/static/images/bg_no_device.webp',
tipText: '扫描访客二维码,人工核验签到',
buttonText: '扫码核验'
}
])
const switchTab = index => {
currentTab.value = index
}
//
const filters = ref([
{ name: '全部', type: 'all' },
{ name: '下发异常', type: 'error' },
{ name: '更多筛选', type: 'more' }
])
const currentFilter = ref(0)
// 访
const records = ref([
{
number: '3',
time: '今天 10:57',
status: '已失效',
type: 'all'
},
{
number: '12',
time: '昨天 18:07',
status: '已失效',
type: 'error'
},
{
number: '123',
time: '2024/12/17 10:17',
status: '已失效',
type: 'all'
}
])
//
const currentRecords = computed(() => {
const filterType = filters.value[currentFilter.value].type
if (filterType === 'all') {
return records.value
}
return records.value.filter(record => record.type === filterType)
})
//
const switchFilter = index => {
currentFilter.value = index
}
</script>
<style scoped lang="scss">
.container {
min-height: 100vh;
// padding: 0 16px;
background-color: #f5f6fa;
}
.tab-header {
display: flex;
padding: 12px 0px;
margin-right: 16px;
margin-left: 16px;
background: #fff;
border-radius: 12px 12px 0 0;
.tab-item {
flex: 1;
padding: 8px 0;
font-size: 14px;
color: #666;
text-align: center;
&.active {
position: relative;
font-weight: 500;
color: #333;
&::after {
position: absolute;
bottom: -4px;
left: 50%;
width: 20px;
height: 2px;
content: '';
background-color: #2b5cff;
border-radius: 1px;
transform: translateX(-50%);
}
}
}
}
.content {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
margin-right: 16px;
margin-left: 16px;
background: #fff;
border-radius: 0 0 12px 12px;
.illustration {
width: 240px;
height: 160px;
margin-bottom: 16px;
// background-color: red;
}
.tip-text {
margin-bottom: 24px;
font-size: 14px;
color: #666;
}
.invite-btn {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
width: calc(100% - 32px);
font-size: 16px;
color: #fff;
background: linear-gradient(90deg, #4080ff, #2b5cff);
border: none;
border-radius: 24px;
image {
width: 16px;
height: 16px;
}
}
}
.visitor-record {
// padding: 0 0 16px;
margin: 12px 16px 16px 16px;
background: #fff;
border-radius: 12px;
.record-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
margin-top: 12px;
margin-bottom: 16px;
background-color: #efeefe;
border-radius: 12px 12px 0 0;
.header-left {
display: flex;
gap: 4px;
align-items: center;
image {
width: 20px;
height: 20px;
}
text {
font-size: 16px;
font-weight: 500;
}
}
.header-right {
display: flex;
gap: 4px;
align-items: center;
font-size: 14px;
color: #666;
image {
width: 16px;
height: 16px;
}
}
}
.record-filter {
display: flex;
gap: 16px;
padding: 0 16px;
margin-bottom: 16px;
.filter-item {
padding-bottom: 4px;
font-size: 14px;
color: #666;
&.active {
color: #2b5cff;
border-bottom: 2px solid #2b5cff;
}
}
}
.record-list {
.record-item {
position: relative;
display: flex;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
.record-number-wrapper {
display: flex;
flex-direction: column;
gap: 4px;
.record-number {
font-size: 24px;
font-weight: 500;
line-height: 1;
color: #333;
}
.visit-time {
font-size: 12px;
color: #999;
white-space: nowrap;
}
}
.status {
position: absolute;
top: 16px;
right: 16px;
padding: 2px 8px;
font-size: 12px;
color: #999;
background: #f5f5f5;
border-radius: 10px;
}
}
}
.data-tip {
display: block;
padding-bottom: 16px;
margin-top: 16px;
font-size: 12px;
color: #999;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,246 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationStyle: 'custom'
}
}
</route>
<template>
<view class="container">
<!-- 顶部标签页 -->
<view class="tab-header">
<text
v-for="(tab, index) in tabs"
:key="index"
:class="['tab-item', { active: currentTab === index }]"
@click="switchTab(index)"
>
{{ tab }}
</text>
</view>
<!-- 设置列表 -->
<view class="settings-list">
<!-- 访客自助预约设置 -->
<view class="setting-group">
<view class="setting-item">
<text class="label">访客自助预约</text>
<switch
:checked="settings.selfBooking"
@change="e => updateSetting('selfBooking', e.detail.value)"
color="#2b5cff"
/>
</view>
<view class="setting-item with-arrow">
<text class="label">预约设置</text>
<image class="arrow" src="/static/images/icon_arrow_right.png" mode="aspectFit" />
</view>
<view class="setting-item with-arrow">
<text class="label">被访人</text>
<view class="value">
<text>全部成员</text>
<image class="arrow" src="/static/images/icon_arrow_right.png" mode="aspectFit" />
</view>
</view>
<text class="tip-text">开启后您可提供预约码给访客访客微信扫码自助预约</text>
</view>
<!-- 其他设置项 -->
<view class="setting-group">
<view class="setting-item">
<text class="label">成员邀约访客</text>
<switch
:checked="settings.memberInvite"
@change="e => updateSetting('memberInvite', e.detail.value)"
color="#2b5cff"
/>
</view>
<view class="setting-item">
<text class="label">访客补录信息</text>
<switch
:checked="settings.infoSupply"
@change="e => updateSetting('infoSupply', e.detail.value)"
color="#2b5cff"
/>
</view>
<view class="setting-item with-arrow">
<text class="label">访问时长</text>
<view class="value">
<text>最大30天/默认2小时</text>
<image class="arrow" src="/static/images/icon_arrow_right.png" mode="aspectFit" />
</view>
</view>
<text class="bottom-tip">
开启后允许成员预约访客生效后系统将发送短信 自定义表单模板登录PC端
<text class="link">hikiot.com</text>
<text class="copy-icon">复制</text>
</text>
</view>
<view class="setting-group">
<view class="setting-item">
<text class="label">访客码</text>
<view class="label-with-icon">
<image class="info-icon" src="/static/images/icon_info.png" mode="aspectFit" />
<switch
:checked="settings.visitorCode"
@change="e => updateSetting('visitorCode', e.detail.value)"
color="#2b5cff"
/>
</view>
</view>
<text class="tip-text">开启后允许团队成员快速生成访客码提供访客通行</text>
</view>
</view>
</view>
</template>
<script setup>
import TopNavigation from '@/components/TopNavigation/TopNavigation.vue'
import { ref } from 'vue'
const tabs = ref(['预约方式', '到访通行', '审批设置'])
const currentTab = ref(0)
const settings = ref({
selfBooking: false,
memberInvite: false,
infoSupply: false,
visitorCode: false
})
const switchTab = index => {
currentTab.value = index
}
const updateSetting = (key, value) => {
settings.value[key] = value
}
</script>
<style scoped lang="scss">
.container {
min-height: 100vh;
background-color: #f5f6fa;
}
.tab-header {
display: flex;
padding: 12px 0;
margin: 12px 16px;
background: #fff;
border-radius: 12px;
.tab-item {
flex: 1;
padding: 8px 0;
font-size: 14px;
color: #666;
text-align: center;
&.active {
position: relative;
font-weight: 500;
color: #333;
&::after {
position: absolute;
bottom: -4px;
left: 50%;
width: 20px;
height: 2px;
content: '';
background-color: #2b5cff;
border-radius: 1px;
transform: translateX(-50%);
}
}
}
}
.settings-list {
margin: 0 16px;
.setting-group {
padding: 0 16px 12px 12px;
margin-bottom: 12px;
background: #fff;
border-radius: 12px;
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 16px;
color: #333;
}
.value {
display: flex;
gap: 4px;
align-items: center;
font-size: 14px;
color: #999;
}
.label-with-icon {
display: flex;
gap: 8px;
align-items: center;
.info-icon {
width: 16px;
height: 16px;
}
}
.arrow {
width: 16px;
height: 16px;
}
&.with-arrow {
cursor: pointer;
}
}
.bottom-tip {
display: block;
padding: 12px 0;
font-size: 12px;
color: #999;
text-align: center;
.link {
color: #2b5cff;
text-decoration: underline;
}
.copy-icon {
display: inline-block;
padding: 2px 4px;
margin-left: 4px;
color: #2b5cff;
background: rgba(43, 92, 255, 0.1);
border-radius: 4px;
}
}
.tip-text {
padding: 12px 0;
font-size: 12px;
color: #999;
}
}
}
</style>

View File

@ -0,0 +1,65 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationStyle: 'custom'
}
}
</route>
<template>
<view class="h-[calc(100vh-50px)] flex flex-col">
<TopNavigation :title="titleTab[curIndex]"></TopNavigation>
<scroll-view class="flex-1 box-border" scroll-y>
<view v-if="curIndex == 0">
<VisitorManage />
</view>
<view v-else-if="curIndex == 1">
<MyVisitor />
</view>
<view v-else-if="curIndex == 2">
<VisitorSet />
</view>
</scroll-view>
</view>
<CustomTabBar :list="list" :default-index="0" @change="change"></CustomTabBar>
</template>
<script lang="ts" setup>
// import { onLoad } from '@dcloudio/uni-app'
import VisitorManage from './VisitorManage/VisitorManage.vue'
import MyVisitor from './MyVisitor/MyVisitor.vue'
import VisitorSet from './VisitorSet/VisitorSet.vue'
import { TabBarItem } from '@/typings'
const titleTab = ['访客管理', '我的访客', '访客设置']
const list = ref<Array<TabBarItem>>([
{
title: '首页',
icon: 'home'
},
{
title: '我的访客',
icon: 'notification'
},
{
title: '访客设置',
icon: 'setting'
}
])
const curIndex = ref(0)
// onLoad(options => {
// console.log(`11111${Number(options.curIndex)}`)
// curIndex.value = Number(options.curIndex)
// })
const change = (data: { value: number }) => {
curIndex.value = data.value
}
</script>
<style lang="scss" scoped>
page {
background-color: #f6f8fc;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -4,8 +4,7 @@
// Generated by vite-plugin-uni-pages
interface NavigateToOptions {
url: "/pages/mine/mine" |
"/pages/home/home" |
url: "/pages/home/home" |
"/pages/application-list/application-list" |
"/pages/approval/approval" |
"/pages/attendance/allowed-time" |
@ -25,6 +24,7 @@ interface NavigateToOptions {
"/pages/info-publish/notice-details" |
"/pages/info-publish/notice-manage" |
"/pages/login/login" |
"/pages/mine/mine" |
"/pages/notification/notification" |
"/pages/personnel-passage/traffic-correlation" |
"/pages/reset-password/reset-password" |
@ -34,7 +34,11 @@ interface NavigateToOptions {
"/pages/attendance/attendance-add-group/attendance-staff" |
"/pages/attendance/attendance-add-group/attendance-time" |
"/pages/attendance/attendance-add-group/outside-rules" |
"/pages/attendance/attendance-add-group/special-date-set";
"/pages/attendance/attendance-add-group/special-date-set" |
"/pages/personnel-passage/one-click-open-door/one-click-open-door" |
"/pages/personnel-passage/password-open-door/password-open-door" |
"/pages/personnel-passage/traffic-record/traffic-record" |
"/pages/personnel-passage/visitor-manage/visitor-manage-tab";
}
interface RedirectToOptions extends NavigateToOptions {}