Преглед изворни кода

门禁以及会议代码场景功能优化

fanghuisheng пре 1 недеља
родитељ
комит
1b04259e86
41 измењених фајлова са 2976 додато и 1021 уклоњено
  1. BIN
      nativeplugins/FaceAISDKModule/android/faceAISDK-release.aar
  2. 9 0
      nativeplugins/FaceAISDKModule/changelog.md
  3. 56 0
      nativeplugins/FaceAISDKModule/package.json
  4. BIN
      nativeplugins/Sys-Plugin/android/sysPlugin-release.aar
  5. 50 0
      nativeplugins/Sys-Plugin/package.json
  6. 1 0
      package.json
  7. 4 4
      src/App.vue
  8. 9 0
      src/api/business/door.js
  9. 7 0
      src/api/business/face.js
  10. 2 2
      src/api/business/meeting.js
  11. 181 124
      src/components/oa-passBody/index.vue
  12. 6 0
      src/config.js
  13. 44 8
      src/pages.json
  14. 191 0
      src/pages/door/components/face-verify-popup.nvue
  15. 1116 0
      src/pages/door/faceStorage/index.vue
  16. 519 0
      src/pages/door/index.js
  17. 325 0
      src/pages/door/index.nvue
  18. 1 0
      src/pages/door/index4.vue
  19. 2 1
      src/pages/door/setting/index.scss
  20. 9 22
      src/pages/door/setting/index.vue
  21. 0 96
      src/pages/door/setting/other/index.vue
  22. 2 41
      src/pages/door/setting/public/index.vue
  23. 0 71
      src/pages/door/setting/serve/index.vue
  24. 54 10
      src/pages/door/setting/system/index.vue
  25. 76 0
      src/pages/door/setting/system/network.vue
  26. 17 7
      src/pages/door/setting/system/senior.vue
  27. 2 4
      src/pages/meeting/index.vue
  28. 12 2
      src/pages/meeting/setting/index.vue
  29. 1 55
      src/plugins/device/ph.plugins.js
  30. 106 18
      src/plugins/device/sys.plugins.js
  31. 0 9
      src/plugins/device/yx.plugins.js
  32. 28 10
      src/static/face/door.html
  33. 0 360
      src/static/face/door1.html
  34. 7 3
      src/static/face/js/meeting-page.js
  35. 11 2
      src/static/face/meeting.html
  36. 11 5
      src/static/face/meeting_template_preview.html
  37. BIN
      src/static/iconfont/iconfont.ttf
  38. BIN
      src/static/iconfont/iconfont.woff
  39. BIN
      src/static/iconfont/iconfont.woff2
  40. 80 167
      src/store/modules/control.js
  41. 37 0
      src/store/modules/setting.js

BIN
nativeplugins/FaceAISDKModule/android/faceAISDK-release.aar


+ 9 - 0
nativeplugins/FaceAISDKModule/changelog.md

@@ -0,0 +1,9 @@
+## 2025.09.22
+ - 1.完善插件各种参数传递
+ - 2.添加人脸可选择精确模式或快速模式
+ - 3.完善不同场景使用,解决bug
+ - 4.允许用户自行管理拓展摄像头
+
+## 2025.01.06
+ - 1.添加人脸识别活体检测Android 插件原生支持
+ 

+ 56 - 0
nativeplugins/FaceAISDKModule/package.json

@@ -0,0 +1,56 @@
+{  
+    "name": "FaceAISDKModule",  
+    "id": "FaceAISDKModule",  
+    "version": "3.4.0",
+    "description": "FaceAI SDK是设备端可离线不联网Android 人脸识别、活体检测以及人脸搜索SDK,可快速集成实现人脸识别,人脸搜索功能",
+    "_dp_type":"nativeplugin",  
+    "_dp_nativeplugin":{  
+        "android": {  
+            "plugins": [  
+                {  
+                    "type": "module",  
+                    "name": "FaceAISDKModule",  
+                    "class": "io.face.uniplugin.FaceAISDKModule"  
+                },
+				{
+					"type": "component",
+					"name": "door-camera-x",
+					"class": "io.face.uniplugin.door.DoorCameraXComponent"
+				}
+            ],  
+            "hooksClass": "",  
+            "integrateType": "aar",  
+            "dependencies": [
+              "io.github.faceaisdk:Android:2025.09.25",
+              "com.airbnb.android:lottie:6.0.0",
+              "androidx.constraintlayout:constraintlayout:2.1.4",
+              "pub.devrel:easypermissions:3.0.0",
+              "com.airbnb.android:lottie:6.0.0",
+              "com.github.princekin-f:EasyFloat:2.0.4",
+              "androidx.cardview:cardview:1.0.0",
+              "androidx.recyclerview:recyclerview:1.3.2",
+              "com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.8",
+              "com.github.bumptech.glide:glide:4.16.0",
+              "androidx.appcompat:appcompat:1.6.0",
+              "com.github.javakam:file.core:3.9.8@aar",
+              "com.github.javakam:file.selector:3.9.8@aar",
+              "com.github.javakam:file.compressor:3.9.8@aar"
+            ],  
+            "compileOptions": {
+                "sourceCompatibility": "17",  
+                "targetCompatibility": "17"  
+            },  
+            "abis": [  
+				"armeabi-v7a",
+                "arm64-v8a"  
+            ],  
+            "minSdkVersion": "21",  
+            "permissions": [  
+				"<uses-permission android:name=\"android.permission.CAMERA\"/>"
+            ],  
+            "parameters": {  
+
+            }  
+        }
+	}  
+}

BIN
nativeplugins/Sys-Plugin/android/sysPlugin-release.aar


+ 50 - 0
nativeplugins/Sys-Plugin/package.json

@@ -0,0 +1,50 @@
+{
+    "name": "Sys-Plugin",
+    "id": "Sys-Plugin",
+    "version": "1.0.0",
+    "description": "系统硬件",
+    "_dp_type": "nativeplugin",
+    "_dp_nativeplugin": {
+        "android": {
+            "plugins": [
+                {
+                    "type": "module",
+                    "name": "sysPlugin",
+                    "class": "com.example.sysplugin.system"
+                },
+                {
+                    "type": "module",
+                    "name": "SerialPortModule",
+                    "class": "com.example.sysplugin.SerialPortModule"
+                },                
+                {
+                    "type": "module",
+                    "name": "yxPlugin",
+                    "class": "com.example.sysplugin.YxPluginModule"
+                },
+				{
+				    "type": "module",
+				    "name": "phPlugin",
+				    "class": "com.example.sysplugin.PhPluginModule"
+				},
+                {
+                    "type": "module",
+                    "name": "mqttPlugin",
+                    "class": "com.example.sysplugin.mqtt"
+                }
+            ],
+            "integrateType": "aar",
+            "dependencies": [],
+            "compileOptions": {
+                "sourceCompatibility": "1.8",
+                "targetCompatibility": "1.8"
+            },
+            "abis": [
+                "armeabi-v7a",
+                "x86"
+            ],
+            "minSdkVersion": "21",
+            "permissions": []
+        }
+    }
+}

+ 1 - 0
package.json

@@ -56,6 +56,7 @@
     "echarts": "^5.3.3",
     "jquery": "^3.7.1",
     "jsencrypt": "^3.3.2",
+    "mqtt": "^5.10.3",
     "pinia": "2.0.14",
     "pinia-plugin-persistedstate": "^3.1.0",
     "vue": "^3.4.21",

+ 4 - 4
src/App.vue

@@ -24,7 +24,7 @@ function stteingInit() {
   plus.screen.unlockOrientation(); //解除屏幕方向的锁定,但是不一定是竖屏;
   // 智能会议
   if (config.appInfo.appid === "__UNI__F3963F8") {
-    // plus.screen.lockOrientation("portrait-primary"); 
+    // plus.screen.lockOrientation("portrait-primary");
     plus.screen.lockOrientation("landscape-primary"); //设置屏幕方向(1.竖屏正方向:portrait-primary 2.竖屏反方向:portrait-secondary 3.横屏正方向:landscape-primary 4.横屏反方向:landscape-secondary 5.自然方向:default)
     proxy.$keyListen.startListen({
       needStopSystem: true,
@@ -59,10 +59,8 @@ onHide(() => {
 </script>
 
 <style lang="scss">
+// #ifndef APP-PLUS-NVUE
 @import "@/static/scss/index.scss";
-</style>
-
-<style lang="scss">
 @import "@/uni_modules/uview-plus/index.scss";
 
 uni-page-body,
@@ -80,4 +78,6 @@ uni-page-refresh {
   font-size: $uni-font-size-lg;
   font-weight: bold;
 }
+
+//#endif
 </style>

+ 9 - 0
src/api/business/door.js

@@ -68,4 +68,13 @@ export function doorApi() {
             });
         },
     };
+}
+
+//门禁设备心跳接口
+export function egDeviceHeartbeat(data) {
+    return request({
+        url: '/service-eg/egDeviceHeartbeat/escalation',
+        method: 'POST',
+        data
+    })
 }

+ 7 - 0
src/api/business/face.js

@@ -13,5 +13,12 @@ export function faceApi() {
                 data,
             });
         },
+        facePerson: (data) => {
+            return request({
+                url: '/service-eg/smartface/compare/person',
+                method: 'POST',
+                data,
+            });
+        },
     };
 }

+ 2 - 2
src/api/business/meeting.js

@@ -288,8 +288,8 @@ export function signOnOut(data) {
     })
 }
 
-//设备心跳接口
-export function escalation(data) {
+//会议设备心跳接口
+export function meetingDeviceHeartbeat(data) {
     return request({
         url: '/service-meeting/meetingDeviceHeartbeat/escalation',
         method: 'POST',

+ 181 - 124
src/components/oa-passBody/index.vue

@@ -1,22 +1,30 @@
 <template>
   <view class="visit-box" v-if="show">
+    <image class="visit-bg" src="./bg.png" mode="scaleToFill"></image>
     <!-- <view class="forget-pass" @tap="forgetpass">忘记密码</view> -->
     <image class="visit-close" src="./close1.png" @click="close"></image>
-    <view class="visit-icon"><image :src="icon"></image></view>
-    <view class="visit-text">请输入密码</view>
+    <text class="visit-text">请输入密码</text>
     <view class="visit-list">
-      <view class="visit-list-box" v-for="(item, index) in passselect" :key="index" :style="{ background: index < selectIndex ? '#FFFFFF' : 'transparent' }"> </view>
+      <view class="visit-list-box" v-for="(item, index) in passselect" :key="index"
+        :class="{ 'visit-list-box-last': index === passselect.length - 1 }" :style="{
+          backgroundColor: index < selectIndex ? '#FFFFFF' : 'rgba(0,0,0,0)',
+        }"></view>
     </view>
     <view class="visit-Nine-palaces">
-      <view
-        class="visit-Nine-palaces-list"
-        v-for="(item, index) in Keylist"
-        :key="index"
-        :style="{ borderColor: styleClass(item.key), background: keytype(item.key, index) }"
-        @click="selectkey(item, index)"
-      >
-        <text v-if="item.key !== 'backspace'">{{ item.key }}</text>
-        <view class="qingchu-box" v-else><image src="./qingchu1.png"></image></view>
+      <view class="nine-row" v-for="(row, ri) in keyRows" :key="'row-' + ri"
+        :class="{ 'nine-row-last': ri === keyRows.length - 1 }">
+        <view class="visit-Nine-palaces-list" v-for="(item, ci) in row" :key="'cell-' + ri + '-' + ci"
+          :class="{ 'visit-Nine-palaces-list-row-end': ci === 2 }" :style="{
+            borderColor: styleClass(item.key),
+            backgroundColor: keytype(item.key, ri * 3 + ci),
+          }" @click="selectkey(item, ri * 3 + ci)">
+          <text v-if="item.key !== 'backspace'" class="nine-key-text">{{
+            item.key
+            }}</text>
+          <view class="qingchu-box" v-else>
+            <image class="qingchu-img" src="./qingchu1.png" mode="aspectFit"></image>
+          </view>
+        </view>
       </view>
     </view>
   </view>
@@ -38,13 +46,37 @@ export default {
   data() {
     return {
       passselect: new Array(6),
-      Keylist: [{ key: 1 }, { key: 2 }, { key: 3 }, { key: 4 }, { key: 5 }, { key: 6 }, { key: 7 }, { key: 8 }, { key: 9 }, { key: "#" }, { key: 0 }, { key: "backspace" }],
+      Keylist: [
+        { key: 1 },
+        { key: 2 },
+        { key: 3 },
+        { key: 4 },
+        { key: 5 },
+        { key: 6 },
+        { key: 7 },
+        { key: 8 },
+        { key: 9 },
+        { key: "#" },
+        { key: 0 },
+        { key: "backspace" },
+      ],
       key_index: null,
       select: [], //选择的时候,
       selectIndex: null,
       KeylistIndex: null,
     };
   },
+  computed: {
+    /** 一行三个键,避免 nvue 下 flex-wrap 表现不一致 */
+    keyRows() {
+      const list = this.Keylist;
+      const rows = [];
+      for (let i = 0; i < list.length; i += 3) {
+        rows.push(list.slice(i, i + 3));
+      }
+      return rows;
+    },
+  },
   methods: {
     styleClass(type) {
       // return typeof type !== "number" ? "transparent" : "#ffffff";
@@ -54,7 +86,7 @@ export default {
       if (this.key_index === index && typeof type === "number") {
         type = this.selectColor;
       } else {
-        type = "transparent";
+        type = "rgba(0,0,0,0)";
       }
       return type;
     },
@@ -101,120 +133,145 @@ export default {
 };
 </script>
 
-<style lang="scss" scoped>
+<style scoped>
 .qingchu-box {
-  width: 100%;
-  height: 100%;
-  text-align: center;
-  padding-top: 0.325rem;
-  & > image {
-    width: 50%;
-    height: 50%;
-  }
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.qingchu-img {
+  width: 46px;
+  height: 46px;
+}
+
+.visit-bg {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
 }
+
 .visit-box {
-  width: 100vw;
-  height: 100vh;
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
   overflow: hidden;
+  flex-direction: column;
+  align-items: center;
+}
+
+/* 预留:忘记密码 */
+.visit-box .forget-pass {
+  width: 72px;
+  height: 28px;
+  background-color: rgba(0, 0, 0, 0.3);
+  border-radius: 14px 0px 0px 14px;
+  font-size: 13px;
+  font-weight: 400;
+  line-height: 28px;
+  color: #ffffff;
+  opacity: 1;
+  text-align: center;
+  position: absolute;
+  top: 10px;
+  right: 0px;
+}
+
+.visit-close {
+  position: absolute;
+  top: 25rpx;
+  right: 25rpx;
+  width: 40px;
+  height: 40px;
+}
+
+.visit-text {
+  /* 不用百分比:直接用左右贴边铺满 */
   position: relative;
-  background: url(./bg.png) no-repeat;
-  background-size: 100% 100%;
-
-  .forget-pass {
-    width: 144rpx;
-    height: 56rpx;
-    background: rgba(0, 0, 0, 0.3);
-    border-radius: 28rpx 0rpx 0rpx 28rpx;
-    font-size: 26rpx;
-    font-family: PingFang SC;
-    font-weight: 400;
-    line-height: 56rpx;
-    color: #ffffff;
-    opacity: 1;
-    text-align: center;
-    position: absolute;
-    top: 19rpx;
-    right: 0rpx;
-  }
-
-  .visit-close {
-    display: block;
-    width: 40rpx;
-    height: 40rpx;
-    margin-top: 50rpx;
-    margin-right: 50rpx;
-    margin-left: auto;
-    margin-bottom: 180rpx;
-  }
-
-  .visit-icon {
-    width: 68rpx;
-    height: 68rpx;
-    opacity: 1;
-    margin: 0 auto;
-    & > image {
-      width: 100%;
-      height: 100%;
-    }
-  }
-  .visit-text {
-    width: 100%;
-    height: 42rpx;
-    font-size: 30rpx;
-    font-family: PingFang SC;
-    font-weight: bold;
-    line-height: 41rpx;
-    color: #ffffff;
-    opacity: 1;
-    text-align: center;
-    margin-top: 15rpx;
-  }
-  .visit-list {
-    width: 45vw;
-    height: 28rpx;
-    display: flex;
-    flex-wrap: wrap;
-    margin: 0 auto;
-    margin-top: 35rpx;
-    margin-bottom: 50rpx;
-    justify-content: center;
-    .visit-list-box {
-      width: 28rpx;
-      height: 28rpx;
-      border: 2px solid #ffffff;
-      border-radius: 50%;
-      margin-right: 20rpx;
-
-      &:nth-last-child(1) {
-        margin-right: 0rpx;
-      }
-    }
-  }
-  .visit-Nine-palaces {
-    width: 70vw;
-    height: 579rpx;
-    margin: 0 auto;
-    // border: 1rpx solid;
-    display: flex;
-    flex-wrap: wrap;
-    justify-content: center;
-    .visit-Nine-palaces-list {
-      width: 128rpx;
-      height: 128rpx;
-      border: 4rpx solid #ffffff;
-      border-radius: 50%;
-      opacity: 1;
-      text-align: center;
-      font-size: 50rpx;
-      font-family: Roboto;
-      font-weight: bold;
-      line-height: 120rpx;
-      color: #ffffff;
-      margin-right: 50rpx;
-      &:nth-child(3n) {
-        margin-right: 0rpx;
-      }
-    }
-  }
+  left: 0px;
+  right: 0px;
+  font-size: 30px;
+  font-weight: bold;
+  color: #ffffff;
+  text-align: center;
+  margin-top: 110rpx;
+}
+
+.visit-list {
+  width: 200px;
+  height: 25px;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  margin-top: 18px;
+  margin-bottom: 50px;
+  justify-content: center;
+  align-items: center;
+}
+
+.visit-list-box {
+  width: 25px;
+  height: 25px;
+  border-radius: 15px;
+  border-width: 2px;
+  border-style: solid;
+  border-color: #ffffff;
+  margin-right: 10px;
+}
+
+.visit-list-box-last {
+  margin-right: 0px;
+}
+
+.visit-Nine-palaces {
+  width: 340px;
+  flex-direction: column;
+}
+
+.nine-row {
+  width: 340px;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  margin-bottom: 18px;
+}
+
+.nine-row-last {
+  margin-bottom: 0px;
+}
+
+.visit-Nine-palaces-list {
+  position: relative;
+  width: 90px;
+  height: 90px;
+  border-radius: 45px;
+  border-width: 3px;
+  border-style: solid;
+  border-color: #ffffff;
+  opacity: 1;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  margin-right: 35px;
+}
+
+.visit-Nine-palaces-list-row-end {
+  margin-right: 0px;
+}
+
+.nine-key-text {
+  font-size: 36px;
+  font-weight: bold;
+  line-height: 86px;
+  color: #ffffff;
+  text-align: center;
 }
 </style>

+ 6 - 0
src/config.js

@@ -6,12 +6,18 @@ export default {
 
   //#ifdef APP-PLUS || MP-WEIXIN
   baseUrl: "https://gateWay.usky.cn/prod-api",
+  // baseUrl: "http://192.168.10.165:801/dev-api",
   //#endif
 
   //#ifdef H5
   baseUrl: import.meta.env.MODE === "production" ? `https://${window.location.host}/prod-api` : `http://192.168.10.165:801/dev-api`,
   //#endif
 
+  //#ifdef APP-PLUS
+  // mqttUrl: "wss://gateWay.usky.cn/mqtt",
+  mqttUrl: "wss://192.168.10.165:801/mqtt",
+  //#endif
+
   websiteUrl: "https://qhome.usky.cn",
   // 应用信息
   appInfo: {

+ 44 - 8
src/pages.json

@@ -13,9 +13,9 @@
             }
         },
         {
-            "path": "pages/door/setting/index",
+            "path": "pages/meeting/setting/password",
             "style": {
-                "navigationBarTitleText": "",
+                "navigationBarTitleText": "密码验证",
                 "enablePullDownRefresh": false,
                 "navigationStyle": "custom",
                 "app-plus": {
@@ -25,9 +25,9 @@
             }
         },
         {
-            "path": "pages/door/setting/other/index",
+            "path": "pages/meeting/setting/SerialPort",
             "style": {
-                "navigationBarTitleText": "",
+                "navigationBarTitleText": "串口通讯",
                 "enablePullDownRefresh": false,
                 "navigationStyle": "custom",
                 "app-plus": {
@@ -37,7 +37,19 @@
             }
         },
         {
-            "path": "pages/door/setting/public/index",
+            "path": "pages/meeting/setting/index",
+            "style": {
+                "navigationBarTitleText": "会议配置",
+                "enablePullDownRefresh": false,
+                "navigationStyle": "custom",
+                "app-plus": {
+                    "bounce": "none",
+                    "titleNView": false
+                }
+            }
+        },
+        {
+            "path": "pages/door/setting/index",
             "style": {
                 "navigationBarTitleText": "",
                 "enablePullDownRefresh": false,
@@ -49,7 +61,7 @@
             }
         },
         {
-            "path": "pages/door/setting/serve/index",
+            "path": "pages/door/setting/public/index",
             "style": {
                 "navigationBarTitleText": "",
                 "enablePullDownRefresh": false,
@@ -85,9 +97,33 @@
             }
         },
         {
-            "path": "pages/face/index",
+            "path": "pages/door/setting/system/network",
+            "style": {
+                "navigationBarTitleText": "",
+                "enablePullDownRefresh": false,
+                "navigationStyle": "custom",
+                "app-plus": {
+                    "bounce": "none",
+                    "titleNView": false
+                }
+            }
+        },
+        {
+            "path": "pages/door/faceStorage/index",
+            "style": {
+                "navigationBarTitleText": "人脸数据管理",
+                "enablePullDownRefresh": false,
+                "navigationStyle": "custom",
+                "app-plus": {
+                    "bounce": "none",
+                    "titleNView": false
+                }
+            }
+        },
+        {
+            "path": "pages/meeting/index",
             "style": {
-                "navigationBarTitleText": "人脸识别",
+                "navigationBarTitleText": "智能会议",
                 "enablePullDownRefresh": false,
                 "navigationStyle": "custom",
                 "app-plus": {

+ 191 - 0
src/pages/door/components/face-verify-popup.nvue

@@ -0,0 +1,191 @@
+<template>
+  <view v-if="show" class="mask" @click="handleMaskClick">
+    <view class="card" :class="success ? 'cardOk' : 'cardFail'" @click.stop="noop">
+      <view class="avatar">
+        <view class="avatar-inner">
+          <image
+            v-if="resolvedAvatarSrc"
+            class="avatar-img"
+            :src="resolvedAvatarSrc"
+            mode="aspectFill"
+          ></image>
+        </view>
+      </view>
+      <text class="mark">{{ success ? "✓" : "×" }}</text>
+      <text class="title">{{ success ? "身份验证通过" : "身份验证失败" }}</text>
+      <text class="sub">{{ subLine }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: "FaceVerifyPopup",
+  props: {
+    show: {
+      type: Boolean,
+      default: false,
+    },
+    success: {
+      type: Boolean,
+      default: false,
+    },
+    faceName: {
+      type: String,
+      default: "",
+    },
+    failMessage: {
+      type: String,
+      default: "",
+    },
+    avatarSrc: {
+      type: String,
+      default: "",
+    },
+    duration: {
+      type: Number,
+      default: 2800,
+    },
+    closeOnMask: {
+      type: Boolean,
+      default: true,
+    },
+  },
+  data() {
+    return {
+      closeTimer: null,
+    };
+  },
+  computed: {
+    /** App 本地绝对路径需 file:// 前缀以便 <image> 加载 */
+    resolvedAvatarSrc() {
+      const raw = String(this.avatarSrc || "").trim();
+      if (!raw) return "";
+      if (/^https?:\/\//i.test(raw)) return raw;
+      if (raw.startsWith("/static/")) return raw;
+      if (raw.startsWith("file://")) return raw;
+      if (raw.startsWith("/")) return "file://" + raw;
+      return raw;
+    },
+    subLine() {
+      if (this.success) {
+        const n = (this.faceName || "").trim();
+        return n ? `欢迎您,${n}` : "欢迎您";
+      }
+      const m = (this.failMessage || "").trim();
+      return m || "陌生人";
+    },
+  },
+  watch: {
+    show(val) {
+      if (this.closeTimer) {
+        clearTimeout(this.closeTimer);
+        this.closeTimer = null;
+      }
+      if (val && this.duration > 0) {
+        this.closeTimer = setTimeout(() => {
+          this.closeTimer = null;
+          this.$emit("close");
+        }, this.duration);
+      }
+    },
+  },
+  beforeDestroy() {
+    if (this.closeTimer) {
+      clearTimeout(this.closeTimer);
+    }
+  },
+  methods: {
+    noop() {},
+    handleMaskClick() {
+      if (this.closeOnMask) {
+        if (this.closeTimer) {
+          clearTimeout(this.closeTimer);
+          this.closeTimer = null;
+        }
+        this.$emit("close");
+      }
+    },
+  },
+};
+</script>
+
+<style scoped>
+.mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.45);
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  z-index: 100080;
+}
+.card {
+  width: 336px;
+  padding-top: 34px;
+  padding-bottom: 30px;
+  padding-left: 26px;
+  padding-right: 26px;
+  border-radius: 22px;
+  flex-direction: column;
+  align-items: center;
+}
+.cardOk {
+  background-color: #525f7f;
+}
+.cardFail {
+  background-color: #a56961;
+}
+/* 外圈白边;内层与图片同尺寸,避免 border 下仍用 104 全宽 + absolute 在 weex 中错位 */
+.avatar {
+  width: 104px;
+  height: 104px;
+  border-radius: 52px;
+  border-width: 4px;
+  border-color: #ffffff;
+  border-style: solid;
+  background-color: #ffffff;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+.avatar-inner {
+  width: 96px;
+  height: 96px;
+  border-radius: 48px;
+  background-color: #ffffff;
+  overflow: hidden;
+}
+.avatar-img {
+  width: 96px;
+  height: 96px;
+  border-radius: 48px;
+}
+.mark {
+  margin-top: 18px;
+  font-size: 54px;
+  line-height: 54px;
+  color: #ffffff;
+  font-weight: 700;
+}
+.title {
+  margin-top: 16px;
+  font-size: 26px;
+  line-height: 34px;
+  color: #ffffff;
+  font-weight: 700;
+  text-align: center;
+}
+.sub {
+  margin-top: 10px;
+  font-size: 20px;
+  line-height: 28px;
+  color: rgba(255, 255, 255, 0.95);
+  font-weight: 400;
+  text-align: center;
+}
+</style>

+ 1116 - 0
src/pages/door/faceStorage/index.vue

@@ -0,0 +1,1116 @@
+<template>
+  <view
+    class="faceStorage-page"
+    :data-theme="'theme-' + proxy.$settingStore.themeColor.name"
+  >
+    <view class="faceStorage-header">
+      <text class="iconfont oaIcon-left" @click="handleExit()"></text>
+      <view class="faceStorage-title">人脸数据管理</view>
+    </view>
+
+    <oa-scroll
+      customClass="faceStorage-container scroll-height"
+      :customStyle="{
+        //#ifdef APP-PLUS || MP-WEIXIN
+        height: `calc(100vh - (60px))`,
+        //#endif
+        //#ifdef H5
+        height: `calc(100vh - (60px))`,
+        //#endif
+      }"
+      :refresherLoad="false"
+      :refresherEnabled="false"
+      :refresherDefaultStyle="'none'"
+      :refresherBackground="'#f5f6f7'"
+      :data-theme="'theme-' + proxy.$settingStore.themeColor.name"
+    >
+      <template #default>
+        <view class="container">
+          <!-- 顶部统计 -->
+          <view class="stats-bar">
+            <view class="stat-item">
+              <text class="stat-num">{{ stats.total || 0 }}</text>
+              <text class="stat-label">总数</text>
+            </view>
+            <view class="stat-item">
+              <text class="stat-num">{{ stats.unsynced || 0 }}</text>
+              <text class="stat-label">未同步</text>
+            </view>
+            <view class="stat-item">
+              <text class="stat-num">{{ stats.synced || 0 }}</text>
+              <text class="stat-label">已同步</text>
+            </view>
+          </view>
+
+          <!-- 操作按钮 -->
+          <view class="action-bar">
+            <button class="btn btn-primary" @click="addFace">添加人脸</button>
+            <button class="btn btn-info" @click="verifyFace">校验人脸</button>
+            <button class="btn btn-secondary" @click="syncServer">
+              同步服务器
+            </button>
+            <button class="btn btn-success" @click="exportData">
+              导出数据
+            </button>
+            <button class="btn btn-warning" @click="importData">
+              导入数据
+            </button>
+            <button class="btn btn-danger" @click="clearAll">清空数据</button>
+          </view>
+
+          <!-- 搜索框 -->
+          <view class="search-box">
+            <input
+              class="search-input"
+              placeholder="搜索人脸ID或姓名..."
+              v-model="searchText"
+              @input="onSearch"
+            />
+          </view>
+
+          <view class="face-list">
+            <view
+              v-for="face in displayList"
+              :key="face.faceId"
+              class="face-item"
+            >
+              <image
+                v-if="faceImageSrc(face)"
+                class="face-thumb"
+                :src="faceImageSrc(face)"
+                mode="aspectFill"
+              />
+              <view class="face-info">
+                <text class="face-id">{{ face.faceId }}</text>
+                <text v-if="face.faceName" class="face-name">{{
+                  face.faceName
+                }}</text>
+                <text class="face-time">{{ formatTime(face.createTime) }}</text>
+                <text
+                  class="face-status"
+                  :class="getStatusClass(face.syncStatus)"
+                >
+                  {{ getStatusText(face.syncStatus) }}
+                </text>
+              </view>
+              <view class="face-actions">
+                <button
+                  class="btn-small btn-danger"
+                  @click.stop="deleteFace(face.faceId)"
+                >
+                  删除
+                </button>
+              </view>
+            </view>
+
+            <view v-if="displayList.length === 0" class="empty-state">
+              <text class="empty-text">暂无数据</text>
+            </view>
+          </view>
+
+          <!-- 添加人脸弹窗 -->
+          <view v-if="showAddDialog" class="modal" @click="closeAddDialog">
+            <view class="modal-content" @click.stop>
+              <view class="modal-header">
+                <text class="modal-title">添加人脸</text>
+                <text class="modal-close" @click="closeAddDialog">×</text>
+              </view>
+
+              <view class="modal-body">
+                <view class="form-item">
+                  <text class="form-label">姓名</text>
+                  <input
+                    class="form-input"
+                    v-model="newFace.faceName"
+                    placeholder="请输入姓名"
+                  />
+                </view>
+
+                <view class="form-item">
+                  <text class="form-label">选择图片</text>
+                  <button class="btn btn-secondary" @click="selectImage">
+                    {{ newFace.imageBase64 ? "重新选择" : "选择图片" }}
+                  </button>
+                  <image
+                    v-if="newFace.imageBase64"
+                    class="preview-img"
+                    :src="newFace.imageBase64"
+                  />
+                </view>
+              </view>
+
+              <view class="modal-footer">
+                <button class="btn btn-secondary" @click="closeAddDialog">
+                  取消
+                </button>
+                <button
+                  class="btn btn-primary"
+                  @click="confirmAdd"
+                  :disabled="!canAdd"
+                >
+                  确定
+                </button>
+              </view>
+            </view>
+          </view>
+
+          <!-- 校验人脸弹窗 -->
+          <view
+            v-if="showVerifyDialog"
+            class="modal"
+            @click="closeVerifyDialog"
+          >
+            <view class="modal-content large" @click.stop>
+              <view class="modal-header">
+                <text class="modal-title">校验人脸</text>
+                <text class="modal-close" @click="closeVerifyDialog">×</text>
+              </view>
+
+              <view class="modal-body">
+                <view class="form-item">
+                  <text class="form-label">选择图片</text>
+                  <button class="btn btn-secondary" @click="selectVerifyImage">
+                    {{ verifyImage.imageBase64 ? "重新选择" : "选择图片" }}
+                  </button>
+                  <image
+                    v-if="verifyImage.imageBase64"
+                    class="preview-img"
+                    :src="verifyImage.imageBase64"
+                  />
+                </view>
+
+                <!-- 校验结果 -->
+                <view v-if="verifyResult" class="verify-result">
+                  <view class="result-header">
+                    <text class="result-title">校验结果</text>
+                  </view>
+
+                  <view class="result-summary">
+                    <text
+                      class="result-text"
+                      :class="verifyResult.hasFace ? 'success' : 'error'"
+                    >
+                      {{ verifyResult.hasFace ? "检测到人脸" : "未检测到人脸" }}
+                    </text>
+                    <text
+                      v-if="verifyResult.hasFace && verifyResult.matchedFaceId"
+                      class="result-text success"
+                    >
+                      匹配的人脸ID: {{ verifyResult.matchedFaceId }}
+                    </text>
+                    <text
+                      v-if="verifyResult.hasFace && verifyResult.matchedFaceId"
+                      class="result-text success"
+                    >
+                      相似度: {{ (verifyResult.similarity * 100).toFixed(1) }}%
+                    </text>
+                    <text
+                      v-if="verifyResult.hasFace && !verifyResult.matchedFaceId"
+                      class="result-text warning"
+                    >
+                      未找到匹配的人脸
+                    </text>
+                  </view>
+                </view>
+              </view>
+
+              <view class="modal-footer">
+                <button class="btn btn-secondary" @click="closeVerifyDialog">
+                  关闭
+                </button>
+                <button
+                  class="btn btn-primary"
+                  @click="confirmVerify"
+                  :disabled="!verifyImage.imageBase64 || loading"
+                >
+                  {{ loading ? "校验中..." : "开始校验" }}
+                </button>
+              </view>
+            </view>
+          </view>
+
+          <!-- 加载提示 -->
+          <view v-if="loading" class="loading">
+            <text class="loading-text">{{ loadingText }}</text>
+          </view>
+        </view>
+      </template>
+    </oa-scroll>
+  </view>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, reactive, ref } from "vue";
+import { onLoad, onPullDownRefresh, onShow } from "@dcloudio/uni-app";
+import sysPlugins from "@/plugins/device/sys.plugins";
+
+const { proxy } = getCurrentInstance();
+
+// 人脸识别插件
+const faceAIModule = uni.requireNativePlugin("FaceAISDKModule");
+const modal = uni.requireNativePlugin("modal");
+
+// 数据
+const faceList = ref([]);
+const stats = ref({});
+const searchText = ref("");
+
+// 弹窗状态
+const showAddDialog = ref(false);
+const showVerifyDialog = ref(false);
+
+// 添加人脸(人脸ID 由原生插件按本地库规则生成,前端不传 faceId)
+const newFace = reactive({
+  faceName: "",
+  imageBase64: "",
+});
+
+// 校验人脸
+const verifyImage = reactive({
+  imageBase64: "",
+});
+const verifyResult = ref(null);
+
+// 加载状态
+const loading = ref(false);
+const loadingText = ref("加载中...");
+
+const displayList = computed(() => {
+  const q = searchText.value.trim().toLowerCase();
+  if (!q) return faceList.value;
+  return faceList.value.filter((face) => {
+    const idMatch =
+      face.faceId && String(face.faceId).toLowerCase().includes(q);
+    const nameMatch =
+      face.faceName && String(face.faceName).toLowerCase().includes(q);
+    return idMatch || nameMatch;
+  });
+});
+
+const canAdd = computed(() => {
+  return Boolean(newFace.faceName.trim() && newFace.imageBase64);
+});
+
+function handleExit() {
+  proxy.$tab.navigateBack(1);
+}
+
+async function loadData() {
+  loading.value = true;
+  loadingText.value = "加载数据中...";
+
+  try {
+    await Promise.all([getStats(), getFaceList()]);
+  } catch (e) {
+    console.error("加载数据失败:", e);
+    modal.toast({ message: "加载失败", duration: 1500 });
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function getStats() {
+  await faceAIModule.getFaceStorageStats({}, (result) => {
+    if (result && result.code === 1) {
+      stats.value = result.data || {};
+    } else {
+      console.error("获取统计失败:", result);
+    }
+  });
+}
+
+async function getFaceList() {
+  await faceAIModule.getFaceList({ page: 0, pageSize: 1000 }, (result) => {
+    if (result && result.code === 1) {
+      faceList.value = result.data || [];
+    } else {
+      console.error("获取列表失败:", result);
+    }
+  });
+}
+
+function addFace() {
+  newFace.faceName = "";
+  newFace.imageBase64 = "";
+  showAddDialog.value = true;
+}
+
+function verifyFace() {
+  verifyImage.imageBase64 = "";
+  verifyResult.value = null;
+  showVerifyDialog.value = true;
+}
+
+// 同步服务器数据:deviceCode 从 sysPlugins.getDeviceInfo().serial 获取
+async function syncServer() {
+  const info = sysPlugins.getDeviceInfo();
+  const deviceCode = info && info.serial ? String(info.serial).trim() : "";
+  if (!deviceCode) {
+    modal.toast({ message: "获取设备编码失败(serial 为空)", duration: 2000 });
+    return;
+  }
+
+  loading.value = true;
+  loadingText.value = "同步服务器数据中...";
+
+  await faceAIModule.downloadFacesFromServer(
+    { deviceCode, faceIds: [] },
+    (result) => {
+      if (result && result.code === 1) {
+        modal.toast({ message: result.msg || "同步完成", duration: 2000 });
+        loadData();
+      } else {
+        modal.toast({
+          message: "同步失败: " + (result ? result.msg : "未知错误"),
+          duration: 2500,
+        });
+      }
+      loading.value = false;
+    },
+  );
+}
+
+function selectImage() {
+  uni.chooseImage({
+    count: 1,
+    sizeType: ["compressed"],
+    sourceType: ["album", "camera"],
+    success: (res) => readImageFile(res.tempFilePaths[0]),
+    fail: (e) => {
+      console.error("选择图片失败:", e);
+      modal.toast({ message: "选择图片失败", duration: 1500 });
+    },
+  });
+}
+
+function selectVerifyImage() {
+  uni.chooseImage({
+    count: 1,
+    sizeType: ["compressed"],
+    sourceType: ["album", "camera"],
+    success: (res) => readVerifyImageFile(res.tempFilePaths[0]),
+    fail: (e) => {
+      console.error("选择图片失败:", e);
+      modal.toast({ message: "选择图片失败", duration: 1500 });
+    },
+  });
+}
+
+function readImageFile(filePath) {
+  console.log("开始读取图片文件:", filePath);
+  readImageFileWithPlus(filePath);
+}
+
+function readVerifyImageFile(filePath) {
+  console.log("开始读取校验图片文件:", filePath);
+  readVerifyImageFileWithPlus(filePath);
+}
+
+function readImageFileWithPlus(filePath) {
+  if (typeof plus === "undefined") {
+    console.error("plus对象不可用,可能不在App环境中");
+    modal.toast({
+      message: "当前环境不支持文件读取,请重新选择",
+      duration: 1500,
+    });
+    return;
+  }
+
+  plus.io.resolveLocalFileSystemURL(
+    filePath,
+    (entry) => {
+      if (entry.isDirectory) {
+        console.error("路径指向目录,不是文件:", filePath);
+        modal.toast({
+          message: "选择的路径是目录,请选择图片文件",
+          duration: 1500,
+        });
+        return;
+      }
+
+      entry.file(
+        (file) => {
+          if (file.size === 0) {
+            modal.toast({ message: "文件为空,请重新选择", duration: 1500 });
+            return;
+          }
+
+          const reader = new plus.io.FileReader();
+          reader.onloadend = (e) => {
+            const dataUrl = (e && e.target && e.target.result) || "";
+            if (dataUrl) {
+              const base64 = String(dataUrl).replace(/^data:[^;]+;base64,/, "");
+              newFace.imageBase64 = `data:image/jpeg;base64,${base64}`;
+            } else {
+              modal.toast({
+                message: "图片数据为空,请重新选择",
+                duration: 1500,
+              });
+            }
+          };
+          reader.onerror = (err) => {
+            console.error("文件读取失败:", err);
+            modal.toast({
+              message: "读取图片失败,请重新选择",
+              duration: 1500,
+            });
+          };
+          reader.readAsDataURL(file);
+        },
+        (err) => {
+          console.error("获取文件对象失败:", err);
+          modal.toast({ message: "文件访问失败,请重新选择", duration: 1500 });
+        },
+      );
+    },
+    (err) => {
+      console.error("解析文件路径失败:", err);
+      modal.toast({ message: "文件路径解析失败,请重新选择", duration: 1500 });
+    },
+  );
+}
+
+function readVerifyImageFileWithPlus(filePath) {
+  if (typeof plus === "undefined") {
+    console.error("plus对象不可用,可能不在App环境中");
+    modal.toast({
+      message: "当前环境不支持文件读取,请重新选择",
+      duration: 1500,
+    });
+    return;
+  }
+
+  plus.io.resolveLocalFileSystemURL(
+    filePath,
+    (entry) => {
+      if (entry.isDirectory) {
+        console.error("路径指向目录,不是文件:", filePath);
+        modal.toast({
+          message: "选择的路径是目录,请选择图片文件",
+          duration: 1500,
+        });
+        return;
+      }
+
+      entry.file(
+        (file) => {
+          if (file.size === 0) {
+            modal.toast({ message: "文件为空,请重新选择", duration: 1500 });
+            return;
+          }
+
+          const reader = new plus.io.FileReader();
+          reader.onloadend = (e) => {
+            const dataUrl = (e && e.target && e.target.result) || "";
+            if (dataUrl) {
+              const base64 = String(dataUrl).replace(/^data:[^;]+;base64,/, "");
+              verifyImage.imageBase64 = `data:image/jpeg;base64,${base64}`;
+            } else {
+              modal.toast({
+                message: "图片数据为空,请重新选择",
+                duration: 1500,
+              });
+            }
+          };
+          reader.onerror = (err) => {
+            console.error("校验图片文件读取失败:", err);
+            modal.toast({
+              message: "读取图片失败,请重新选择",
+              duration: 1500,
+            });
+          };
+          reader.readAsDataURL(file);
+        },
+        (err) => {
+          console.error("获取校验图片文件对象失败:", err);
+          modal.toast({ message: "文件访问失败,请重新选择", duration: 1500 });
+        },
+      );
+    },
+    (err) => {
+      console.error("解析校验图片文件路径失败:", err);
+      modal.toast({ message: "文件路径解析失败,请重新选择", duration: 1500 });
+    },
+  );
+}
+
+async function confirmAdd() {
+  if (!canAdd.value) {
+    modal.toast({ message: "请填写完整信息", duration: 1500 });
+    return;
+  }
+
+  loading.value = true;
+  loadingText.value = "添加中...";
+
+  await faceAIModule.addFaceToStorage(
+    {
+      faceName: newFace.faceName.trim(),
+      faceBase64: newFace.imageBase64,
+      source: 0,
+    },
+    (result) => {
+      if (result && result.code === 1) {
+        const fid = result.data && result.data.faceId;
+        modal.toast({
+          message: fid ? `添加成功,人脸ID:${fid}` : "添加成功",
+          duration: 1500,
+        });
+        closeAddDialog();
+        loadData();
+      } else {
+        modal.toast({
+          message: "添加失败: " + (result ? result.msg : "未知错误"),
+          duration: 2000,
+        });
+      }
+      loading.value = false;
+    },
+  );
+}
+
+async function confirmVerify() {
+  if (!verifyImage.imageBase64) {
+    modal.toast({ message: "请选择图片", duration: 1500 });
+    return;
+  }
+
+  loading.value = true;
+  loadingText.value = "校验中...";
+
+  await faceAIModule.verifyFaceByImage(
+    { imageBase64: verifyImage.imageBase64 },
+    (result) => {
+      if (result && result.code === 1) {
+        modal.toast({
+          message: `人脸验证成功: ${result.matchedFaceId}`,
+          duration: 2000,
+        });
+      } else {
+        modal.toast({
+          message: `人脸验证失败: ${result ? result.msg : "未知错误"}`,
+          duration: 2000,
+        });
+      }
+      loading.value = false;
+    },
+  );
+}
+
+async function deleteFace(faceId) {
+  const res = await new Promise((resolve) => {
+    uni.showModal({
+      title: "确认删除",
+      content: `确定要删除 "${faceId}" 吗?`,
+      success: (r) => resolve(Boolean(r.confirm)),
+    });
+  });
+  if (!res) return;
+
+  loading.value = true;
+  loadingText.value = "删除中...";
+
+  await faceAIModule.deleteFaceFromStorage({ faceId }, (result) => {
+    if (result && result.code === 1) {
+      modal.toast({ message: "删除成功", duration: 1500 });
+      loadData();
+    } else {
+      modal.toast({
+        message: "删除失败: " + (result ? result.msg : "未知错误"),
+        duration: 2000,
+      });
+    }
+    loading.value = false;
+  });
+}
+
+async function exportData() {
+  loading.value = true;
+  loadingText.value = "导出中...";
+
+  await faceAIModule.exportFaceData({}, (result) => {
+    if (result && result.code === 1) {
+      modal.toast({
+        message: `已导出 ${faceList.value.length} 条数据`,
+        duration: 2000,
+      });
+    } else {
+      modal.toast({
+        message: "导出失败: " + (result ? result.msg : "未知错误"),
+        duration: 2000,
+      });
+    }
+    loading.value = false;
+  });
+}
+
+function importData() {
+  modal.toast({ message: "导入功能开发中", duration: 1500 });
+}
+
+async function clearAll() {
+  const res = await new Promise((resolve) => {
+    uni.showModal({
+      title: "确认清空",
+      content: "确定要清空所有数据吗?此操作不可恢复!",
+      success: (r) => resolve(Boolean(r.confirm)),
+    });
+  });
+  if (!res) return;
+
+  loading.value = true;
+  loadingText.value = "清空中...";
+
+  await faceAIModule.clearAllFaceData({}, (result) => {
+    if (result && result.code === 1) {
+      modal.toast({ message: "清空成功", duration: 1500 });
+      loadData();
+    } else {
+      modal.toast({
+        message: "清空失败: " + (result ? result.msg : "未知错误"),
+        duration: 2000,
+      });
+    }
+
+    loading.value = false;
+  });
+}
+
+function onSearch() {
+  // 搜索逻辑在computed中处理
+}
+
+function closeAddDialog() {
+  showAddDialog.value = false;
+  newFace.faceName = "";
+  newFace.imageBase64 = "";
+}
+
+function closeVerifyDialog() {
+  showVerifyDialog.value = false;
+  verifyImage.imageBase64 = "";
+  verifyResult.value = null;
+}
+
+function formatTime(timestamp) {
+  if (!timestamp) return "";
+  const date = new Date(timestamp);
+  return date.toLocaleDateString() + " " + date.toLocaleTimeString();
+}
+
+function getStatusText(status) {
+  const statusMap = { 0: "未同步", 1: "已同步", 2: "同步失败" };
+  return statusMap[status] || "未知";
+}
+
+function getStatusClass(status) {
+  const classMap = {
+    0: "status-unsynced",
+    1: "status-synced",
+    2: "status-failed",
+  };
+  return classMap[status] || "";
+}
+
+function getSourceText(source) {
+  const sourceMap = { 0: "本地录入", 1: "服务器下发" };
+  return sourceMap[source] || "未知";
+}
+
+function faceImageSrc(face) {
+  if (!face) return "";
+  const p = String(face.imagePath || face.face_image_path || "").trim();
+  if (!p) return "";
+  if (p.startsWith("data:image/")) return p;
+  if (p.startsWith("/")) return "file://" + p;
+  if (p.includes("://")) return p;
+  return p;
+}
+
+onLoad(() => {
+  loadData();
+});
+
+onShow(() => {
+  loadData();
+});
+
+onPullDownRefresh(() => {
+  loadData().finally(() => {
+    uni.stopPullDownRefresh();
+  });
+});
+</script>
+<style lang="scss" scoped>
+@import "../setting/index.scss";
+</style>
+<style scoped>
+.faceStorage-page {
+  background: white;
+  min-height: 100vh;
+}
+
+.faceStorage-header {
+  height: 60px;
+  padding: 0 20rpx;
+  background: white;
+  display: flex;
+  align-items: center;
+  position: sticky;
+  top: 0;
+  z-index: 50;
+}
+
+.faceStorage-title {
+  flex: 1;
+  text-align: center;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #111;
+  margin-right: 40rpx; /* 视觉居中,抵消左侧返回按钮 */
+}
+
+.faceStorage-container {
+  background: #f5f5f5 !important;
+}
+
+/* 统计栏 */
+.stats-bar {
+  display: flex;
+  background: white;
+  border-radius: 12rpx;
+  padding: 30rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+}
+
+.stat-item {
+  flex: 1;
+  text-align: center;
+}
+
+.stat-num {
+  display: block;
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #007aff;
+  margin-bottom: 8rpx;
+}
+
+.stat-label {
+  font-size: 24rpx;
+  color: #666;
+}
+
+/* 操作栏 */
+.action-bar {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 15rpx;
+  margin-bottom: 20rpx;
+}
+
+.btn {
+  flex: 1;
+  min-width: 150rpx;
+  height: 70rpx;
+  border-radius: 8rpx;
+  border: none;
+  font-size: 24rpx;
+  color: white;
+}
+
+.btn-primary {
+  background: #007aff;
+}
+
+.btn-success {
+  background: #28a745;
+}
+
+.btn-warning {
+  background: #ffc107;
+  color: #333;
+}
+
+.btn-danger {
+  background: #dc3545;
+}
+
+.btn-secondary {
+  background: #6c757d;
+}
+
+.btn-info {
+  background: #17a2b8;
+}
+
+.btn:disabled {
+  opacity: 0.6;
+}
+
+/* 搜索框 */
+.search-box {
+  margin-bottom: 20rpx;
+}
+
+.search-input {
+  width: 100%;
+  height: 70rpx;
+  padding: 0 20rpx;
+  border: 2rpx solid #ddd;
+  border-radius: 8rpx;
+  background: white;
+  font-size: 26rpx;
+  box-sizing: border-box;
+}
+
+.face-list {
+  width: 100%;
+  background: white;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+}
+
+.face-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.face-thumb {
+  width: 96rpx;
+  height: 96rpx;
+  border-radius: 10rpx;
+  background: #f0f0f0;
+  margin-right: 18rpx;
+  flex-shrink: 0;
+}
+
+.face-item:last-child {
+  border-bottom: none;
+}
+
+.face-info {
+  flex: 1;
+}
+
+.face-id {
+  display: block;
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+
+.face-name {
+  display: block;
+  font-size: 24rpx;
+  color: #555;
+  margin-bottom: 6rpx;
+}
+
+.face-time {
+  display: block;
+  font-size: 22rpx;
+  color: #666;
+  margin-bottom: 4rpx;
+}
+
+.face-status {
+  font-size: 20rpx;
+  padding: 4rpx 8rpx;
+  border-radius: 4rpx;
+  color: white;
+}
+
+.status-unsynced {
+  background: #ffc107;
+  color: #333;
+}
+
+.status-synced {
+  background: #28a745;
+}
+
+.status-failed {
+  background: #dc3545;
+}
+
+.face-actions {
+  margin-left: 20rpx;
+}
+
+.btn-small {
+  padding: 8rpx 16rpx;
+  border-radius: 6rpx;
+  border: none;
+  font-size: 20rpx;
+  color: white;
+}
+
+/* 空状态 */
+.empty-state {
+  text-align: center;
+  padding: 100rpx 0;
+}
+
+.empty-text {
+  font-size: 26rpx;
+  color: #999;
+}
+
+/* 弹窗 */
+.modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal-content {
+  width: 90%;
+  max-width: 600rpx;
+  background: white;
+  border-radius: 12rpx;
+  overflow: hidden;
+}
+
+.modal-content.large {
+  max-width: 800rpx;
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.modal-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.modal-close {
+  font-size: 40rpx;
+  color: #999;
+  line-height: 1;
+}
+
+.modal-body {
+  padding: 30rpx;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 20rpx;
+  padding: 30rpx;
+  border-top: 1rpx solid #f0f0f0;
+}
+
+/* 表单 */
+.form-item {
+  margin-bottom: 30rpx;
+}
+
+.form-label {
+  display: block;
+  font-size: 26rpx;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.form-input {
+  width: 100%;
+  height: 70rpx;
+  padding: 0 20rpx;
+  border: 2rpx solid #ddd;
+  border-radius: 8rpx;
+  font-size: 26rpx;
+  box-sizing: border-box;
+}
+
+.preview-img {
+  width: 200rpx;
+  height: 200rpx;
+  border-radius: 8rpx;
+  margin-top: 20rpx;
+}
+
+/* 加载提示 */
+.loading {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 2000;
+}
+
+.loading-text {
+  background: white;
+  padding: 30rpx 40rpx;
+  border-radius: 8rpx;
+  font-size: 26rpx;
+  color: #333;
+}
+
+/* 校验结果样式 */
+.verify-result {
+  margin-top: 30rpx;
+  padding: 20rpx;
+  background: #f8f9fa;
+  border-radius: 8rpx;
+}
+
+.result-header {
+  margin-bottom: 20rpx;
+}
+
+.result-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.result-summary {
+  margin-bottom: 20rpx;
+}
+
+.result-text {
+  display: block;
+  font-size: 26rpx;
+  margin-bottom: 10rpx;
+}
+
+.result-text.success {
+  color: #28a745;
+}
+
+.result-text.error {
+  color: #dc3545;
+}
+
+.result-text.warning {
+  color: #ffc107;
+}
+</style>

+ 519 - 0
src/pages/door/index.js

@@ -0,0 +1,519 @@
+import tab from "@/plugins/tab.plugins.js";
+import permission from "@/plugins/permission.plugins.js";
+import modal from "@/plugins/modal.plugins.js";
+import sysPlugins from "@/plugins/device/sys.plugins";
+import config from "@/config";
+import { doorApi, egDeviceHeartbeat } from "@/api/business/door.js";
+import { meetingDeviceHeartbeat } from "@/api/business/meeting.js";
+import time from "@/plugins/time.plugins.js";
+
+/** 门禁相关子页路径 */
+export const DoorRoutes = {
+    faceStorage: "/pages/door/faceStorage/index",
+    setting: "/pages/door/setting/index",
+};
+
+/**
+ * 门禁相关常量
+ */
+export const DoorArr = {
+    workStatusData: [
+        { label: "办公", value: 1, class: 'workRed' },
+        { label: "会客", value: 2, class: 'workRed' },
+        { label: "外出", value: 3, class: 'workGreen' },
+        { label: "勿扰", value: 4, class: 'workRed' },
+    ],
+    timerId: null,
+    doorList: [],
+    isClicked: false,
+    doorInter: {
+        doorDom: null,
+        egEscalation: null,
+        ipTimer: null,
+        meetingEscalation: null,
+        meeting: null,
+    },
+    /** 门禁页:表单与左下角展示用字段(index.nvue 初始化时拷贝,避免与单例共用引用) */
+    form: {
+        door: {
+            id: undefined,
+            name: undefined,
+        },
+        meetingId: undefined,
+        meetingName: undefined,
+    },
+    ipAddress: "",
+    deviceSerial: "",
+};
+
+/**
+ * 同步设备序列号到 DoorArr.deviceSerial(左下角 SN 展示)
+ */
+export function syncDeviceSerial() {
+    try {
+        const info = sysPlugins.getDeviceInfo && sysPlugins.getDeviceInfo();
+        DoorArr.deviceSerial = info && info.serial != null ? String(info.serial) : "";
+    } catch (e) {
+        DoorArr.deviceSerial = "";
+    }
+}
+
+/**
+ * 门禁开门方法
+ */
+export const DoorOpenFunc = {
+    click(vm, event) {
+        const ev = event || {};
+        if (!DoorArr.form || !DoorArr.form.door) {
+            modal.msg("请先绑定门禁!");
+            return;
+        }
+        if (!DoorArr.form.door.id) {
+            modal.msg("请先绑定门禁!");
+            return;
+        }
+        if (DoorArr.isClicked) {
+            modal.msg("请勿重复点击!");
+            return;
+        }
+        DoorArr.isClicked = true;
+
+        doorApi()
+            .control({
+                skipCheck: true,
+                userId: ev.userId ? ev.userId : undefined,
+                userName: ev.userName ? ev.userName : undefined,
+                passType: 1, // 1: 人脸 2: 刷卡 3: 手机
+                productCode: "502_USKY",
+                deviceUuid: DoorArr.form.door.deviceUuid,
+                commandCode: "door_onoff",
+                commandValue: 1,
+            })
+            .then((item) => {
+                if (item.status == "success") {
+                    modal.msg("开门成功");
+                } else {
+                    modal.msg("开门失败");
+                }
+                setTimeout(() => {
+                    DoorArr.isClicked = false;
+                }, 2000);
+            })
+            .catch(() => {
+                setTimeout(() => {
+                    DoorArr.isClicked = false;
+                }, 2000);
+            });
+    },
+    localOpen(vm) {
+        if (DoorArr.isClicked) {
+            modal.msg("请勿重复点击!");
+            return;
+        }
+        sysPlugins.openDoor(1, 0);
+        DoorArr.isClicked = true;
+    },
+};
+
+/**
+ * 相机相关
+ */
+export const cameraFunc = {
+    /** 取页面 doorCam 原生组件实例 */
+    getRef(vm) {
+        const cam = vm && vm.$refs ? vm.$refs.doorCam : null;
+        return cam || null;
+    },
+    /** 假隐藏控制:仅控制预览显示,不影响分析 */
+    setFakeHidden(vm, enabled) {
+        // #ifdef APP-PLUS
+        const cam = this.getRef(vm);
+        if (!cam || typeof cam.setFakeHidden !== "function") return;
+        cam.setFakeHidden(enabled != null && enabled);
+        // #endif
+    },
+    /** 语音播报(TTS) */
+    speak(vm, text) {
+        // #ifdef APP-PLUS
+        const cam = this.getRef(vm);
+        if (!cam || typeof cam.speak !== "function") return;
+        if (!vm || !vm.thresholds || vm.thresholds.ttsEnabled !== true) return;
+        const t = (text || "").toString();
+        if (!t.trim()) return;
+        cam.speak(t);
+        // #endif
+    },
+    /** 申请相机权限后启动门禁识别(含 setConfig) */
+    async start(vm) {
+        // #ifdef APP-PLUS
+        const ok = await permission.getPermisson("camera", true).catch(() => null);
+        if (!ok) return;
+
+        const cam = this.getRef(vm);
+        if (!cam || typeof cam.start !== "function") return;
+
+        if (typeof cam.setConfig === "function") {
+            cam.setConfig({ ...vm.thresholds, autoStart: true });
+        }
+        cam.start();
+        // #endif
+    },
+    /** 停止门禁相机/分析 */
+    stop(vm) {
+        // #ifdef APP-PLUS
+        const cam = this.getRef(vm);
+        if (cam && typeof cam.stop === "function") {
+            cam.stop();
+        }
+        // #endif
+    },
+    /** door-camera-x:状态回调 */
+    onState(vm, e) {
+        const d = e && e.detail ? e.detail : {};
+        if (d && d.state === "error") {
+            // 启动/运行失败:由页面生命周期或用户操作决定是否重试
+        }
+    },
+    /** door-camera-x:识别结果 */
+    onResult(vm, e) {
+        const d = (e && (e.detail?.detail || e.detail || e)) || {};
+
+        if (d.code == 1) {
+            sysPlugins.openDoor(1, 0);
+            FaceVerifyFunc.show(vm, d);
+        } else if (d.hasFace === true) {
+            FaceVerifyFunc.show(vm, d);
+        }
+    },
+};
+
+/**
+ * 门禁页底部按钮:远程开门 / 密码面板
+ */
+export const doorPageUi = {
+    parentMessage(vm, type) {
+        if (type === "点击开门") {
+            DoorOpenFunc.click(vm, {
+                userId: "",
+                userName: "快捷方式开门",
+            });
+            return;
+        }
+        if (type === "密码") {
+            cameraFunc.stop(vm);
+            vm.popup.show = true;
+            return;
+        }
+        uni.showToast({ title: type, icon: "none" });
+    },
+};
+
+/**
+ * 人脸识别结果弹窗(成功 / 库比对未命中)
+ */
+export const FaceVerifyFunc = {
+    show(vm, detail) {
+        if (!vm) return;
+        const code = Number(detail && detail.code);
+        const name = String((detail && (detail.matchedFaceName || detail.faceName)) || "").trim();
+        const msg = String((detail && detail.msg) || "").trim();
+        const avatarSrc = String((detail && (detail.avatarSrc || detail.capturePath)) || "").trim();
+
+        if (!vm.faceVerifyPopup) {
+            vm.faceVerifyPopup = {
+                show: false,
+                success: false,
+                faceName: "",
+                failMessage: "",
+                avatarSrc: "",
+            };
+        }
+
+        if (code === 1) {
+            vm.faceNotFoundStreak = 0;
+            if (vm.thresholds?.ttsEnabled) {
+                cameraFunc.speak(vm, "验证成功");
+            }
+            vm.faceVerifyPopup.success = true;
+            vm.faceVerifyPopup.faceName = name;
+            vm.faceVerifyPopup.failMessage = "";
+            vm.faceVerifyPopup.avatarSrc = avatarSrc;
+            vm.faceVerifyPopup.show = true;
+            return;
+        }
+
+        const streak = (vm.faceNotFoundStreak || 0) + 1;
+        vm.faceNotFoundStreak = streak;
+        if (streak < 3) {
+            return;
+        }
+        vm.faceNotFoundStreak = 0;
+
+        if (vm.thresholds?.ttsEnabled) {
+            cameraFunc.speak(vm, "验证失败");
+        }
+        vm.faceVerifyPopup.success = false;
+        vm.faceVerifyPopup.faceName = "";
+        vm.faceVerifyPopup.failMessage = "陌生人";
+        vm.faceVerifyPopup.avatarSrc = avatarSrc;
+        vm.faceVerifyPopup.show = true;
+    },
+    close(vm) {
+        if (!vm || !vm.faceVerifyPopup) return;
+        vm.faceVerifyPopup.show = false;
+    },
+};
+
+/**
+ * 按路由 key 跳转
+ */
+export function navigateByKey(vm, key) {
+    const url = DoorRoutes[key];
+    if (!url) return;
+
+    if (key === "faceStorage" || key === "setting") {
+        if (vm && typeof vm.stop === "function") {
+            vm.stop();
+        }
+    }
+
+    tab.navigateTo(url);
+}
+
+/**
+ * 时钟方法
+ */
+export const clockFunc = {
+    start(vm) {
+        if (!vm || DoorArr.timerId) return;
+        this.tick(vm);
+        DoorArr.timerId = setInterval(() => this.tick(vm), 1000);
+    },
+    stop(vm) {
+        if (!DoorArr.timerId) return;
+        clearInterval(DoorArr.timerId);
+        DoorArr.timerId = null;
+    },
+    tick(vm) {
+        if (!vm || !vm.state) return;
+        const d = new Date();
+        const pad2 = (n) => (n < 10 ? "0" + n : "" + n);
+        const yyyy = d.getFullYear();
+        const mm = pad2(d.getMonth() + 1);
+        const dd = pad2(d.getDate());
+        const hh = pad2(d.getHours());
+        const mi = pad2(d.getMinutes());
+        vm.state.date = `${yyyy}/${mm}/${dd}`;
+        vm.state.dateTime = `${hh}:${mi}`;
+    },
+};
+
+/**
+ * 密码面板
+ */
+export const onDoorPass = {
+    /** 密码键盘提交:校验、关弹层、恢复相机 */
+    change(vm, event) {
+        if (event == DoorArr.form.door?.password) {
+            vm.popup.show = false;
+            DoorOpenFunc.click(vm);
+        } else {
+            vm.popup.show = false;
+            modal.msg("密码错误");
+        }
+        vm.$nextTick(() => {
+            vm.$nextTick(() => {
+                cameraFunc.start(vm);
+            });
+        });
+    },
+    /** 密码面板关闭:同步显隐并恢复预览 */
+    close(vm, event) {
+        vm.popup.show = event;
+        vm.$nextTick(() => {
+            vm.$nextTick(() => {
+                cameraFunc.start(vm);
+            });
+        });
+    },
+};
+
+/**
+ * 移除指定字符串
+ */
+export function removeSegmentWithTarget(originalStr, targetStr) {
+    if (!originalStr || typeof originalStr !== "string") return originalStr;
+    if (originalStr.includes(targetStr)) {
+        const regex = new RegExp(`\\d*${targetStr}\\d*`, "g");
+        return originalStr.replace(regex, "");
+    }
+    return originalStr;
+}
+
+/**
+ * 将 form.door 同步到页面 state
+ */
+export function syncDoorUiFromForm(vm) {
+    const d = DoorArr.form && DoorArr.form.door;
+    if (!d || !vm.state) return;
+    if (d.deviceName != null) vm.state.doorName = d.deviceName;
+    else if (d.name != null) vm.state.doorName = d.name;
+    if (d.imgPath != null) vm.state.imgPath = d.imgPath;
+    if (d.openMode != null) vm.state.openMode = d.openMode;
+    if (d.remark != null) vm.state.remark = d.remark;
+    if (d.workStatus != null) {
+        const workStatusItem = DoorArr.workStatusData.find(item => item.value == d.workStatus);
+        if (workStatusItem) {
+            vm.state.workName = workStatusItem.label;
+            vm.state.workClass = workStatusItem.class;
+        } else {
+            vm.state.workName = '';
+            vm.state.workClass = '';
+        }
+    }
+}
+
+/**
+ * 从本地 storage_face 恢复表单
+ */
+export function initDoorData(vm) {
+    const storage = uni.getStorageSync("storage_face");
+    if (!storage) return;
+
+    if (config.appInfo.appid === "__UNI__F3963F8") {
+        DoorArr.form.meetingId = storage.meetingId || undefined;
+        DoorArr.form.meetingName = storage.meetingName || undefined;
+    } else if (config.appInfo.appid === "__UNI__8D6E9FD") {
+        DoorArr.form.door = {
+            ...DoorArr.form.door,
+            id: storage.door && storage.door.id != null ? storage.door.id : undefined,
+            name: storage.door && storage.door.name != null ? storage.door.name : undefined,
+            ...(storage.door || {}),
+        };
+        syncDoorUiFromForm(vm);
+    }
+}
+
+/**
+ * 网络请求方法
+ */
+export const apiRequest = {
+    /**
+     * 设备心跳上报
+     */
+    initDoorEscalation(vm, type) {
+        if (!sysPlugins.getDeviceInfo().serial) return;
+        sysPlugins.getIpAddress({
+            success: (res) => {
+                const param = {
+                    deviceCode: sysPlugins.getDeviceInfo().serial,
+                    ipAddr: res,
+                    deviceType: 1,
+                    model: sysPlugins.getDeviceInfo().model,
+                    manuFacturer: sysPlugins.getDeviceInfo().manufacturer,
+                    version: sysPlugins.getDeviceInfo().version,
+                    sdk: sysPlugins.getDeviceInfo().Sdk,
+                    deviceStatus: type === "eg" ? 1 : undefined,
+                };
+                if (type === "meeting") {
+                    meetingDeviceHeartbeat(param);
+                } else if (type === "eg") {
+                    egDeviceHeartbeat(param);
+                }
+            },
+        });
+    },
+    /**
+     * 获取门禁列表
+     */
+    getDoorList(vm) {
+        DoorArr.doorList.length = 0;
+        return doorApi()
+            .Select({
+                current: 1,
+                size: 2000,
+                deviceCode: sysPlugins.getDeviceInfo().serial || "NTECVSG3PL",
+            })
+            .then((requset) => {
+                if (!requset.data.records || requset.data.records.length <= 0) return;
+
+                let data = requset.data.records[0];
+                if (!time.isInRange(8, 18)) {
+                    data = { ...data, openMode: removeSegmentWithTarget(data.openMode, "点击开门") };
+                }
+
+                const openModeStr = typeof data.openMode === "string" ? data.openMode : "";
+                const silentFace = openModeStr.includes("静默人脸");
+                cameraFunc.setFakeHidden(vm, silentFace);
+
+                Object.assign(DoorArr.form.door, data);
+                uni.setStorageSync("storage_face", DoorArr.form);
+                syncDoorUiFromForm(vm);
+            })
+            .catch(() => { });
+    }
+}
+
+/**
+ * 门禁页定时器
+ */
+export const IntervalFunc = {
+    /**
+     * 开启定时器
+     */
+    open(vm, type) {
+        if (type === "ipAddress") {
+            if (DoorArr.doorInter.ipTimer) return;
+            DoorArr.doorInter.ipTimer = setInterval(() => {
+                sysPlugins.getIpAddress({
+                    success: (res) => {
+                        DoorArr.ipAddress = res;
+                    },
+                    error: () => {
+                        DoorArr.ipAddress = "";
+                    },
+                });
+            }, 1000);
+            return;
+        }
+
+        if (type === "door") {
+            if (DoorArr.doorInter.doorDom) return;
+
+            apiRequest.initDoorEscalation(vm, "eg");
+            DoorArr.doorInter.egEscalation = setInterval(() => {
+                apiRequest.initDoorEscalation(vm, "eg");
+            }, 1000 * 10);
+
+            apiRequest.getDoorList(vm);
+            DoorArr.doorInter.doorDom = setInterval(() => {
+                apiRequest.getDoorList(vm);
+            }, 1000 * 3);
+        }
+    },
+
+    /** 清理定时器 */
+    clear(vm) {
+        if (!vm) return;
+        if (DoorArr.doorInter.doorDom) {
+            clearInterval(DoorArr.doorInter.doorDom);
+            DoorArr.doorInter.doorDom = null;
+        }
+        if (DoorArr.doorInter.egEscalation) {
+            clearInterval(DoorArr.doorInter.egEscalation);
+            DoorArr.doorInter.egEscalation = null;
+        }
+        if (DoorArr.doorInter.meetingEscalation) {
+            clearInterval(DoorArr.doorInter.meetingEscalation);
+            DoorArr.doorInter.meetingEscalation = null;
+        }
+        if (DoorArr.doorInter.meeting) {
+            clearInterval(DoorArr.doorInter.meeting);
+            DoorArr.doorInter.meeting = null;
+        }
+        if (DoorArr.doorInter.ipTimer) {
+            clearInterval(DoorArr.doorInter.ipTimer);
+            DoorArr.doorInter.ipTimer = null;
+        }
+    },
+};

+ 325 - 0
src/pages/door/index.nvue

@@ -0,0 +1,325 @@
+<template>
+  <view class="pageRoot">
+    <image class="bg" :src="state.imgPath || defaultBg" mode="aspectFill"></image>
+
+    <view class="homeCard">
+      <view class="homeTop" @longpress="navigateByKey(this,'setting')">
+        <view class="dateArea">
+          <text class="time1">{{ state.dateTime }}</text>
+          <text class="time2">{{ state.date }}</text>
+        </view>
+        <text class="doorTitle" v-if="state.openMode?.includes('人脸') && !state.openMode?.includes('静默人脸')">{{ state.doorName || "未绑定门禁" }}</text>
+      </view>
+
+      <view class="homeCenter">
+        <view class="centerInfo" v-if="!state.openMode?.includes('人脸') || state.openMode?.includes('静默人脸')" @longpress="navigateByKey(this,'setting')">
+          <text class="infoTitle">{{ state.doorName || "未绑定门禁" }}</text>
+          <text class="remark" v-if="state.remark">{{ state.remark }}</text>
+          <text class="workStatus" v-if="state.workName" :class="[state.workClass]">{{ state.workName }}</text>
+        </view>
+
+        <view class="faceArea" v-if="state.openMode?.includes('人脸')">
+          <view class="faceRing">
+            <door-camera-x
+              ref="doorCam"
+              class="camera"
+              @onResult="cameraFunc.onResult(this, $event)"
+              @result="cameraFunc.onResult(this, $event)"
+              @on-result="cameraFunc.onResult(this, $event)"
+              @onState="cameraFunc.onState(this, $event)"
+              @state="cameraFunc.onState(this, $event)"
+              @on-state="cameraFunc.onState(this, $event)"
+            ></door-camera-x>
+          </view>
+        </view>
+      </view>
+
+      <view class="homeFooter">
+        <view class="btnArea">
+          <text class="iconfont" @click="doorPageUi.parentMessage(this, '点击开门')" v-if="state.openMode?.includes('点击开门')">{{ icons.openDoor }}</text>
+          <text class="iconfont" @click="doorPageUi.parentMessage(this, '密码')" v-if="state.openMode?.includes('密码')">{{ icons.password }}</text>
+        </view>
+      </view>
+    </view>
+
+    <view v-if="popup.show" class="passwordLayer">
+      <oaPassBody :show="popup.show" @change="onDoorPass.change(this, $event)" @close="onDoorPass.close(this, $event)"></oaPassBody>
+    </view>
+
+    <face-verify-popup
+      :show="faceVerifyPopup.show"
+      :success="faceVerifyPopup.success"
+      :face-name="faceVerifyPopup.faceName"
+      :fail-message="faceVerifyPopup.failMessage"
+      :avatar-src="faceVerifyPopup.avatarSrc"
+      @close="FaceVerifyFunc.close(this)"
+    ></face-verify-popup>
+
+    <view class="cornerMeta" v-if="DoorArr.ipAddress || DoorArr.deviceSerial">
+      <text class="cornerText cornerIp" v-if="DoorArr.ipAddress">IP:{{ DoorArr.ipAddress }}</text>
+      <text class="cornerText cornerSn" v-if="DoorArr.deviceSerial">SN:{{ DoorArr.deviceSerial }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import oaPassBody from "@/components/oa-passBody/index.vue";
+import {
+  navigateByKey,
+  cameraFunc,
+  onDoorPass,
+  initDoorData,
+  IntervalFunc,
+  clockFunc,
+  FaceVerifyFunc,
+  doorPageUi,
+  syncDeviceSerial,
+  DoorArr,
+} from "./index.js";
+import FaceVerifyPopup from "./components/face-verify-popup.nvue";
+
+const domModule = weex.requireModule('dom');
+domModule .addRule('fontFace', {
+    fontFamily: 'iconfont',
+    src: "url('https://at.alicdn.com/t/c/font_4400427_u9biyws6c8.ttf?t=1776693398678')"
+});
+
+export default {
+  components: {
+    oaPassBody,
+    FaceVerifyPopup,
+  },
+  data() {
+    return {
+      cameraFunc,
+      onDoorPass,
+      FaceVerifyFunc,
+      doorPageUi,
+      DoorArr,
+      popup: {
+        show: false,
+      },
+      faceVerifyPopup: {
+        show: false,
+        success: false,
+        faceName: "",
+        failMessage: "",
+        avatarSrc: "",
+      },
+      defaultBg: "/static/face/img/face_bg.png",
+      thresholds: {
+        strongThreshold: 0.73, // 强阈值:高置信通过
+        weakThreshold: 0.68, // 弱阈值:疑似通过下限
+        minGap: 0.04, // 最小间隔:结果节流
+        analyzeIntervalMs: 500, // 分析间隔(ms):降低占用
+        showFaceBox: true, // 显示人脸框:绘制检测框
+        stopOnSuccess: false, // 成功即停止:命中后停相机
+        previewShape: "circle", // circle / square:原生预览裁剪形状
+        overlayEnabled: true, // 原生叠加层开关(覆盖在相机预览上)
+        overlayStyle: "gif", // gif / ring / none:叠加层样式
+        overlayGifPath: "auto",
+        ttsEnabled: true, // 语音播报总开关(原生)
+        ttsVolume: 100, // 语音播报音量:1-100
+      },
+      state: {
+        dateTime: "",
+        date: "",
+        doorName: "",
+        imgPath: "",
+        openMode: "人脸,点击开门,密码",
+        remark: "",
+        workName: "",
+        workClass: "",
+      },
+      icons: {
+        openDoor: "\ue62c",
+        password: "\ue8b2",
+        setting: "\ue626",
+      },
+    };
+  },
+  onReady() {
+    syncDeviceSerial();
+    clockFunc.start(this);
+    initDoorData(this);
+    IntervalFunc.open(this, "ipAddress");
+    IntervalFunc.open(this, "door");
+    this.$nextTick(() => {
+      cameraFunc.start(this);
+    });
+  },
+  onShow() {
+    syncDeviceSerial();
+    clockFunc.start(this);
+    IntervalFunc.open(this, "ipAddress");
+    IntervalFunc.open(this, "door");
+    this.$nextTick(() => {
+      cameraFunc.start(this);
+    });
+  },
+  onHide() {
+    FaceVerifyFunc.close(this);
+    clockFunc.stop(this);
+    IntervalFunc.clear(this);
+    cameraFunc.stop(this);
+  },
+  methods: {
+    navigateByKey,
+  },
+};
+</script>
+
+<style scoped>
+.pageRoot {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: #0b1020;
+}
+.bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+.homeCard {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  flex-direction: column;
+  justify-content: space-between;
+  padding: 24px;
+}
+.homeTop {
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding: 10px 10px;
+}
+.dateArea {
+  flex-direction: row;
+  align-items: flex-end;
+}
+.time1 {
+  color: #ffffff;
+  font-size: 50px;
+  font-weight: 700;
+  margin-right: 15px;
+}
+.time2 {
+  color: rgba(255, 255, 255, 0.9);
+  font-size: 22px;
+  padding-bottom: 8px;
+}
+.doorTitle {
+  color: rgba(255, 255, 255, 0.92);
+  font-size: 25px;
+  font-weight: 600;
+  padding-top: 20px;
+}
+.homeCenter {
+  flex: 1;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+.centerInfo {
+  flex-direction: column;
+  align-items: center;
+}
+.infoTitle {
+  color: #fff;
+  font-size: 40px;
+  font-weight: 700;
+  margin-bottom: 30px;
+}
+.remark {
+  color: rgba(255, 255, 255, 0.88);
+  font-size: 25px;
+  text-align: center;
+  margin-bottom: 30px;
+}
+.workStatus {
+  color: #fff;
+  font-size: 28px;
+  padding: 10px 26px;
+  border-radius: 14px;
+}
+.workRed {
+  background-color: #f11e16;
+}
+.workYellow {
+  background-color: #e6a23c;
+}
+.workGreen {
+  background-color: #67c23a;
+}
+.faceArea {
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  margin-top: -180px;
+}
+.faceRing {
+  position: relative;
+  width: 300px;
+  height: 300px;
+  overflow: hidden;
+}
+.camera {
+  width: 300px;
+  height: 300px;
+  border-radius: 150px;
+}
+.homeFooter {
+  flex-direction: row;
+  justify-content: center;
+  padding: 18px 22px;
+}
+.btnArea {
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+}
+.iconfont {
+  font-family: iconfont;
+  font-size: 55px;
+  color: #fff;
+}
+.iconfont+.iconfont{
+  margin-left: 26rpx;
+}
+
+.passwordLayer {
+  position: fixed;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background-color: #474747;
+  z-index: 100074;
+}
+
+.cornerMeta {
+  position: absolute;
+  left: 18px;
+  bottom: 14px;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: nowrap;
+}
+
+.cornerText {
+  font-size: 18px;
+  color: rgba(255, 255, 255, 0.25);
+}
+
+.cornerIp {
+  margin-right: 14px;
+}
+</style>

+ 1 - 0
src/pages/door/index.vue → src/pages/door/index4.vue

@@ -42,6 +42,7 @@ const { version, webviewStyles } = toRefs(state);
 
 // 初始化
 function init() {
+  controlStore.openInterval("ipAddress"); //定时获取IP地址
   controlStore.initCamera(); //初始化摄像头
   // controlStore.initNfc();//初始化NFC
   controlStore.initData(); //初始化数据

+ 2 - 1
src/pages/door/setting/index.scss

@@ -1,3 +1,4 @@
+.faceStorage-container,
 .doorSetting-container,
 .doorSettingOther-container,
 .doorSettingServe-container,
@@ -70,7 +71,7 @@
 }
 
 @media (min-width: 500px) {
-
+    .faceStorage-container,
     .doorSetting-container,
     .doorSettingOther-container,
     .doorSettingServe-container,

+ 9 - 22
src/pages/door/setting/index.vue

@@ -19,17 +19,9 @@
       <text class="iconfont oaIcon-exit" @click="handleExit()"></text>
 
       <view class="Grid">
-        <view class="Grid-item" @click="handle('serve')">
-          <image class="Grid-item-image" src="@/static/face/img/serveSetting.png"></image>
-          <view class="Grid-item-lable">服务器配置</view>
-        </view>
-        <view class="Grid-item" @click="handle('other')">
-          <image class="Grid-item-image" src="@/static/face/img/otherSetting.png"></image>
-          <view class="Grid-item-lable">门禁配置</view>
-        </view>
         <view class="Grid-item" @click="handle('sysetm')">
           <image class="Grid-item-image" src="@/static/face/img/sysetmSetting.png"></image>
-          <view class="Grid-item-lable">更多配置</view>
+          <view class="Grid-item-lable">系统配置</view>
         </view>
         <view class="Grid-item" @click="handle('public')">
           <image class="Grid-item-image" src="@/static/face/img/sysetmSetting.png"></image>
@@ -39,6 +31,10 @@
           <image class="Grid-item-image" src="@/static/face/img/sysetmSetting.png"></image>
           <view class="Grid-item-lable">测试</view>
         </view>
+        <view class="Grid-item" @click="handle('faceStorage')">
+          <image class="Grid-item-image" src="@/static/face/img/sysetmSetting.png"></image>
+          <view class="Grid-item-lable">人脸数据管理</view>
+        </view>
       </view>
     </template>
   </oa-scroll>
@@ -61,18 +57,12 @@ const state = reactive({});
 const {} = toRefs(state);
 
 function handle(type) {
-  if (type == "serve") {
-    proxy.$tab.navigateTo("/pages/door/setting/serve/index");
-  } else if (type == "other") {
-    if (!controlStore.form.linkUrl) {
-      proxy.$modal.msg("请先配置服务器信息!");
-      return;
-    }
-    proxy.$tab.navigateTo("/pages/door/setting/other/index");
-  } else if (type == "public") {
+  if (type == "public") {
     proxy.$tab.navigateTo("/pages/door/setting/public/index");
   } else if (type == "sysetm") {
     proxy.$tab.navigateTo("/pages/door/setting/system/index");
+  } else if (type == "faceStorage") {
+    proxy.$tab.navigateTo("/pages/door/faceStorage/index");
   }
 }
 
@@ -90,12 +80,9 @@ function startActivity() {
 }
 
 onLoad((options) => {
-  controlStore.initData(); //初始化数据
 });
 
-onShow(() => {
-  controlStore.getDoorList();
-});
+onShow(() => {});
 </script>
 <style lang="scss">
 @import "./index.scss";

+ 0 - 96
src/pages/door/setting/other/index.vue

@@ -1,96 +0,0 @@
-<template>
-  <oa-scroll
-    customClass="doorSettingOther-container scroll-height"
-    :customStyle="{
-      //#ifdef APP-PLUS || MP-WEIXIN
-      height: `calc(100vh - (0px))`,
-      //#endif
-      //#ifdef H5
-      height: `calc(100vh - (0px))`,
-      //#endif
-    }"
-    :refresherLoad="false"
-    :refresherEnabled="false"
-    :refresherDefaultStyle="'none'"
-    :refresherBackground="'#f5f6f7'"
-    :data-theme="'theme-' + proxy.$settingStore.themeColor.name"
-  >
-    <template #default>
-      <text class="iconfont oaIcon-left" @click="handleExit()"></text>
-
-      <view class="mb10">绑定门禁</view>
-      <view class="flex">
-        <uni-data-select
-          v-model="controlStore.form.door.id"
-          :localdata="controlStore.doorList"
-          placeholder="门禁(必选)"
-          mode="none"
-          :clear="false"
-          @change="(e) => controlStore.handleSelectChange({ value: e, type: '绑定门禁' })"
-        >
-        </uni-data-select>
-      </view>
-    </template>
-  </oa-scroll>
-</template>
-<script setup>
-/*----------------------------------依赖引入-----------------------------------*/
-import { onLoad, onShow, onReady, onHide, onLaunch, onUnload, onNavigationBarButtonTap, onPageScroll } from "@dcloudio/uni-app";
-import { ref, reactive, computed, getCurrentInstance, toRefs, inject, nextTick, watch } from "vue";
-/*----------------------------------接口引入-----------------------------------*/
-/*----------------------------------组件引入-----------------------------------*/
-/*----------------------------------store引入-----------------------------------*/
-import { controlStores } from "@/store/modules/index";
-/*----------------------------------公共方法引入-----------------------------------*/
-const { proxy } = getCurrentInstance();
-const controlStore = controlStores();
-/*----------------------------------公共变量-----------------------------------*/
-const state = reactive({});
-const {} = toRefs(state);
-
-function handleExit() {
-  if (controlStore.isDataChange) {
-    proxy.$modal.loading("保存中");
-    uni.setStorageSync("storage_face", controlStore.form);
-    controlStore.initData(); //初始化数据
-    setTimeout(() => {
-      proxy.$tab.navigateBack(1);
-      proxy.$modal.closeLoading();
-    }, 1000);
-  } else {
-    proxy.$tab.navigateBack(1);
-  }
-}
-
-onLoad((options) => {
-  controlStore.initData(); //初始化数据
-});
-
-onShow(() => {
-  controlStore.getDoorList();
-});
-</script>
-<style lang="scss">
-@import "../index.scss";
-
-:deep() {
-  .uni-stat-box {
-    background-color: transparent !important;
-    color: black !important;
-
-    .uni-select {
-      padding-left: 0px !important;
-      padding-right: 0px !important;
-
-      &__input-text {
-        color: #ffffff !important;
-      }
-
-      &__input-placeholder {
-        font-size: 15px !important;
-        color: #c0c4cc !important;
-      }
-    }
-  }
-}
-</style>

+ 2 - 41
src/pages/door/setting/public/index.vue

@@ -17,56 +17,17 @@
   >
     <template #default>
       <text class="iconfont oaIcon-left" @click="handleExit()"></text>
-      <u-cell-group>
-        <!-- #ifdef APP-PLUS -->
-        <u-cell title="WiFi管理" :value="1">
-          <template #value> <view class="u-cell__value">点击设置</view> <u-icon class="iconfont" name="arrow-right"></u-icon> </template>
-        </u-cell>
-        <u-cell title="IP地址">
-          <template #value> </template>
-        </u-cell>
-        <!-- #endif -->
-      </u-cell-group>
     </template>
   </oa-scroll>
-
-  <oa-upgrade ref="oaUpgradeRef" :themesColor="proxy.$settingStore.themeColor.color" />
 </template>
 <script setup>
-/*----------------------------------依赖引入-----------------------------------*/
-import config from "@/config";
-import { onLoad, onShow, onReady, onHide, onLaunch, onUnload, onNavigationBarButtonTap, onPageScroll } from "@dcloudio/uni-app";
-import { ref, reactive, computed, getCurrentInstance, toRefs, inject, nextTick, watch } from "vue";
-/*----------------------------------接口引入-----------------------------------*/
-/*----------------------------------组件引入-----------------------------------*/
-/*----------------------------------store引入-----------------------------------*/
-import { controlStores, commonStores } from "@/store/modules/index";
-/*----------------------------------公共方法引入-----------------------------------*/
-const { proxy } = getCurrentInstance();
-const controlStore = controlStores();
-const commonStore = commonStores();
-/*----------------------------------公共变量-----------------------------------*/
-const state = reactive({
-  version: computed(() => {
-    return config.appInfo.version;
-  }),
-});
-const { version } = toRefs(state);
+import { getCurrentInstance } from "vue";
 
-// 按钮点击事件
-function handleToSetting(type) {
-  if (type == "senior") {
-    proxy.$tab.navigateTo("/pages/door/setting/system/senior");
-  }
-}
+const { proxy } = getCurrentInstance();
 
 function handleExit() {
   proxy.$tab.navigateBack(1);
 }
-
-onLoad((options) => {
-  controlStore.initData(); //初始化数据
-});
 </script>
 <style lang="scss" scoped>
 @import "../index.scss";

+ 0 - 71
src/pages/door/setting/serve/index.vue

@@ -1,71 +0,0 @@
-<template>
-  <oa-scroll
-    customClass="doorSettingServe-container scroll-height"
-    :customStyle="{
-      //#ifdef APP-PLUS || MP-WEIXIN
-      height: `calc(100vh - (0px))`,
-      //#endif
-      //#ifdef H5
-      height: `calc(100vh - (0px))`,
-      //#endif
-    }"
-    :refresherLoad="false"
-    :refresherEnabled="false"
-    :refresherDefaultStyle="'none'"
-    :refresherBackground="'#f5f6f7'"
-    :data-theme="'theme-' + proxy.$settingStore.themeColor.name"
-  >
-    <template #default>
-      <text class="iconfont oaIcon-left" @click="handleExit()"></text>
-
-      <view class="mb10 required">服务器地址</view>
-      <view class="mb20">
-        <u-input v-model="controlStore.form.linkUrl" placeholder="服务器地址(必填)" border="bottom" color="white" @change="handleChange" style="padding: 6px 0px" />
-      </view>
-
-      <view class="mb10">服务器端口</view>
-      <view class="mb20">
-        <u-input v-model="controlStore.form.port" placeholder="服务器端口(非必填)" border="bottom" color="white" @change="handleChange" style="padding: 6px 0px" />
-      </view>
-    </template>
-  </oa-scroll>
-</template>
-<script setup>
-/*----------------------------------依赖引入-----------------------------------*/
-import { onLoad, onShow, onReady, onHide, onLaunch, onUnload, onNavigationBarButtonTap, onPageScroll } from "@dcloudio/uni-app";
-import { ref, reactive, computed, getCurrentInstance, toRefs, inject, nextTick, watch } from "vue";
-/*----------------------------------接口引入-----------------------------------*/
-/*----------------------------------组件引入-----------------------------------*/
-/*----------------------------------store引入-----------------------------------*/
-import { controlStores } from "@/store/modules/index";
-/*----------------------------------公共方法引入-----------------------------------*/
-const { proxy } = getCurrentInstance();
-const controlStore = controlStores();
-/*----------------------------------公共变量-----------------------------------*/
-const state = reactive({});
-const {} = toRefs(state);
-
-function handleChange(e) {
-  controlStore.isDataChange = true;
-}
-
-function handleExit() {
-  if (controlStore.isDataChange) {
-    proxy.$modal.loading("保存中");
-    controlStore.serveChange();
-    setTimeout(() => {
-      proxy.$tab.navigateBack(1);
-      proxy.$modal.closeLoading();
-    }, 1000);
-  } else {
-    proxy.$tab.navigateBack(1);
-  }
-}
-
-onLoad((options) => {
-  controlStore.initData(); //初始化数据
-});
-</script>
-<style lang="scss">
-@import "../index.scss";
-</style>

+ 54 - 10
src/pages/door/setting/system/index.vue

@@ -21,34 +21,77 @@
         <!-- #ifdef APP-PLUS || H5 -->
         <u-cell title="软件版本号" :value="version"></u-cell>
         <u-cell title="清除缓存" @click="proxy.$setting.clearCache()">
-          <template #value> <view class="iconfont oaIcon-qingchu"></view> </template>
+          <template #value>
+            <view class="iconfont oaIcon-qingchu"></view>
+          </template>
         </u-cell>
         <u-cell title="检查更新" @click="handleToUpgrade()">
-          <template #value> <view class="iconfont oaIcon-jianchagengxin"></view> </template>
+          <template #value>
+            <view class="iconfont oaIcon-jianchagengxin"></view>
+          </template>
         </u-cell>
         <!-- #endif -->
         <!-- #ifdef APP-PLUS -->
         <u-cell title="设备型号" :value="proxy.$sys.getModel()"></u-cell>
         <u-cell title="系统版本" :value="proxy.$sys.getVersion()"></u-cell>
         <u-cell title="设备序列号" :value="proxy.$sys.getSerial()"></u-cell>
+        <u-cell title="网络" @click="handleToSetting('network')">
+          <template #value>
+            <view class="u-cell__value">点击设置</view>
+            <u-icon class="iconfont" name="arrow-right"></u-icon>
+          </template>
+        </u-cell>
         <u-cell title="高级" @click="handleToSetting('senior')">
-          <template #value> <view class="u-cell__value">点击设置</view> <u-icon class="iconfont" name="arrow-right"></u-icon> </template>
+          <template #value>
+            <view class="u-cell__value">点击设置</view>
+            <u-icon class="iconfont" name="arrow-right"></u-icon>
+          </template>
         </u-cell>
-        <u-cell title="设备重启" @click="proxy.$ph.reboot()">
-          <template #value> <view class="iconfont oaIcon-zhongqi1"></view> </template>
+        <u-cell
+          title="设备重启"
+          @click="
+            proxy.$modal.confirm('是否重启设备?').then((e) => {
+              proxy.$sys.reboot();
+            })
+          "
+        >
+          <template #value>
+            <view class="iconfont oaIcon-zhongqi1"></view>
+          </template>
         </u-cell>
         <!-- #endif -->
       </u-cell-group>
     </template>
   </oa-scroll>
 
-  <oa-upgrade ref="oaUpgradeRef" :themesColor="proxy.$settingStore.themeColor.color" />
+  <oa-upgrade
+    ref="oaUpgradeRef"
+    :themesColor="proxy.$settingStore.themeColor.color"
+  />
 </template>
 <script setup>
 /*----------------------------------依赖引入-----------------------------------*/
 import config from "@/config";
-import { onLoad, onShow, onReady, onHide, onLaunch, onUnload, onNavigationBarButtonTap, onPageScroll } from "@dcloudio/uni-app";
-import { ref, reactive, computed, getCurrentInstance, toRefs, inject, nextTick, watch } from "vue";
+import {
+  onLoad,
+  onShow,
+  onReady,
+  onHide,
+  onLaunch,
+  onUnload,
+  onNavigationBarButtonTap,
+  onPageScroll,
+} from "@dcloudio/uni-app";
+import {
+  ref,
+  reactive,
+  computed,
+  getCurrentInstance,
+  toRefs,
+  inject,
+  nextTick,
+  watch,
+} from "vue";
 /*----------------------------------接口引入-----------------------------------*/
 /*----------------------------------组件引入-----------------------------------*/
 /*----------------------------------store引入-----------------------------------*/
@@ -80,7 +123,9 @@ function handleToUpgrade() {
 
 // 按钮点击事件
 function handleToSetting(type) {
-  if (type == "senior") {
+  if (type == "network") {
+    proxy.$tab.navigateTo("/pages/door/setting/system/network");
+  } else if (type == "senior") {
     proxy.$tab.navigateTo("/pages/door/setting/system/senior");
   }
 }
@@ -90,7 +135,6 @@ function handleExit() {
 }
 
 onLoad((options) => {
-  controlStore.initData(); //初始化数据
 });
 </script>
 <style lang="scss" scoped>

+ 76 - 0
src/pages/door/setting/system/network.vue

@@ -0,0 +1,76 @@
+<template>
+  <oa-scroll
+    customClass="doorSettingSystem-container scroll-height"
+    :customStyle="{
+      //#ifdef APP-PLUS || MP-WEIXIN
+      height: `calc(100vh - (0px))`,
+      //#endif
+      //#ifdef H5
+      height: `calc(100vh - (0px))`,
+      //#endif
+    }"
+    :refresherLoad="false"
+    :refresherEnabled="false"
+    :refresherDefaultStyle="'none'"
+    :refresherBackground="'#f5f6f7'"
+    :data-theme="'theme-' + proxy.$settingStore.themeColor.name"
+  >
+    <template #default>
+      <text class="iconfont oaIcon-left" @click="handleExit()"></text>
+      <u-cell-group>
+        <!-- #ifdef APP-PLUS -->
+        <u-cell title="网络设置" :value="1">
+          <template #value>
+            <view class="u-cell__value" @click="proxy.$settingStore.openSystemNetwork()">点击设置</view>
+            <u-icon class="iconfont" name="arrow-right"></u-icon>
+          </template>
+        </u-cell>
+        <u-cell title="IP地址" :value="ipAddress || '-'"></u-cell>
+        <!-- #endif -->
+      </u-cell-group>
+    </template>
+  </oa-scroll>
+
+  <oa-upgrade ref="oaUpgradeRef" :themesColor="proxy.$settingStore.themeColor.color" />
+</template>
+<script setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { onLoad, onShow } from "@dcloudio/uni-app";
+import { reactive, getCurrentInstance, toRefs } from "vue";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------组件引入-----------------------------------*/
+/*----------------------------------store引入-----------------------------------*/
+/*----------------------------------公共方法引入-----------------------------------*/
+const { proxy } = getCurrentInstance();
+/*----------------------------------公共变量-----------------------------------*/
+const state = reactive({
+  ipAddress: "",
+});
+const { ipAddress } = toRefs(state);
+
+function refreshIpAddress() {
+  proxy.$sys.getIpAddress({
+    success: (res) => {
+      state.ipAddress = res;
+    },
+    error: () => {
+      state.ipAddress = "";
+    },
+  });
+}
+
+function handleExit() {
+  proxy.$tab.navigateBack(1);
+}
+
+onLoad(() => {
+  refreshIpAddress();
+});
+
+onShow(() => {
+  refreshIpAddress();
+});
+</script>
+<style lang="scss" scoped>
+@import "../index.scss";
+</style>

+ 17 - 7
src/pages/door/setting/system/senior.vue

@@ -21,22 +21,22 @@
         <u-cell title="测试内置硬件" @click="text()"></u-cell>
         <u-cell title="继电器">
           <template #value>
-            <u-switch v-model="commonStore.doorArray.systemSeeting.relayControl" size="15" @change="proxy.$ph.relayControl"></u-switch>
+            <u-switch v-model="commonStore.doorArray.systemSeeting.relayControl" size="15" @change="proxy.$sys.relayControl"></u-switch>
           </template>
         </u-cell>
         <u-cell title="隐藏导航栏">
           <template #value>
-            <u-switch v-model="commonStore.doorArray.systemSeeting.navBarNew" size="15" @change="proxy.$ph.navigationBar"></u-switch>
+            <u-switch v-model="commonStore.doorArray.systemSeeting.navBarNew" size="15" @change="proxy.$sys.navigationBar"></u-switch>
           </template>
         </u-cell>
         <u-cell title="隐藏状态栏">
           <template #value>
-            <u-switch v-model="commonStore.doorArray.systemSeeting.statusBar" size="15" @change="proxy.$ph.statusBar"></u-switch>
+            <u-switch v-model="commonStore.doorArray.systemSeeting.statusBar" size="15" @change="proxy.$sys.statusBar('set', $event)"></u-switch>
           </template>
         </u-cell>
         <u-cell title="禁止状态栏下拉">
           <template #value>
-            <u-switch v-model="commonStore.doorArray.systemSeeting.statusBarDrop" size="15" @change="proxy.$ph.statusBarDrop"></u-switch>
+            <u-switch v-model="commonStore.doorArray.systemSeeting.statusBarDrop" size="15" @change="proxy.$sys.statusBarDrop"></u-switch>
           </template>
         </u-cell>
         <u-cell title="退出应用" @click="Cancel()">
@@ -49,7 +49,7 @@
 <script setup>
 /*----------------------------------依赖引入-----------------------------------*/
 import config from "@/config";
-import { onLoad, onShow, onReady, onHide, onLaunch, onUnload, onNavigationBarButtonTap, onPageScroll } from "@dcloudio/uni-app";
+import { onLoad, onShow } from "@dcloudio/uni-app";
 import { ref, reactive, computed, getCurrentInstance, toRefs, inject, nextTick, watch } from "vue";
 /*----------------------------------接口引入-----------------------------------*/
 /*----------------------------------组件引入-----------------------------------*/
@@ -83,8 +83,18 @@ function Cancel() {
   //#endif
 }
 
-onLoad((options) => {
-  controlStore.initData(); //初始化数据
+/** 从系统读取状态栏是否可见,同步「隐藏状态栏」开关(可见则关隐藏,不可见则开隐藏) */
+function syncStatusBarSwitchFromDevice() {
+  //#ifdef APP-PLUS
+  const visible = proxy.$sys.statusBar("get");
+  if (typeof visible === "boolean") {
+    commonStore.doorArray.systemSeeting.statusBar = !visible;
+  }
+  //#endif
+}
+
+onShow(() => {
+  syncStatusBarSwitchFromDevice();
 });
 </script>
 <style lang="scss" scoped>

+ 2 - 4
src/pages/meeting/index.vue

@@ -3,7 +3,6 @@
 </template>
 <script setup>
 /*----------------------------------依赖引入-----------------------------------*/
-import config from "@/config";
 import { onLoad, onShow, onReady, onHide, onLaunch, onUnload, onNavigationBarButtonTap, onPageScroll } from "@dcloudio/uni-app";
 import { ref, reactive, computed, getCurrentInstance, toRefs, inject, nextTick, watch } from "vue";
 /*----------------------------------接口引入-----------------------------------*/
@@ -25,13 +24,12 @@ const { webviewStyles } = toRefs(state);
 // 初始化
 function init() {
   controlStore.openInterval("ipAddress"); //定时获取IP地址
-  controlStore.pageFunction = ["会议"];
   controlStore.initCamera(); //初始化摄像头
-  // controlStore.initNfc();
+  controlStore.initNfc(); //初始化NFC
   controlStore.initData(); //初始化数据
   controlStore.openInterval("meeting"); //定时获取会议信息
 
-  controlStore.doorControl(false); //关闭门禁
+  proxy.$sys.relayControl(false); // 关闭门禁(继电器)
 }
 
 /**

+ 12 - 2
src/pages/meeting/setting/index.vue

@@ -25,7 +25,7 @@
         <u-cell-group>
           <u-cell title="门禁开关">
             <template #value>
-              <u-switch v-model="setting.doorStatus" size="15" @change="controlStore.doorControl"></u-switch>
+              <u-switch v-model="setting.doorStatus" size="15" @change="proxy.$sys.relayControl"></u-switch>
             </template>
           </u-cell>
           <u-cell title="导航栏显示">
@@ -33,6 +33,8 @@
               <u-switch v-model="setting.navBarNew" size="15" @change="navBarNewChange"></u-switch>
             </template>
           </u-cell>
+          <u-cell title="串口通讯" @click="SerialPortPage()"></u-cell>
+          <u-cell title="M11读卡器测试" @click="CardReaderTestPage()"></u-cell>
           <u-cell title="软件版本号" :value="version"></u-cell>
           <u-cell title="IP" :value="controlStore.ipAddress || '-'"></u-cell>
           <u-cell title="设备型号" :value="sysPlugins.getDeviceInfo().model || '-'"></u-cell>
@@ -42,7 +44,7 @@
               <view class="iconfont oaIcon-jianchagengxin menu-item-icon mr2" style="color: #2979ff"></view>
             </template>
           </u-cell>
-          <u-cell title="设备重启" @click="proxy.$yx.rebootNow()">
+          <u-cell title="设备重启" @click="proxy.$modal.confirm('是否重启设备?').then((e) => { proxy.$sys.reboot() })">
             <template #value>
               <u-icon name="reload" color="#2979ff" size="20"></u-icon>
             </template>
@@ -82,6 +84,14 @@ const state = reactive({
 });
 const { version, setting } = toRefs(state);
 
+function SerialPortPage() {
+  proxy.$tab.navigateTo("/pages/meeting/setting/SerialPort");
+}
+
+function CardReaderTestPage() {
+  proxy.$tab.navigateTo("/pages/meeting/setting/CardReaderTest");
+}
+
 /**
  * @退出
  */

+ 1 - 55
src/plugins/device/ph.plugins.js

@@ -1,58 +1,4 @@
-import modal from "../modal.plugins";
-
 /**
  * @海清硬件api
  */
-export default {
-    /**
-     * @继电器控制
-     * @param {状态值} status true/false
-     */
-    relayControl(status) {
-        //#ifdef APP-PLUS
-        const phPlugin = uni.requireNativePlugin("phPlugin");
-        phPlugin.relay_Control(status);
-        //#endif
-    },
-    /**
-     * @导航栏显示隐藏
-     * @param {状态值} status true/false
-     */
-    navigationBar(status) {
-        //#ifdef APP-PLUS
-        const phPlugin = uni.requireNativePlugin("phPlugin");
-        phPlugin.navigationBar(status);
-        //#endif
-    },
-    /**
-     * @状态显示隐藏控制
-     * @param {状态值} status true/false
-     */
-    statusBar(status) {
-        //#ifdef APP-PLUS
-        const phPlugin = uni.requireNativePlugin("phPlugin");
-        phPlugin.statusBar(status);
-        //#endif
-    },
-    /**
-     * @状态面板下拉控制
-     * @param {状态值} status true/false
-     */
-    statusBarDrop(status) {
-        //#ifdef APP-PLUS
-        const phPlugin = uni.requireNativePlugin("phPlugin");
-        phPlugin.statusBarDrop(status);
-        //#endif
-    },
-    /**
-    * @设备重启
-    */
-    reboot() {
-        //#ifdef APP-PLUS
-        modal.confirm("是否重启设备?").then((e) => {
-            const phPlugin = uni.requireNativePlugin("phPlugin");
-            phPlugin.reboot();
-        })
-        //#endif
-    },
-};
+export default {};

+ 106 - 18
src/plugins/device/sys.plugins.js

@@ -10,9 +10,23 @@ export default {
      * @isOpen  开关门(1开、2常闭、3常开)
      */
     openDoor(isOpen, timeout) {
-        // 智能会议
-        if (config.appInfo.appid === "__UNI__F3963F8") {
-            //#ifdef APP-PLUS
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        if (sysPlugin.getManufacturer() == "rockchip") {
+            // 海清设备
+            const phPlugin = uni.requireNativePlugin("phPlugin");
+            if (isOpen == 1) {
+                phPlugin.relay_Control(true);
+                setTimeout(() => {
+                    phPlugin.relay_Control(false);
+                }, timeout);
+            } else if (isOpen == 2) {
+                phPlugin.relay_Control(false);
+            } else if (isOpen == 3) {
+                phPlugin.relay_Control(true);
+            }
+        } else if (sysPlugin.getManufacturer() == "yuxian") {
+            // 武汉智企
             const yxPlugin = uni.requireNativePlugin("yxPlugin");
             if (isOpen == 1) {
                 yxPlugin.setDoor("开");
@@ -24,24 +38,98 @@ export default {
             } else if (isOpen == 3) {
                 yxPlugin.setDoor("开");
             }
-            //#endif
+        } else {
+            return
         }
-        // 智能门禁
-        else if (config.appInfo.appid === "__UNI__8D6E9FD") {
-            //#ifdef APP-PLUS
+        //#endif
+    },
+    /**
+     * @继电器控制
+     * @param {状态值} status true/false
+     */
+    relayControl(status) {
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        if (sysPlugin.getManufacturer() == "rockchip") {
+            // 海清设备
             const phPlugin = uni.requireNativePlugin("phPlugin");
-            if (isOpen == 1) {
-                phPlugin.relay_Control(true);
-                setTimeout(() => {
-                    phPlugin.relay_Control(false);
-                }, timeout);
-            } else if (isOpen == 2) {
-                phPlugin.relay_Control(false);
-            } else if (isOpen == 3) {
-                phPlugin.relay_Control(true);
-            }
-            //#endif
+            phPlugin.relay_Control(status);
+        } else if (sysPlugin.getManufacturer() == "yuxian") {
+            // 武汉智企
+            const yxPlugin = uni.requireNativePlugin("yxPlugin");
+            yxPlugin.setDoor(status == false ? "关" : "开");
+        } else {
+            return
         }
+        //#endif
+    },
+    /**
+     * @导航栏显示隐藏
+     * @param {状态值} status true/false
+     */
+    navigationBar(status) {
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        if (sysPlugin.getManufacturer() == "rockchip") {
+            // 海清设备
+            const phPlugin = uni.requireNativePlugin("phPlugin");
+            phPlugin.navigationBar(status);
+        } else if (sysPlugin.getManufacturer() == "yuxian") {
+            // 武汉智企
+            const yxPlugin = uni.requireNativePlugin("yxPlugin");
+            yxPlugin.setNavBarNew(!status);
+        } else {
+            return
+        }
+        //#endif
+    },
+    /**
+     * @设置状态栏显示隐藏
+     * @param {类型} type set:设置 get:获取
+     * @param {状态值} status true/false
+     */
+    statusBar(type, status) {
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        if (type == "set") {
+            sysPlugin.setStaBar(!status);
+        } else if (type == "get") {
+            return sysPlugin.getStaBarVisible();
+        }
+        //#endif
+    },
+    /**
+     * @状态面板下拉控制
+     * @param {状态值} status true/false
+     */
+    statusBarDrop(status) {
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        if (sysPlugin.getManufacturer() == "rockchip") {
+            // 海清设备
+            const phPlugin = uni.requireNativePlugin("phPlugin");
+            phPlugin.statusBarDrop(status);
+        } else if (sysPlugin.getManufacturer() == "yuxian") {
+            // 武汉智企
+        } else {
+            return
+        }
+        //#endif
+    },
+    /**
+     * @设备重启 
+    */
+    reboot() {
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        if (sysPlugin.getManufacturer() == "rockchip") {
+            const phPlugin = uni.requireNativePlugin("phPlugin");
+            phPlugin.reboot();
+        } else if (sysPlugin.getManufacturer() == "yuxian") {
+            const yxPlugin = uni.requireNativePlugin("yxPlugin");
+            yxPlugin.rebootNow();
+        }
+        //#endif
     },
     /**
      * @获取IP地址

+ 0 - 9
src/plugins/device/yx.plugins.js

@@ -21,15 +21,6 @@ export default {
         yxPlugin.setLed(value);
         //#endif
     },
-    /**
-     * @设备重启 
-    */
-    rebootNow() {
-        //#ifdef APP-PLUS
-        const yxPlugin = uni.requireNativePlugin("yxPlugin");
-        yxPlugin.rebootNow();
-        //#endif
-    },
     /**
      * @隐藏导航栏
      */

+ 28 - 10
src/static/face/door.html

@@ -4,6 +4,18 @@
 <head>
     <meta charset="utf-8">
     <title>人脸识别</title>
+    <!-- 按当前 HTML 所在目录设置 base,避免 web-view 下 ./js 解析到错误路径导致 face.js 未执行 -->
+    <script>
+        (function () {
+            try {
+                var p = window.location.pathname || "";
+                var i = p.lastIndexOf("/");
+                if (i >= 0) {
+                    document.write('<base href="' + p.substring(0, i + 1).replace(/"/g, "") + '">');
+                }
+            } catch (e) {}
+        })();
+    </script>
     <script type="text/javascript" src="./js/tracking.js"></script>
     <script type="text/javascript" src="./js/face_data/face.js"></script>
     <script type="text/javascript" src="./js/face_data/eye.js"></script>
@@ -128,7 +140,7 @@
     <div id="face-container" class="face-container home-card">
         <!-- 背景图片 -->
         <div class="home-card-image"
-            :style="{background: `url(${state.imgPath ? state.imgPath :'img/face_bg.png'}) no-repeat`,backgroundSize: '100% 100%'}">
+            :style="{backgroundImage: `url('${state.imgPath || 'img/face_bg.png'}')`, backgroundSize: '100% 100%', backgroundRepeat: 'no-repeat'}">
         </div>
         <!-- 顶部内容区域 -->
         <div class="home-card-top">
@@ -136,16 +148,16 @@
                 <span class="time1">{{ state.dateTime }}</span>
                 <span class="time2">{{ state.date }}</span>
             </div>
-            <div class="title" v-if="state.openMode.includes('人脸')">{{ state.doorName || '未绑定门禁' }}</div>
+            <div class="title" v-if="state.openMode && state.openMode.includes('人脸')">{{ state.doorName || '未绑定门禁' }}</div>
         </div>
         <!-- 人脸内容区域 -->
         <div class="home-card-center">
-            <div class="date" id="configDialog1" v-if="!state.openMode.includes('人脸')">
+            <div class="date" id="configDialog1" v-if="!state.openMode || !state.openMode.includes('人脸')">
                 <div class="title">{{ state.doorName || '未绑定门禁' }}</div>
                 <div class="remark" v-if="state.remark">{{ state.remark }}</div>
-                <div class="workStatus" :class="state.workClass">{{ state.workName }}</div>
+                <div class="workStatus" :class="state.workClass" v-if="state.workName">{{ state.workName }}</div>
             </div>
-            <div class="face" v-if="state.openMode.includes('人脸')">
+            <div class="face" v-if="state.openMode && state.openMode.includes('人脸')">
                 <!-- height="1564" -->
                 <video id="video" width="300" height="300" style="width:40vh;height:40vh" preload autoplay loop
                     muted></video>
@@ -157,8 +169,8 @@
         <!-- 底部内容区域 -->
         <div class="home-card-footer">
             <div class="btnArea">
-                <i class="iconfont oa-dianji" @click="parentMessage('点击开门')" v-if="state.openMode.includes('点击开门')"></i>
-                <i class="iconfont oa-mima" @click="parentMessage('密码')" v-if="state.openMode.includes('密码')"></i>
+                <i class="iconfont oa-dianji" @click="parentMessage('点击开门')" v-if="state.openMode && state.openMode.includes('点击开门')"></i>
+                <i class="iconfont oa-mima" @click="parentMessage('密码')" v-if="state.openMode && state.openMode.includes('密码')"></i>
             </div>
         </div>
     </div>
@@ -205,10 +217,16 @@
                     that.state.imgPath = event.imgPath
                     that.state.openMode = event.openMode
                     that.state.remark = event.remark
-                    that.state.workName = that.workStatusData.filter(item => item.value == event.workStatus)[0].label;
-                    that.state.workClass = that.workStatusData.filter(item => item.value == event.workStatus)[0].class;
+                    var workStatusItem = that.workStatusData.find(item => item.value == event.workStatus);
+                    if (workStatusItem) {
+                        that.state.workName = workStatusItem.label;
+                        that.state.workClass = workStatusItem.class;
+                    } else {
+                        that.state.workName = '';
+                        that.state.workClass = '';
+                    }
 
-                    if (event.openMode.includes("人脸")) {
+                    if (event.openMode && event.openMode.includes("人脸")) {
                         that.tracker == null ? that.initVido() : undefined
                     } else {
                         that.tracker != null ? that.closeFace() : undefined

+ 0 - 360
src/static/face/door1.html

@@ -1,360 +0,0 @@
-<!doctype html>
-<html>
-
-<head>
-    <meta charset="utf-8">
-    <title>人脸识别</title>
-    <script type="text/javascript" src="./js/tracking.js"></script>
-    <script type="text/javascript" src="./js/face_data/face.js"></script>
-    <script type="text/javascript" src="./js/face_data/eye.js"></script>
-    <script type="text/javascript" src="./js/face_data/mouth.js"></script>
-    <script type="text/javascript" src="./js/jquery-2.2.1.min.js"></script>
-    <!-- VUE3 的 SDK -->
-    <script type="text/javascript" src="./js/vue.global.prod.js"></script>
-    <!-- uni 的 SDK -->
-    <script type="text/javascript" src="./js/uni.webview.1.5.4.js"></script>
-    <!-- 全局区域样式 -->
-    <link rel="stylesheet" href="./css/door_homeCard.css">
-    <!-- 中心区域样式 -->
-    <link rel="stylesheet" href="./css/door_homeCardCenter.css">
-    <!-- 底部区域样式 -->
-    <link rel="stylesheet" href="./css/door_homeCardFooter.css">
-    <style>
-        @media (min-width: 768px) {
-            .home-card-center>.date {
-                font-size: 4rem;
-            }
-
-            .home-card-footer {
-                font-size: 2.5rem !important;
-                margin-bottom: 3rem !important;
-            }
-
-            .home-card-footer .date .time1 {
-                font-size: 3rem !important;
-                margin-right: 2rem !important;
-            }
-
-            .home-card-footer .date .time2 {
-                font-size: 2rem !important;
-            }
-
-            .home-card-footer .date .title {
-                margin-top: 1rem !important;
-            }
-        }
-    </style>
-</head>
-
-<body>
-    <div id="face-container" class="face-container home-card">
-        <!-- 背景图片 -->
-        <div class="home-card-image"
-            :style="{background: `url(${state.imgPath ? state.imgPath :'img/face_bg.png'}) no-repeat`,backgroundSize: '100% 100%'}">
-        </div>
-
-        <!-- 人脸内容区域 -->
-        <div class="home-card-center" id="home-card-center">
-            <div class="date" id="configDialog1" style="display: none;">
-                <div class="title">{{ state.doorName || '未绑定门禁' }}</div>
-            </div>
-            <div class="face">
-                <!-- height="1564" -->
-                <video id="video" width="300" height="300" style="width:40vh;height:40vh" preload autoplay loop
-                    muted></video>
-                <canvas id="myCanvas" width="300" height="300" style="width:40vh;height:40vh"></canvas>
-                <!-- 人脸特效区域 -->
-                <div id="specialEffects" class="specialEffects"></div>
-            </div>
-        </div>
-        <!-- 底部内容区域 -->
-        <div class="home-card-footer">
-            <div class="date" id="configDialog2">
-                <span class="time1">{{ state.dateTime }}</span>
-                <span class="time2">{{ state.date }}</span>
-                <div class="title">{{ state.doorName || '未绑定门禁' }}</div>
-            </div>
-            <div class="qrCode">
-                <!-- <div class="buttom">打开二维码</div> -->
-                <div class="buttom" @click="parentMessage('点击开门')">点击开门</div>
-            </div>
-        </div>
-    </div>
-    <script>
-        // 创建Vue实例
-        Vue.createApp({
-            components: {},
-            emits: [],
-            props: {},
-            data() {
-                return {
-                    flag: true,
-                    time: 1000,
-                    tracker: null,
-                    trackerTask: null,
-                    state: {
-                        date: null,
-                        dateTime: null,
-                        doorName: null,
-                        imgPath: null,
-                        openMode: null,
-                        remark: null,
-                    },
-                    timeOutEvent: 0,
-                    inter: {
-                        dateDom: null
-                    },
-                };
-            },
-            computed: {},
-            methods: {
-                // 初始化数据
-                initData(event) {
-                    var that = this;
-                    that.state.doorName = event.deviceName
-                    that.state.imgPath = event.imgPath
-                    that.state.openMode = event.openMode
-                    that.state.remark = event.remark
-
-                    if (event.openMode.includes("人脸")) {
-                        $('.home-card-center .face').css('display', 'block')
-                        $('.home-card-center .date').css('display', 'none')
-                        $('.home-card-footer .date .title').css('display', 'block')
-                        that.tracker == null ? that.initVido() : undefined
-                    } else {
-                        $('.home-card-center .face').css('display', 'none')
-                        $('.home-card-center .date').css('display', 'block')
-                        $('.home-card-footer .date .title').css('display', 'none')
-                        that.tracker != null ? that.closeFace() : undefined
-                    }
-
-                    if (event.openMode.includes("点击开门")) {
-                        $('.home-card-footer .qrCode').css('display', 'block')
-                    } else {
-                        $('.home-card-footer .qrCode').css('display', 'none')
-                    }
-                },
-                // 初始化事件
-                initHandle() {
-                    var that = this;
-
-                    $("#configDialog1,#configDialog2").on({
-                        touchstart: function (e) {
-                            that.timeOutEvent = setTimeout(() => {
-                                that.longPress()
-                            }, 1000);
-                            e.preventDefault();
-                        },
-                        touchmove: function () {
-                            clearTimeout(that.timeOutEvent);
-                            that.timeOutEvent = 0;
-                        },
-                        touchend: function () {
-                            clearTimeout(that.timeOutEvent);
-                            if (that.timeOutEvent != 0) {
-                                console.log("你这是点击,不是长按");
-                            }
-                            return false;
-                        }
-                    })
-                },
-                // 初始化摄像头
-                initVido() {
-                    var that = this;
-
-                    var video = document.getElementById("video");//视频dom
-                    video.style.transform = 'scaleX(-1)';//视频翻转(1.水平翻转-scaleX(-1) 2.垂直翻转-scaleY(-1))
-                    var canvas = document.getElementById('myCanvas');//画布dom
-                    canvas.style.transform = 'scaleX(-1)';//画布翻转(1.水平翻转-scaleX(-1) 2.垂直翻转-scaleY(-1))
-                    var context = canvas.getContext('2d');
-                    that.tracker = new tracking.ObjectTracker(['face']);//'face', 'eye', 'mouth'
-                    that.tracker.setInitialScale(4); //设置识别的放大比例
-                    that.tracker.setStepSize(2);//设置步长
-                    that.tracker.setEdgesDensity(0.1);//边缘密度
-                    //启动摄像头,并且识别视频内容
-                    that.trackerTask = tracking.track('#video', that.tracker, {
-                        camera: true,
-                    });
-
-                    that.tracker.on('track', function (event) {
-                        // console.log(event.data.length)
-                        if (that.flag) {
-                            // console.log("拍照");
-                            that.state.faceImgState = false;
-                            context.drawImage(video, 0, 0, video.width, video.height);
-                            that.saveAsLocalImage()
-                            // that.capturePartialImage(rect.x, rect.y, rect.width, rect.height);
-                            context.clearRect(0, 0, canvas.width, canvas.height);
-                            that.flag = false;
-                        } else {
-                            //console.log("冷却中");
-                        }
-
-                        if (event.data.length === 0) {
-                            // console.log('未检测到人脸')
-                            context.clearRect(0, 0, canvas.width, canvas.height);
-                        } else if (event.data.length > 1) {
-                            // console.log('检测到多张人脸')
-                            context.clearRect(0, 0, canvas.width, canvas.height);
-                        } else {
-                            context.clearRect(0, 0, canvas.width, canvas.height);
-                            event.data.forEach(function (rect) {
-                                context.strokeStyle = '#409eff';
-                                context.strokeRect(rect.x, rect.y, rect.width, rect.height);
-                                context.fillStyle = "#409eff";
-                                context.lineWidth = 1.5;
-                            });
-                        }
-                    });
-                },
-                // 向父页面推送数据
-                parentMessage(type, data) {
-                    var message = {
-                        funcName: type,
-                        data: data,
-                    };
-
-                    //APP-PLUS
-                    uni.postMessage({
-                        data: message
-                    });
-
-                    //H5
-                    if (window.parent) {
-                        window.parent.postMessage(message, '*');
-                    }
-                },
-                // 当需要抓拍部分画布时
-                capturePartialImage(x, y, width, height) {
-                    // 创建一个新的canvas,用于抓拍部分画布
-                    const canvas = document.getElementById('myCanvas');//画布dom
-                    const croppedCanvas = document.createElement('canvas');
-                    croppedCanvas.width = width;
-                    croppedCanvas.height = height;
-                    const croppedCtx = croppedCanvas.getContext('2d');
-
-                    // 只抓取需要的部分
-                    croppedCtx.drawImage(canvas, x, y, width, height, 0, 0, width, height);
-                    var image = croppedCanvas.toDataURL("image/png")
-                    that.parentMessage('人脸识别', { imageBase: image })
-                },
-                // 获取图片bold
-                saveAsLocalImage() {
-                    var that = this
-                    // var myCanvas = document.getElementById("myCanvas");
-                    // var image = myCanvas.toDataURL("image/png")
-                    // that.parentMessage('人脸识别', { imageBase: image })
-
-                    // 创建一个新的canvas,用于抓拍部分画布
-                    const canvas = document.getElementById('myCanvas');//画布dom
-                    const croppedCanvas = document.createElement('canvas');
-                    croppedCanvas.width = 200;
-                    croppedCanvas.height = 200;
-                    const croppedCtx = croppedCanvas.getContext('2d');
-
-                    // 只抓取需要的部分
-                    croppedCtx.drawImage(canvas, 0, 0, 150, 150);
-                    var image = croppedCanvas.toDataURL("image/png");
-                    that.parentMessage('人脸识别', { imageBase: image })
-                },
-                // 人脸冷却
-                faceCooling() {
-                    var that = this
-                    setTimeout(() => {
-                        that.flag = true
-                        that.state.faceImgState = false;
-                    }, that.time);
-                },
-                // 解析数据
-                analysisData(event) {
-                    console.log(event.funcName)
-                    if ("funcName" in event) {
-                        if (event.funcName == "初始化数据") {
-                            this.initData(JSON.parse(event.data));
-                        } else if (event.funcName == "初始化事件") {
-                            this.initHandle()
-                        } else if (event.funcName == "开启摄像头") {
-                            this.initVido();//调用初始化摄像头
-                        } else if (event.funcName == "关闭摄像头") {
-                            this.closeFace();
-                        } else if (event.funcName == "人脸冷却") {
-                            this.faceCooling();
-                        }
-                    }
-                },
-                // 长按事件
-                longPress() {
-                    this.parentMessage('打开配置')
-                    this.timeOutEvent = 0
-                },
-                // 监听页面是否隐藏
-                handleVisibilityChange() {
-                    if (document.visibilityState === 'visible') {
-                        // 页面变为可见时的处理逻辑
-                        console.log('页面变为可见');
-                        this.tracker == null ? this.initVido() : undefined
-                    } else if (document.visibilityState === 'hidden') {
-                        // 页面变为不可见时的处理逻辑
-                        console.log('页面变为不可见');
-                        this.tracker != null ? this.closeFace() : undefined
-                    }
-                },
-                // 关闭摄像头
-                closeFace() {
-                    try {
-                        this.tracker = null
-                        // 关闭摄像头
-                        let video = document.getElementById('video')
-                        video.srcObject.getTracks()[0].stop()
-                        // 停止侦测
-                        this.trackerTask.stop()
-                    } catch (error) { }
-                },
-                /**
-                * @获取年月日时分
-                * @returns
-                */
-                getFormatterDate(time3) {
-                    var date = new Date(time3);
-                    var Y = date.getFullYear() + "/";
-                    var M = (date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1) + "/";
-                    var D = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + " ";
-
-                    var h = (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":";
-                    var m = (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes())
-                    var strDate = Y + M + D + h + m;
-
-                    return strDate;
-                },
-            },
-            created() {
-                var that = this
-                // APP-PLUS || H5(接收父页面传过来的值)
-                window.receiveData = (msg) => {
-                    that.analysisData(msg)
-                }
-                window.addEventListener("message", function (event) {
-                    that.analysisData(event.data)
-                });
-            },
-            mounted() {
-                document.addEventListener('visibilitychange', this.handleVisibilityChange);
-
-                if (!this.inter.dateDom) {
-                    this.inter.dateDom = setInterval(() => {
-                        this.state.date = this.getFormatterDate(new Date()).split(' ')[0]
-                        this.state.dateTime = this.getFormatterDate(new Date()).split(' ')[1]
-                    }, 1000);
-                }
-            },
-            beforeDestroy() {
-                // 移除window方法
-                window.receiveData = null;
-                clearInterval(inter.dateDom); //销毁之前定时器
-            },
-            watch: {},
-        }).mount('#face-container');
-    </script>
-</body>
-
-</html>

+ 7 - 3
src/static/face/js/meeting-page.js

@@ -121,6 +121,10 @@
                     if (!canvas) return;
                     canvas.style.transform = 'scaleX(-1)'; // 水平翻转
                     var context = canvas.getContext('2d');
+                    if (!tracking || !tracking.ViolaJones || !tracking.ViolaJones.classifiers || !tracking.ViolaJones.classifiers.face) {
+                        console.error('人脸 Haar 分类器未加载,请确认已引入 face-min.js / face.js 且在 tracking 之后');
+                        return;
+                    }
                     that.tracker = new tracking.ObjectTracker('face');
                     that.tracker.setInitialScale(4); // 识别放大比例
                     that.tracker.setStepSize(2); // 步长
@@ -252,14 +256,14 @@
                     }
 
                     if (this.state.meetingTemplate && event.id == this.state.meetingTemplate.id) {
-                        this.state.meetingTemplate = event
                         return;
                     } else {
                         this.state.meetingTemplate = event
+                        this.initVido(); // 调用初始化摄像头
                     }
 
-                    var s1Con = this.state.meetingTemplate.s1Con ? JSON.parse(this.state.meetingTemplate.s1Con) : []
-                    if (!s1Con) return
+                    var s1Con = this.state.meetingTemplate.s1Con && this.state.meetingTemplate.s1Con.includes("[{") ? JSON.parse(this.state.meetingTemplate.s1Con) : [];
+                    if (s1Con.length <= 0) return
 
                     var stage = document.getElementById(options.containerId);
                     if (!stage) return

+ 11 - 2
src/static/face/meeting.html

@@ -4,6 +4,17 @@
 <head>
     <meta charset="utf-8">
     <title>人脸识别</title>
+    <script>
+        (function () {
+            try {
+                var p = window.location.pathname || "";
+                var i = p.lastIndexOf("/");
+                if (i >= 0) {
+                    document.write('<base href="' + p.substring(0, i + 1).replace(/"/g, "") + '">');
+                }
+            } catch (e) {}
+        })();
+    </script>
     <script type="text/javascript" src="./js/tracking-min.js"></script>
     <script type="text/javascript" src="./js/face_data/face-min.js"></script>
     <script type="text/javascript" src="./js/jquery-2.2.1.min.js"></script>
@@ -240,8 +251,6 @@
                     <span style="padding: 0px 5px; border-right: 1px #fff solid">PM2.5:10μg/m3</span>
                     <span style="padding: 0 0 0 5px">甲醛:0ppm</span>
                 </div>
-                <div class="home-card-right-header-title">{{state.thisVenueData ? "&nbsp;&nbsp;会议进行中" :
-                    "&nbsp;"}}</div>
             </div>
             <div class="home-card-right-content">
                 <div style="font-size: 20px; margin-bottom: 15px;">{{state.thisVenueData?.meetingName || "空闲中"}}

+ 11 - 5
src/static/face/meeting_template_preview.html

@@ -4,6 +4,17 @@
 <head>
     <meta charset="utf-8" />
     <title>会议模板预览</title>
+    <script>
+        (function () {
+            try {
+                var p = window.location.pathname || "";
+                var i = p.lastIndexOf("/");
+                if (i >= 0) {
+                    document.write('<base href="' + p.substring(0, i + 1).replace(/"/g, "") + '">');
+                }
+            } catch (e) {}
+        })();
+    </script>
     <script type="text/javascript" src="./js/tracking-min.js"></script>
     <script type="text/javascript" src="./js/face_data/face-min.js"></script>
     <script type="text/javascript" src="./js/jquery-2.2.1.min.js"></script>
@@ -72,9 +83,6 @@
         .image-item {
             position: absolute;
             overflow: hidden;
-            border-radius: 12px;
-            /* border: 2px dotted #c7c7c7; */
-            /* background: #f7f8fa; */
             display: flex;
             align-items: center;
             justify-content: center;
@@ -88,8 +96,6 @@
 
         .carousel {
             position: absolute;
-            /* border: 2px dotted #c7c7c7; */
-            /* border-radius: 12px; */
             overflow: hidden;
             background: #f6f6f6;
             display: flex;

BIN
src/static/iconfont/iconfont.ttf


BIN
src/static/iconfont/iconfont.woff


BIN
src/static/iconfont/iconfont.woff2


+ 80 - 167
src/store/modules/control.js

@@ -1,7 +1,7 @@
 import { defineStore } from "pinia";
-import { doorApi } from "@/api/business/door.js";
+import { doorApi, egDeviceHeartbeat } from "@/api/business/door.js";
 import { faceApi } from "@/api/business/face.js";
-import { deviceApi, meetingApi, signOnOut, escalation } from "@/api/business/meeting.js";
+import { deviceApi, meetingApi, signOnOut, meetingDeviceHeartbeat, } from "@/api/business/meeting.js";
 import { getToken, setToken, removeToken } from "@/utils/auth";
 import dayjs from 'dayjs'
 import config from "@/config";
@@ -18,13 +18,9 @@ import yxPlugins from "@/plugins/device/yx.plugins";
 const controlStore = defineStore("control", {
     state: () => ({
         ipAddress: "",//设备IP地址
-        pageFunction: [], //被包含的功能
         isClicked: false, //按钮是否被点击
         isDataChange: false,//数据是否改变
         form: {
-            linkUrl: "",
-            port: "",
-            domain: undefined,
             door: {
                 id: undefined,
                 name: undefined,
@@ -33,9 +29,6 @@ const controlStore = defineStore("control", {
         popup: {
             show: false,
         },
-        modal: {
-            show: false,
-        },
         doorList: [],
         meetingDoorList: [],//会议门禁列表数据
         meetingRoomList: [],//会议室列表数据
@@ -53,6 +46,7 @@ const controlStore = defineStore("control", {
         inter: {
             doorDom: null,
             meeting: null,
+            egEscalation: null,
             meetingEscalation: null,
             rebootNow: null
         },
@@ -73,13 +67,9 @@ const controlStore = defineStore("control", {
             }
             // 智能门禁
             else if (config.appInfo.appid === "__UNI__8D6E9FD") {
-                config.baseUrl = "http://" + storage.domain + "/prod-api";
-                that.form.domain = storage.domain;
-                that.form.linkUrl = storage.linkUrl.indexOf(":") != -1 ? storage.linkUrl.split(":")[0] : storage.linkUrl;
-                that.form.port = storage.port ? storage.port : "";
                 that.form.door = {
-                    id: storage.door.id || undefined,
-                    name: storage.door.name || undefined,
+                    id: storage.door?.id || undefined,
+                    name: storage.door?.name || undefined,
                 }
             }
         },
@@ -103,34 +93,14 @@ const controlStore = defineStore("control", {
             var that = this
             //#ifdef APP-PLUS
             nfc.initNFC();
-            nfc.readNFC().then((e) => {
-                that.openDoor();
-                that.initNfc();
-            });
-            //#endif
-        },
-        /**
-         * @服务器配置保存事件
-         */
-        serveChange() {
-            if (this.form.linkUrl) {
-                if (!/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}(?:\.[a-zA-Z0-9]{2,})+$/.test(this.form.linkUrl)) {
-                    modal.msg("请输入正确的链接地址");
-                    return;
-                }
+            // nfc.readNFC().then((e) => {
+            //     console.log(e)
 
-                var domain = "";
-                if (this.form.linkUrl) {
-                    domain = this.form.linkUrl;
-                    if (this.form.port) {
-                        domain += ":" + this.form.port;
-                    }
-                }
+            //     that.openDoor();
+            //     that.initNfc();
+            // });
 
-                this.form.domain = domain;
-                config.baseUrl = "http://" + this.form.domain + "/prod-api";
-            }
-            uni.setStorageSync("storage_face", this.form);
+            //#endif
         },
         /**
          * @开启定时任务
@@ -152,17 +122,21 @@ const controlStore = defineStore("control", {
             }
             //门禁
             else if (type == "door") {
-                if (this.inter.doorDom) return;
-                this.getDoorList("updateData");
+                this.initEscalation("eg");//调用初始化会议心跳方法
+                this.inter.egEscalation = setInterval(() => {
+                    this.initEscalation("eg");//调用初始化会议心跳方法
+                }, 1000 * 10)
+
+                this.getDoorList();
                 this.inter.doorDom = setInterval(() => {
-                    this.getDoorList("updateData");
+                    this.getDoorList();
                 }, 1000 * 3);
             }
             // 会议
             else if (type == "meeting") {
-                this.initEscalation();//调用初始化会议心跳方法
+                this.initEscalation("meeting");//调用初始化会议心跳方法
                 this.inter.meetingEscalation = setInterval(() => {
-                    this.initEscalation();//调用初始化会议心跳方法
+                    this.initEscalation("meeting");//调用初始化会议心跳方法
                 }, 1000 * 10)
 
                 this.getMeetingDeviceList()
@@ -183,44 +157,10 @@ const controlStore = defineStore("control", {
                 this.inter.meeting = null;
             }
         },
-        /**
-         * @弹窗事件
-         * @Confirm 确定
-         * @Cancel 退出应用
-         * @Close 关闭
-         */
-        handleModal(type) {
-            if (type == "Confirm") {
-                uni.setStorageSync("storage_face", this.form);
-            } else if (type == 'Cancel') {
-                //#ifdef APP-PLUS
-                keyListen.quitApp();
-                //#endif
-            } else if (type == "Close") {
-
-            }
-
-            this.handleChildren({ funcName: "开启摄像头", data: {} });
-            this.modal.show = false;
-
-        },
-        /**
-         * @Select下拉框回调事件
-         */
-        handleSelectChange(e) {
-            if (e.type === "绑定门禁") {
-                const match = this.doorList.find(item => item.value === e.value);
-                if (match) {
-                    this.form.door.id = match.value;
-                    this.form.door.name = match.name;
-                }
-            }
-            this.isDataChange = true;
-        },
         /**
          * @初始化设备心跳数据
          */
-        initEscalation() {
+        initEscalation(type) {
             if (!sysPlugins.getDeviceInfo().serial) return;
             sysPlugins.getIpAddress({
                 success: (res) => {
@@ -231,10 +171,15 @@ const controlStore = defineStore("control", {
                         "model": sysPlugins.getDeviceInfo().model,
                         "manuFacturer": sysPlugins.getDeviceInfo().manufacturer,
                         "version": sysPlugins.getDeviceInfo().version,
-                        "sdk": sysPlugins.getDeviceInfo().Sdk
+                        "sdk": sysPlugins.getDeviceInfo().Sdk,
+                        "deviceStatus": type == "eg" ? 1 : undefined,
                     }
 
-                    escalation(param)
+                    if (type == "meeting") {
+                        meetingDeviceHeartbeat(param)
+                    } else if (type == "eg") {
+                        egDeviceHeartbeat(param)
+                    }
                 }
             });
         },
@@ -265,7 +210,7 @@ const controlStore = defineStore("control", {
 
             deviceApi()
                 .Select({
-                    deviceCode: sysPlugins.getDeviceInfo().serial || "NTECVSG3PL",
+                    deviceCode: sysPlugins.getDeviceInfo().serial || "",
                     current: 1,
                     size: 10
                 })
@@ -275,15 +220,16 @@ const controlStore = defineStore("control", {
                         this.form.meetingName = requset.data.records[0].meetingRoom.roomName
                         this.meetingTemplateList = requset.data.records[0].meetingTemplate
 
-                        if (this.meetingTemplateList && this.meetingTemplateList?.type == 1) {
-                            this.meetingTemplateSrc = `/static/face/${this.meetingTemplateList.s1Con}`
-                        } else {
-                            this.meetingTemplateSrc = "/static/face/meeting_template_preview.html"
-                            this.handleChildren({
-                                funcName: "初始化模板",
-                                data: JSON.stringify(this.meetingTemplateList || {}),
-                            });
-                        }
+                        // 根据模板类型设置模板路径
+                        this.meetingTemplateSrc = (this.meetingTemplateList && this.meetingTemplateList?.type == 1)
+                            ? `/static/face/${this.meetingTemplateList.s1Con}`
+                            : "/static/face/meeting_template_preview.html";
+
+                        // 初始化模板数据
+                        this.handleChildren({
+                            funcName: "初始化模板",
+                            data: JSON.stringify(this.meetingTemplateList || {}),
+                        });
 
                         this.getMeetingRoomReservationList();
                     } else {
@@ -405,28 +351,27 @@ const controlStore = defineStore("control", {
          */
         faceVerify(imageBase) {
             var that = this
+            console.log(1)
+
             faceApi()
-                .faceVef({
-                    deviceCode: sysPlugins.getDeviceInfo().serial,
-                    imageBase: imageBase,
+                .facePerson({
+                    faceBase: imageBase,
                 })
-                .then((item) => {
-                    if (item.data.code === 200 || item.data.code === 201) {
-                        item.data.userName = item.data.faceName;
-                        if (that.pageFunction.includes('会议')) {
-                            that.meetingVerify(item.data);
-                        } else {
-                            that.openDoor(item.data);
+                .then((res) => {
+                    console.log(res)
+
+                    if ((res.code == '0' || res.code == 200) && res.data.success) {
+                        res.data.userName = res.data.faceName;
+                        if (that.inter.doorDom != null) {
+                            // that.openDoor(res.data);
+                            sysPlugins.openDoor(1, 0);
+                        } else if (that.inter.meeting != null) {
+                            that.meetingVerify(res.data);
                         }
                     }
 
-                    if (item.data.msg != "人脸验证接口返回异常") {
-                        modal.msg(item.data.msg);
-                    }
-
                     that.handleChildren({ funcName: "人脸冷却", data: {} });
-                })
-                .catch((err) => {
+                }).catch((err) => {
                     that.handleChildren({ funcName: "人脸冷却", data: {} });
                 });
         },
@@ -465,48 +410,32 @@ const controlStore = defineStore("control", {
             }).then((item) => { });
         },
         /**
-         * @门禁下拉列表
+         * @门禁列表
          */
-        getDoorList(type) {
+        getDoorList() {
             this.doorList = [];
-
-            if (!this.form.domain) return;
-            if (type == 'updateData' && !this.form.door.id) return;
-
-
             doorApi()
                 .Select({
                     current: 1, //页数
                     size: 2000, //条数
-                    domain: this.form.domain, //域名
-                    deviceUuid: type == 'updateData' ? this.form.door.id : undefined
+                    deviceCode: sysPlugins.getDeviceInfo().serial || "NTECVSG3PL",
                 })
                 .then((requset) => {
                     if (requset.data.records.length <= 0) return;
 
-                    if (type == "updateData") {
-                        let data = requset.data.records[0]
-                        if (!time.isInRange(8, 18)) {
-                            data.openMode = this.removeSegmentWithTarget(data.openMode, "点击开门")
-                        }
-
-                        this.handleChildren({
-                            funcName: "初始化数据",
-                            data: JSON.stringify(data),
-                        });
+                    let data = requset.data.records[0];
 
-                        delete data.id
-                        Object.assign(this.form.door, data)
-                        uni.setStorageSync("storage_face", this.form);
-                    } else {
-                        requset.data.records.forEach((e) => {
-                            this.doorList.push({
-                                value: e.deviceUuid,
-                                name: e.deviceName,
-                                text: e.deviceName,
-                            });
-                        });
+                    if (!time.isInRange(8, 18)) {
+                        data.openMode = this.removeSegmentWithTarget(data.openMode, "点击开门")
                     }
+
+                    this.handleChildren({
+                        funcName: "初始化数据",
+                        data: JSON.stringify(data),
+                    });
+
+                    Object.assign(this.form.door, data)
+                    uni.setStorageSync("storage_face", this.form);
                 });
         },
         /**
@@ -529,52 +458,36 @@ const controlStore = defineStore("control", {
 
             doorApi()
                 .control({
-                    domain: !getToken() ? this.form.domain : undefined, //域名
+                    skipCheck: true,
                     userId: !getToken() ? event.userId : undefined,
                     userName: !getToken() ? event.userName : undefined,
                     productCode: "502_USKY",
-                    deviceUuid: !getToken() ? this.form.door.id : event.deviceUuid,
+                    deviceUuid: this.form.door.deviceUuid,
                     commandCode: "door_onoff",
                     commandValue: 1,
                 })
                 .then((item) => {
-                    modal.msg("开门成功");
+                    if (item.status == "success") {
+                        modal.msg("开门成功");
+                    } else {
+                        modal.msg("开门失败");
+                    }
                     setTimeout(() => {
                         this.isClicked = false;
                     }, 2000);
-                    that.insertDoorRecord(event, "成功");
                 })
                 .catch((err) => {
                     setTimeout(() => {
                         this.isClicked = false;
                     }, 2000);
-                    that.insertDoorRecord(event, "失败:" + err);
-                });
-        },
-        /**
-         * @新增开门记录
-         */
-        insertDoorRecord(event, msg) {
-            doorApi()
-                .RecordInsert({
-                    domain: !getToken() ? this.form.domain : undefined, //域名
-                    userName: event.userName, //用户姓名
-                    deviceUuid: !getToken() ? this.form.door.id : event.deviceUuid,//设备Uuid
-                    passType: 3, //通行方式(1、人脸 2、刷卡 3、手机)
-                    passTime: dayjs().format("YYYY-MM-DDTHH:mm:ss"), //通行时间
-                    passResult: msg, //通行结果
-                })
-                .then((item) => {
-                    console.log(msg);
-                })
-                .catch((err) => {
-                    console.log(err);
                 });
         },
         /**
          * @解析父页面传回的数据
          */
         analysisData(event) {
+            var that = this
+
             if ("funcName" in event) {
                 if (event.funcName == "打开门禁配置") {
                     this.handleChildren({ funcName: "关闭摄像头", data: {} });
@@ -587,8 +500,8 @@ const controlStore = defineStore("control", {
                     this.faceVerify(event.data.imageBase);
                 } else if (event.funcName == "点击开门") {
                     this.openDoor({
-                        userId: 99,
-                        userName: "方惠圣",
+                        userId: "",
+                        userName: "快捷方式开门",
                     });
                 } else if (event.funcName == "密码") {
                     this.handleChildren({ funcName: "关闭摄像头", data: {} });
@@ -675,7 +588,7 @@ const controlStore = defineStore("control", {
 
                 // 比较当前时间是否等于早上6点
                 if (now.getTime() === targetTime.getTime()) {
-                    yxPlugins.rebootNow();
+                    sysPlugins.reboot();
                 }
             }
         },

+ 37 - 0
src/store/modules/setting.js

@@ -214,6 +214,43 @@ const settingStores = defineStore("storage-setting", {
                 }
             }, 0);
         },
+
+        /**
+         * @打开系统网络设置页面
+         */
+        openSystemNetwork() {
+            let main = plus.android.runtimeMainActivity();
+            let Intent = plus.android.importClass("android.content.Intent");
+            let mIntent = new Intent('android.settings.WIRELESS_SETTINGS');
+            main.startActivity(mIntent);
+        },
+        /**
+         * @打开系统WiFi设置页面
+         */
+        openSystemWifi() {
+            uni.getNetworkType({
+                success: function (res) {
+                    const networkType = res.networkType;
+                    if (networkType === 'none') {
+                        // 没有网络,跳转到系统网络设置
+                        uni.showToast({
+                            title: '当前无网络,请检查网络设置',
+                            icon: 'none'
+                        });
+                        let main = plus.android.runtimeMainActivity();
+                        let Intent = plus.android.importClass("android.content.Intent");
+                        let mIntent = new Intent('android.settings.WIFI_SETTINGS');
+                        main.startActivity(mIntent);
+                    } else {
+                        // 有网络,可以继续你的业务逻辑
+                        let main = plus.android.runtimeMainActivity();
+                        let Intent = plus.android.importClass("android.content.Intent");
+                        let mIntent = new Intent('android.settings.WIFI_SETTINGS');
+                        main.startActivity(mIntent);
+                    }
+                }
+            })
+        },
         SET_FINGERPRINT(array) {
             this.fingerprintUserList = array;
             storage.set("fingerprintUserList", array);