Forráskód Böngészése

功能模块优化/冗余代码删除

fanghuisheng 2 napja
szülő
commit
d4a7a9f4dc
68 módosított fájl, 699 hozzáadás és 7013 törlés
  1. BIN
      nativeplugins/FaceAISDKModule/android/faceAISDK-release.aar
  2. 0 9
      nativeplugins/FaceAISDKModule/changelog.md
  3. 0 56
      nativeplugins/FaceAISDKModule/package.json
  4. BIN
      nativeplugins/Fvv-AutoStart/android/autoStart-release.aar
  5. 0 20
      nativeplugins/Fvv-AutoStart/package.json
  6. BIN
      nativeplugins/MyFaceModule/android/myFaceSDK-release.aar
  7. 4 0
      nativeplugins/MyFaceModule/changelog.md
  8. 49 0
      nativeplugins/MyFaceModule/package.json
  9. BIN
      nativeplugins/Sys-Plugin/android/sysPlugin-release.aar
  10. 27 10
      nativeplugins/Sys-Plugin/package.json
  11. 0 1
      package.json
  12. 36 5
      src/App.vue
  13. 2 9
      src/api/business/face.js
  14. 1 1
      src/api/index/index.js
  15. 8 1
      src/components/oa-passBody/index.vue
  16. 0 7
      src/config.js
  17. 3 3
      src/main.js
  18. 46 22
      src/manifest.json
  19. 34 10
      src/pages.json
  20. 0 191
      src/pages/door/components/face-verify-popup.nvue
  21. 0 1116
      src/pages/door/faceStorage/index.vue
  22. 115 64
      src/pages/door/index.js
  23. 23 37
      src/pages/door/index.nvue
  24. 0 124
      src/pages/door/index4.vue
  25. 0 132
      src/pages/door/setting/index.scss
  26. 0 89
      src/pages/door/setting/index.vue
  27. 0 34
      src/pages/door/setting/public/index.vue
  28. 0 142
      src/pages/door/setting/system/index.vue
  29. 0 76
      src/pages/door/setting/system/network.vue
  30. 0 102
      src/pages/door/setting/system/senior.vue
  31. 0 68
      src/pages/meeting/index.vue
  32. 0 168
      src/pages/meeting/setting/index.vue
  33. 0 117
      src/pages/meeting/setting/password.vue
  34. 1 1
      src/permission.js
  35. 0 60
      src/plugins/auth.plugins.js
  36. 19 1
      src/plugins/device/sys.plugins.js
  37. 1 1
      src/plugins/device/yx.plugins.js
  38. 12 7
      src/plugins/index.js
  39. 0 72
      src/plugins/jsencrypt.js
  40. 14 17
      src/plugins/keyListen.plugins.js
  41. 268 243
      src/plugins/nfc.plugins.js
  42. 0 54
      src/static/face/css/door_homeCard.css
  43. 0 61
      src/static/face/css/door_homeCardCenter.css
  44. 0 24
      src/static/face/css/door_homeCardFooter.css
  45. 0 26
      src/static/face/css/door_homeCardTop.css
  46. 0 454
      src/static/face/door.html
  47. BIN
      src/static/face/font/Alibaba-PuHuiTi-Medium.ttf
  48. BIN
      src/static/face/font/BebasNeue.otf
  49. BIN
      src/static/face/img/face.gif
  50. BIN
      src/static/face/img/face_detection.gif
  51. BIN
      src/static/face/img/face_vef.gif
  52. BIN
      src/static/face/img/face_vef.png
  53. BIN
      src/static/face/img/otherSetting.png
  54. BIN
      src/static/face/img/serveSetting.png
  55. 0 7
      src/static/face/js/face_data/eye-min.js
  56. 0 7
      src/static/face/js/face_data/eye.js
  57. 0 7
      src/static/face/js/face_data/face.js
  58. 0 7
      src/static/face/js/face_data/mouth-min.js
  59. 0 7
      src/static/face/js/face_data/mouth.js
  60. 0 3111
      src/static/face/js/tracking.js
  61. 17 2
      src/store/modules/common.js
  62. 5 45
      src/store/modules/control.js
  63. 0 17
      src/utils/dict.js
  64. 0 45
      src/utils/hideHead.js
  65. 0 38
      src/utils/jsencrypt.js
  66. 0 57
      src/utils/jssdk.js
  67. 2 22
      unpackage/config/setting.js
  68. 12 6
      vite.config.js

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


+ 0 - 9
nativeplugins/FaceAISDKModule/changelog.md

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

+ 0 - 56
nativeplugins/FaceAISDKModule/package.json

@@ -1,56 +0,0 @@
-{  
-    "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/Fvv-AutoStart/android/autoStart-release.aar


+ 0 - 20
nativeplugins/Fvv-AutoStart/package.json

@@ -1,20 +0,0 @@
-{
-    "name": "Fvv-AutoStart",
-    "id": "Fvv-AutoStart",
-    "version": "1.0.0",
-    "description": "安卓开机自启动",
-    "_dp_type": "nativeplugin",
-    "_dp_nativeplugin": {
-        "android": {
-            "plugins": [
-                {
-                    "type": "module",
-                    "name": "Fvv-AutoStart",
-                    "class": "uni.fvv.autostart"
-                }
-            ],
-            "integrateType": "aar",
-            "minSdkVersion": "17"
-        }
-    }
-}

BIN
nativeplugins/MyFaceModule/android/myFaceSDK-release.aar


+ 4 - 0
nativeplugins/MyFaceModule/changelog.md

@@ -0,0 +1,4 @@
+## 1.0.0
+ - 1.自研人脸 Module:录入 / 检索 / 比对 / 同步服务器
+ - 2.自研相机组件 my-face-camera:实时 1:N、正脸与完整人脸约束
+ - 3.依赖 ML Kit 检测 + LiteRT 特征 + CameraX 预览

+ 49 - 0
nativeplugins/MyFaceModule/package.json

@@ -0,0 +1,49 @@
+{
+    "name": "MyFaceModule",
+    "id": "MyFaceModule",
+    "version": "1.0.0",
+    "description": "自研人脸识别 UniApp 原生插件:离线检测、特征提取、1:N 检索、CameraX 实时识别",
+    "_dp_type": "nativeplugin",
+    "_dp_nativeplugin": {
+        "android": {
+            "plugins": [
+                {
+                    "type": "module",
+                    "name": "MyFaceModule",
+                    "class": "io.myface.uniplugin.MyFaceModule"
+                },
+                {
+                    "type": "component",
+                    "name": "my-face-camera",
+                    "class": "io.myface.uniplugin.MyFaceCameraComponent"
+                }
+            ],
+            "hooksClass": "",
+            "integrateType": "aar",
+            "dependencies": [
+                "androidx.appcompat:appcompat:1.6.1",
+                "com.alibaba:fastjson:1.2.83",
+                "androidx.camera:camera-core:1.3.0",
+                "androidx.camera:camera-camera2:1.3.0",
+                "androidx.camera:camera-lifecycle:1.3.0",
+                "androidx.camera:camera-view:1.3.0",
+                "com.google.mlkit:face-detection:16.1.7",
+                "com.google.ai.edge.litert:litert:1.2.0",
+                "com.github.bumptech.glide:glide:4.16.0"
+            ],
+            "compileOptions": {
+                "sourceCompatibility": "1.8",
+                "targetCompatibility": "1.8"
+            },
+            "abis": [
+                "armeabi-v7a",
+                "arm64-v8a"
+            ],
+            "minSdkVersion": "21",
+            "permissions": [
+                "<uses-permission android:name=\"android.permission.CAMERA\"/>"
+            ],
+            "parameters": {}
+        }
+    }
+}

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


+ 27 - 10
nativeplugins/Sys-Plugin/package.json

@@ -15,36 +15,53 @@
                 {
                     "type": "module",
                     "name": "SerialPortModule",
-                    "class": "com.example.sysplugin.SerialPortModule"
-                },                
+                    "class": "com.example.serialplugin.SerialPortModule"
+                },
                 {
                     "type": "module",
                     "name": "yxPlugin",
                     "class": "com.example.sysplugin.YxPluginModule"
                 },
-				{
-				    "type": "module",
-				    "name": "phPlugin",
-				    "class": "com.example.sysplugin.PhPluginModule"
-				},
+                {
+                    "type": "module",
+                    "name": "phPlugin",
+                    "class": "com.example.sysplugin.PhPluginModule"
+                },
                 {
                     "type": "module",
                     "name": "mqttPlugin",
                     "class": "com.example.sysplugin.mqtt"
+                },
+                {
+                    "type": "module",
+                    "name": "SilentInstall",
+                    "class": "com.example.sysplugin.SilentInstallModule"
+                },
+                {
+                    "type": "module",
+                    "name": "AppGuard",
+                    "class": "com.example.appguardplugin.AppGuardModule"
+                },
+                {
+                    "type": "module",
+                    "name": "autoStart",
+                    "class": "com.example.autostart.BootCompletedReceiver"
                 }
             ],
             "integrateType": "aar",
-            "dependencies": [],
+            "dependencies": [
+                "org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5"
+            ],
             "compileOptions": {
                 "sourceCompatibility": "1.8",
                 "targetCompatibility": "1.8"
             },
             "abis": [
                 "armeabi-v7a",
-                "x86"
+                "arm64-v8a"
             ],
             "minSdkVersion": "21",
             "permissions": []
         }
     }
-}
+}

+ 0 - 1
package.json

@@ -37,7 +37,6 @@
     "update:setting": "node unpackage/config/setting.js"
   },
   "dependencies": {
-    "@amap/amap-jsapi-loader": "^1.0.1",
     "@dcloudio/uni-app": "3.0.0-alpha-4020320240703001",
     "@dcloudio/uni-app-harmony": "3.0.0-alpha-4020320240703001",
     "@dcloudio/uni-app-plus": "3.0.0-alpha-4020320240703001",

+ 36 - 5
src/App.vue

@@ -1,6 +1,6 @@
 <script setup>
 /*----------------------------------依赖引入-----------------------------------*/
-import { onLoad, onShow, onHide, onLaunch, onReady, onBackPress } from "@dcloudio/uni-app";
+import { onShow, onHide, onLaunch, onReady } from "@dcloudio/uni-app";
 import { defineComponent, getCurrentInstance, inject, nextTick, onMounted, watchEffect, ref } from "vue";
 /*----------------------------------接口引入-----------------------------------*/
 /*----------------------------------组件引入-----------------------------------*/
@@ -11,6 +11,7 @@ import { storage } from "@/utils/storage";
 /*----------------------------------公共变量-----------------------------------*/
 const { proxy } = getCurrentInstance();
 /*----------------------------------变量声明-----------------------------------*/
+
 function initApp() {
   proxy.$settingStore.initThemeColor(storage.get("themeColor")); //初始化默认主题
   proxy.$settingStore.systemHeightTop(); //初始化获取安全区高度
@@ -22,10 +23,12 @@ function initApp() {
 //设置初始化
 function stteingInit() {
   plus.screen.unlockOrientation(); //解除屏幕方向的锁定,但是不一定是竖屏;
+  plus.navigator.hideSystemNavigation(); //隐藏安卓底部虚拟导航键
+
   // 智能会议
   if (config.appInfo.appid === "__UNI__F3963F8") {
-    // plus.screen.lockOrientation("portrait-primary");
-    plus.screen.lockOrientation("landscape-primary"); //设置屏幕方向(1.竖屏正方向:portrait-primary 2.竖屏反方向:portrait-secondary 3.横屏正方向:landscape-primary 4.横屏反方向:landscape-secondary 5.自然方向:default)
+    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,
       packageName: "android.meeting.uskyMobile",
@@ -34,11 +37,39 @@ function stteingInit() {
   // 智能门禁
   else if (config.appInfo.appid === "__UNI__8D6E9FD") {
     plus.screen.lockOrientation("portrait-primary"); //设置屏幕方向(1.竖屏正方向:portrait-primary 2.竖屏反方向:portrait-secondary 3.横屏正方向:landscape-primary 4.横屏反方向:landscape-secondary 5.自然方向:default)
-    plus.navigator.hideSystemNavigation(); //隐藏安卓底部虚拟导航键
     proxy.$keyListen.startListen({
       needStopSystem: true,
       packageName: "android.Door.uskyMobile",
     }); //开启物理按钮监听
+
+    proxy.$mqtt.connectWithDevice({
+      deviceId: proxy.$sys.getSerial() || "",
+      serverUri: "tcp://47.98.201.73:1883",
+      username: "usky",
+      password: "usky",
+      keepAliveInterval: 30,
+      subscribeTopic: `usky/device/${proxy.$sys.getSerial()}/command`,
+      onCommand(cmd, raw) {
+        if (cmd && cmd.type === "egRemoteUpgrade" && cmd.fileUrl) {
+          proxy.$upgrade.install(
+            {
+              path: cmd.fileUrl, // APK 本地路径或 http(s) 下载地址(MQTT 下发的 fileUrl)
+              autostart: !!cmd.launchPackage, // 安装完成后是否自动拉起应用
+              launchPackage: cmd.launchPackage || "", // 安装完成后要启动的包名(空则不自启)
+              fallbackToSystem: true, // 厂商静默安装失败后是否降级为系统安装界面
+            },
+            (ret) => {
+              console.log("[upgrade]", ret);
+              // TODO: 按业务上报 ack,例如 publishAck(cmd.id, ret.success)
+            },
+          );
+        }
+      },
+      onConnected(res) {
+        if (res.code === 0) console.log("MQTT 已连接");
+        else console.warn("MQTT 未连接", res);
+      },
+    });
   }
 }
 
@@ -47,7 +78,7 @@ onLaunch(() => {
   initApp();
 });
 
-onReady(() => {});
+onReady(() => { });
 
 onShow(() => {
   console.log("App Show");

+ 2 - 9
src/api/business/face.js

@@ -2,17 +2,10 @@ import { request } from "@/utils/request";
 
 /**
  * 人脸校验接口集合
- * @method faceVef 人脸验证
+ * @method facePerson 人员比对
  */
 export function faceApi() {
     return {
-        faceVef: (data) => {
-            return request({
-                url: '/service-meeting/meetingFace/vef',
-                method: 'POST',
-                data,
-            });
-        },
         facePerson: (data) => {
             return request({
                 url: '/service-eg/smartface/compare/person',
@@ -21,4 +14,4 @@ export function faceApi() {
             });
         },
     };
-}
+}

+ 1 - 1
src/api/index/index.js

@@ -28,7 +28,7 @@ export function scan_push(param) {
   });
 }
 
-//echarts图表请求
+// 首页数据
 export function getHomePageData(param) {
   return request({
     url: "/Index/getHomePageData",

+ 8 - 1
src/components/oa-passBody/index.vue

@@ -31,6 +31,8 @@
 </template>
 
 <script>
+import sys from "@/plugins/device/sys.plugins.js";
+
 export default {
   name: "visit-box",
   props: {
@@ -92,6 +94,7 @@ export default {
     },
     //选择事件
     selectkey(even, index) {
+      sys.playClickSound();
       this.key_index = index;
 
       if (even.key !== "backspace" && this.selectIndex < 6) {
@@ -108,7 +111,10 @@ export default {
       }
 
       if (this.selectIndex === 6) {
-        this.$emit("change", this.select.join(""));
+        // 延迟提交,避免弹层立刻关闭截断最后一声嘟
+        setTimeout(() => {
+          this.$emit("change", this.select.join(""));
+        }, 150);
       }
     },
     forgetpass() {
@@ -116,6 +122,7 @@ export default {
     },
     //关闭按钮事件
     close() {
+      sys.playClickSound();
       this.$emit("close", false);
     },
   },

+ 0 - 7
src/config.js

@@ -2,8 +2,6 @@ import manifest from './manifest.json'
 
 // 应用全局配置
 export default {
-  ip: "192.168.10.165:9300",
-
   //#ifdef APP-PLUS || MP-WEIXIN
   baseUrl: "https://gateWay.usky.cn/prod-api",
   // baseUrl: "http://192.168.10.165:801/dev-api",
@@ -13,11 +11,6 @@ export default {
   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: {

+ 3 - 3
src/main.js

@@ -1,4 +1,5 @@
 import { createSSRApp } from "vue";
+import * as Pinia from "pinia";
 import App from "./App";
 import store from "./store"; // store
 import plugins from "./plugins"; // plugins
@@ -18,11 +19,8 @@ import oaUpgrade from "@/components/oa-upgrade/index"
 import oaWeather from "@/components/oa-weather/index"
 import oaSteps from "@/components/oa-steps/index"
 
-// import hideHead from "./utils/hideHead.js";
 export function createApp() {
   const app = createSSRApp(App);
-  // app.mixin(hideHead);// 隐藏头部双重标题
-
 
   //挂载全局组件
   app.component('oa-calendar', oaCalendar)
@@ -47,5 +45,7 @@ export function createApp() {
 
   return {
     app,
+    // uni-app 要求返回 Pinia,否则 App 端跨页/重启后持久化实例可能异常
+    Pinia,
   };
 }

+ 46 - 22
src/manifest.json

@@ -2,8 +2,8 @@
     "name": "智能门禁",
     "appid": "__UNI__8D6E9FD",
     "description": "智能门禁APP,是一款集成了现代信息技术和智能化管理功能的移动应用程序。",
-    "versionName": "1.0.1",
-    "versionCode": 2,
+    "versionName": "1.1.6",
+    "versionCode": 16,
     "transformPx": false,
     "app-plus": {
         "compatible": {
@@ -28,14 +28,13 @@
             "landscape-secondary"
         ],
         "modules": {
-            "VideoPlayer": {},
             "iBeacon": {},
-            "Geolocation": {},
-            "Maps": {},
             "Barcode": {},
             "Fingerprint": {},
             "Camera": {},
-            "LivePusher": {}
+            "LivePusher": {},
+            "Record": {},
+            "VideoPlayer": {}
         },
         "distribute": {
             "android": {
@@ -58,7 +57,7 @@
                     "<uses-permission android:name=\"android.permission.INTERNET\"/>",
                     "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
                     "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
-                    "<uses-permission android:name=\"android.permission.NFC\"/>",
+                    "\t",
                     "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
                     "<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
                     "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
@@ -77,14 +76,15 @@
                     "<uses-permission android:name=\"android.permission.BLUETOOTH\"/>",
                     "<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>",
                     "<uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\"/>",
-                    "<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>"
+                    "<uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\"/>",
+                    "<uses-permission android:name=\"android.permission.LOCAL_MAC_ADDRESS\"/>"
                 ],
                 "minSdkVersion": 21,
                 "abiFilters": [
                     "armeabi-v7a",
                     "arm64-v8a"
                 ],
-                "targetSdkVersion": 30
+                "targetSdkVersion": 31
             },
             "ios": {
                 "dSYMs": false,
@@ -102,15 +102,16 @@
                     "NFCReaderUsageDescription": "",
                     "NSBluetoothAlwaysUsageDescription": "该应用需要你的蓝牙,以便读取相关蓝牙设备"
                 },
-                "UIBackgroundModes": ""
+                "UIBackgroundModes": "",
+                "idfa": false
             },
             "sdkConfigs": {
                 "ad": {},
                 "maps": {
                     "amap": {
+                        "name": "amapBOujshtbA",
                         "appkey_ios": "fb35d03fbb17cbf7a8743a522da3c7fc",
-                        "appkey_android": "ffc71dfd4e576596027f8f45a1b8fb2f",
-                        "name": "amapBOujshtbA"
+                        "appkey_android": "ffc71dfd4e576596027f8f45a1b8fb2f"
                     }
                 },
                 "geolocation": {
@@ -121,13 +122,13 @@
                         ]
                     },
                     "amap": {
+                        "name": "amapBOujshtbA",
                         "__platform__": [
                             "ios",
                             "android"
                         ],
                         "appkey_ios": "fb35d03fbb17cbf7a8743a522da3c7fc",
-                        "appkey_android": "ffc71dfd4e576596027f8f45a1b8fb2f",
-                        "name": "amapBOujshtbA"
+                        "appkey_android": "ffc71dfd4e576596027f8f45a1b8fb2f"
                     },
                     "tencent": {
                         "__platform__": [
@@ -194,7 +195,36 @@
                 "iosStyle": "common"
             }
         },
-        "nativePlugins": {},
+        "nativePlugins": {
+            "Sys-Plugin": {
+                "__plugin_info__": {
+                    "name": "Sys-Plugin",
+                    "description": "系统硬件",
+                    "platforms": "Android",
+                    "url": "",
+                    "android_package_name": "",
+                    "ios_bundle_id": "",
+                    "isCloud": false,
+                    "bought": -1,
+                    "pid": "",
+                    "parameters": {}
+                }
+            },
+            "MyFaceModule": {
+                "__plugin_info__": {
+                    "name": "MyFaceModule",
+                    "description": "自研人脸识别 UniApp 原生插件:离线检测、特征提取、1:N 检索、CameraX 实时识别",
+                    "platforms": "Android",
+                    "url": "",
+                    "android_package_name": "",
+                    "ios_bundle_id": "",
+                    "isCloud": false,
+                    "bought": -1,
+                    "pid": "",
+                    "parameters": {}
+                }
+            }
+        },
         "safearea": {
             "offset": "none"
         }
@@ -234,13 +264,7 @@
             "proxy": {}
         },
         "sdkConfigs": {
-            "maps": {
-                "amap": {
-                    "key": "d4d73a7d572b6ff6028d5f67de62029a",
-                    "securityJsCode": "be916fcd16d0b33d228c49f0ff096b17",
-                    "serviceHost": ""
-                }
-            }
+            "maps": {}
         },
         "optimization": {
             "treeShaking": {

+ 34 - 10
src/pages.json

@@ -13,7 +13,7 @@
             }
         },
         {
-            "path": "pages/meeting/setting/password",
+            "path": "pages/setting/password",
             "style": {
                 "navigationBarTitleText": "密码验证",
                 "enablePullDownRefresh": false,
@@ -25,7 +25,7 @@
             }
         },
         {
-            "path": "pages/meeting/setting/SerialPort",
+            "path": "pages/setting/device/SerialPort",
             "style": {
                 "navigationBarTitleText": "串口通讯",
                 "enablePullDownRefresh": false,
@@ -37,9 +37,9 @@
             }
         },
         {
-            "path": "pages/meeting/setting/index",
+            "path": "pages/setting/device/CardReaderTest",
             "style": {
-                "navigationBarTitleText": "会议配置",
+                "navigationBarTitleText": "M11读卡器测试",
                 "enablePullDownRefresh": false,
                 "navigationStyle": "custom",
                 "app-plus": {
@@ -49,7 +49,7 @@
             }
         },
         {
-            "path": "pages/door/setting/index",
+            "path": "pages/setting/index",
             "style": {
                 "navigationBarTitleText": "",
                 "enablePullDownRefresh": false,
@@ -61,7 +61,7 @@
             }
         },
         {
-            "path": "pages/door/setting/public/index",
+            "path": "pages/setting/public/index",
             "style": {
                 "navigationBarTitleText": "",
                 "enablePullDownRefresh": false,
@@ -73,7 +73,7 @@
             }
         },
         {
-            "path": "pages/door/setting/system/index",
+            "path": "pages/setting/system/index",
             "style": {
                 "navigationBarTitleText": "",
                 "enablePullDownRefresh": false,
@@ -85,7 +85,7 @@
             }
         },
         {
-            "path": "pages/door/setting/system/senior",
+            "path": "pages/setting/system/senior",
             "style": {
                 "navigationBarTitleText": "",
                 "enablePullDownRefresh": false,
@@ -97,7 +97,7 @@
             }
         },
         {
-            "path": "pages/door/setting/system/network",
+            "path": "pages/setting/system/network",
             "style": {
                 "navigationBarTitleText": "",
                 "enablePullDownRefresh": false,
@@ -109,7 +109,7 @@
             }
         },
         {
-            "path": "pages/door/faceStorage/index",
+            "path": "pages/setting/faceStorage/index",
             "style": {
                 "navigationBarTitleText": "人脸数据管理",
                 "enablePullDownRefresh": false,
@@ -120,6 +120,30 @@
                 }
             }
         },
+        {
+            "path": "pages/setting/myFace/index",
+            "style": {
+                "navigationBarTitleText": "自研人脸调试",
+                "enablePullDownRefresh": false,
+                "navigationStyle": "custom",
+                "app-plus": {
+                    "bounce": "none",
+                    "titleNView": false
+                }
+            }
+        },
+        {
+            "path": "pages/setting/myFace/camera",
+            "style": {
+                "navigationBarTitleText": "实时识别",
+                "enablePullDownRefresh": false,
+                "navigationStyle": "custom",
+                "app-plus": {
+                    "bounce": "none",
+                    "titleNView": false
+                }
+            }
+        },
         {
             "path": "pages/meeting/index",
             "style": {

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

@@ -1,191 +0,0 @@
-<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>

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

@@ -1,1116 +0,0 @@
-<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>

+ 115 - 64
src/pages/door/index.js

@@ -6,11 +6,12 @@ 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";
+import sys from "@/plugins/device/sys.plugins.js";
 
 /** 门禁相关子页路径 */
 export const DoorRoutes = {
-    faceStorage: "/pages/door/faceStorage/index",
-    setting: "/pages/door/setting/index",
+    faceStorage: "/pages/setting/faceStorage/index",
+    setting: "/pages/setting/index",
 };
 
 /**
@@ -43,32 +44,35 @@ export const DoorArr = {
         meetingName: undefined,
     },
     ipAddress: "",
-    deviceSerial: "",
+    /** my-face-camera setConfig 参数 */
+    thresholds: {
+        strongThreshold: 0.60, // 强阈值:高置信通过
+        weakThreshold: 0.55, // 弱阈值:疑似通过下限
+        minGap: 0.04, // top1-top2 分差
+        analyzeIntervalMs: 500, // 分析间隔(ms)
+        showFaceBox: false, // 是否显示人脸框
+        stopOnSuccess: false, // 成功后是否停止识别
+        previewShape: "circle", // circle 圆形 / square 正方形 / rect 长方形
+        overlayStyle: "gif", // 叠加层样式:gif / ring / none
+        overlayGifPath: "auto", // GIF 路径,auto=按预览形态自动选
+        ttsEnabled: true, // 是否开启语音播报
+        ttsVolume: 100, // TTS 音量 0-100
+        requireFrontal: true, // 仅正脸才比对
+        requireCompleteFace: true, // 仅完整人脸(未半脸、五官齐全)才比对
+        maxYawDeg: 18, // 偏航角上限(度)
+        maxPitchDeg: 18, // 俯仰角上限(度)
+        maxRollDeg: 22, // 翻滚角上限(度)
+        successCooldownMs: 2000, // 同一人成功冷却(ms),0=不限制
+    },
 };
 
-/**
- * 同步设备序列号到 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) {
+        if (!DoorArr.form?.door?.id) {
             modal.msg("请先绑定门禁!");
             return;
         }
@@ -90,11 +94,7 @@ export const DoorOpenFunc = {
                 commandValue: 1,
             })
             .then((item) => {
-                if (item.status == "success") {
-                    modal.msg("开门成功");
-                } else {
-                    modal.msg("开门失败");
-                }
+                modal.msg("开门成功");
                 setTimeout(() => {
                     DoorArr.isClicked = false;
                 }, 2000);
@@ -116,32 +116,34 @@ export const DoorOpenFunc = {
 };
 
 /**
- * 相机相关
+ * 相机相关(原生 my-face-camera)
  */
 export const cameraFunc = {
-    /** 取页面 doorCam 原生组件实例 */
+    /** 取页面 myFaceCam 原生组件实例 */
     getRef(vm) {
-        const cam = vm && vm.$refs ? vm.$refs.doorCam : null;
+        if (!vm || !vm.$refs) return null;
+        const cam = vm.$refs.myFaceCam || vm.$refs.doorCam;
+        if (Array.isArray(cam)) return cam[0] || 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
+    /** App 本地绝对路径补 file://,供 <image> 加载 */
+    toLocalImageSrc(raw) {
+        const p = String(raw || "").trim();
+        if (!p) return "";
+        if (/^https?:\/\//i.test(p)) return p;
+        if (p.startsWith("data:image/")) return p;
+        if (p.startsWith("file://")) return p;
+        if (p.startsWith("/")) return "file://" + p;
+        return p;
     },
-    /** 语音播报(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
+    /** 统一解析 my-face-camera 结果 */
+    pickDetail(e) {
+        if (!e) return {};
+        if (e.detail && e.detail.detail && typeof e.detail.detail === "object") {
+            return e.detail.detail;
+        }
+        if (e.detail && typeof e.detail === "object") return e.detail;
+        return e;
     },
     /** 申请相机权限后启动门禁识别(含 setConfig) */
     async start(vm) {
@@ -150,12 +152,16 @@ export const cameraFunc = {
         if (!ok) return;
 
         const cam = this.getRef(vm);
-        if (!cam || typeof cam.start !== "function") return;
+        if (!cam || typeof cam.setConfig !== "function") return;
 
-        if (typeof cam.setConfig === "function") {
-            cam.setConfig({ ...vm.thresholds, autoStart: true });
+        cam.setConfig({
+            ...(DoorArr.thresholds || {}),
+            previewShape: (DoorArr.thresholds && DoorArr.thresholds.previewShape) || "circle",
+        });
+        if (typeof cam.start === "function") cam.start();
+        if (typeof vm?.state?.openMode === "string" && vm.state.openMode.includes("静默人脸")) {
+            this.setFakeHidden(vm, true);
         }
-        cam.start();
         // #endif
     },
     /** 停止门禁相机/分析 */
@@ -167,23 +173,66 @@ export const cameraFunc = {
         }
         // #endif
     },
-    /** door-camera-x:状态回调 */
+    /** 相机状态回调 */
     onState(vm, e) {
-        const d = e && e.detail ? e.detail : {};
-        if (d && d.state === "error") {
+        const d = this.pickDetail(e);
+        if (d && (d.state === "error" || d.code === "bind_error" || d.code === "bind_failed")) {
             // 启动/运行失败:由页面生命周期或用户操作决定是否重试
         }
     },
-    /** door-camera-x:识别结果 */
+    /** 人脸识别结果回调(适配 my-face-camera:matched/faceId/name/avatarSrc) */
     onResult(vm, e) {
-        const d = (e && (e.detail?.detail || e.detail || e)) || {};
-
-        if (d.code == 1) {
+        const d = this.pickDetail(e);
+        const matched =
+            d.matched === true ||
+            d.matched === 1 ||
+            d.matched === "true" ||
+            Number(d.code) === 1;
+        const faceId = String(d.faceId || d.matchedFaceId || d.userId || "").trim();
+        const faceName = String(d.name || d.matchedFaceName || d.faceName || "").trim();
+        const avatarSrc = this.toLocalImageSrc(d.avatarSrc || d.imagePath || d.capturePath || "");
+
+        if (matched) {
             sysPlugins.openDoor(1, 0);
-            FaceVerifyFunc.show(vm, d);
-        } else if (d.hasFace === true) {
-            FaceVerifyFunc.show(vm, d);
+            FaceVerifyFunc.show(vm, {
+                code: 1,
+                matched: true,
+                matchedFaceId: faceId,
+                matchedFaceName: faceName,
+                faceId,
+                faceName,
+                avatarSrc,
+                capturePath: avatarSrc,
+                similarity: d.similarity,
+            });
+            return;
         }
+
+        if (d.hasFace === true) {
+            FaceVerifyFunc.show(vm, {
+                code: 0,
+                matched: false,
+                hasFace: true,
+                avatarSrc,
+                capturePath: avatarSrc,
+            });
+        }
+    },
+    /** 静默人脸:仅隐藏预览,不影响分析 */
+    setFakeHidden(vm, enabled) {
+        if (!vm) return;
+        vm.cameraFakeHidden = enabled != null && enabled;
+    },
+    /** 语音播报(TTS) */
+    speak(vm, text) {
+        // #ifdef APP-PLUS
+        const cam = this.getRef(vm);
+        if (!cam || typeof cam.speak !== "function") return;
+        if (!DoorArr.thresholds || DoorArr.thresholds.ttsEnabled !== true) return;
+        const t = (text || "").toString();
+        if (!t.trim()) return;
+        cam.speak(t);
+        // #endif
     },
 };
 
@@ -192,6 +241,7 @@ export const cameraFunc = {
  */
 export const doorPageUi = {
     parentMessage(vm, type) {
+        sys.playClickSound();
         if (type === "点击开门") {
             DoorOpenFunc.click(vm, {
                 userId: "",
@@ -217,7 +267,9 @@ export const FaceVerifyFunc = {
         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();
+        const avatarSrc = cameraFunc.toLocalImageSrc(
+            String((detail && (detail.avatarSrc || detail.capturePath || detail.imagePath)) || "").trim()
+        );
 
         if (!vm.faceVerifyPopup) {
             vm.faceVerifyPopup = {
@@ -231,7 +283,7 @@ export const FaceVerifyFunc = {
 
         if (code === 1) {
             vm.faceNotFoundStreak = 0;
-            if (vm.thresholds?.ttsEnabled) {
+            if (DoorArr.thresholds?.ttsEnabled) {
                 cameraFunc.speak(vm, "验证成功");
             }
             vm.faceVerifyPopup.success = true;
@@ -249,7 +301,7 @@ export const FaceVerifyFunc = {
         }
         vm.faceNotFoundStreak = 0;
 
-        if (vm.thresholds?.ttsEnabled) {
+        if (DoorArr.thresholds?.ttsEnabled) {
             cameraFunc.speak(vm, "验证失败");
         }
         vm.faceVerifyPopup.success = false;
@@ -442,8 +494,7 @@ export const apiRequest = {
                     data = { ...data, openMode: removeSegmentWithTarget(data.openMode, "点击开门") };
                 }
 
-                const openModeStr = typeof data.openMode === "string" ? data.openMode : "";
-                const silentFace = openModeStr.includes("静默人脸");
+                const silentFace = typeof data.openMode === "string" && data.openMode.includes("静默人脸");
                 cameraFunc.setFakeHidden(vm, silentFace);
 
                 Object.assign(DoorArr.form.door, data);

+ 23 - 37
src/pages/door/index.nvue

@@ -20,25 +20,24 @@
 
         <view class="faceArea" v-if="state.openMode?.includes('人脸')">
           <view class="faceRing">
-            <door-camera-x
-              ref="doorCam"
+            <my-face-camera
+              ref="myFaceCam"
               class="camera"
+              :class="cameraFakeHidden ? 'cameraHidden' : ''"
               @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>
+            ></my-face-camera>
           </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>
+        <text class="iconfont"  v-if="state.openMode?.includes('点击开门')" @click="doorPageUi.parentMessage(this, '点击开门')" style="margin-right:36rpx">{{ icons.openDoor }}</text>
+        <text class="iconfont"  v-if="state.openMode?.includes('密码')" @click="doorPageUi.parentMessage(this, '密码')">{{ icons.password }}</text>
       </view>
     </view>
 
@@ -55,15 +54,16 @@
       @close="FaceVerifyFunc.close(this)"
     ></face-verify-popup>
 
-    <view class="cornerMeta" v-if="DoorArr.ipAddress || DoorArr.deviceSerial">
+    <view class="cornerMeta" v-if="DoorArr.ipAddress || 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>
+      <text class="cornerText cornerSn" v-if="deviceSerial">SN:{{ deviceSerial }}</text>
     </view>
   </view>
 </template>
 
 <script>
 import oaPassBody from "@/components/oa-passBody/index.vue";
+import sysPlugins from "@/plugins/device/sys.plugins";
 import {
   navigateByKey,
   cameraFunc,
@@ -73,10 +73,9 @@ import {
   clockFunc,
   FaceVerifyFunc,
   doorPageUi,
-  syncDeviceSerial,
   DoorArr,
 } from "./index.js";
-import FaceVerifyPopup from "./components/face-verify-popup.nvue";
+import FaceVerifyPopup from "@/components/oa-face-verify-popup/index.nvue";
 
 const domModule = weex.requireModule('dom');
 domModule .addRule('fontFace', {
@@ -107,26 +106,13 @@ export default {
         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
-      },
+      cameraFakeHidden: false,
       state: {
         dateTime: "",
         date: "",
         doorName: "",
         imgPath: "",
-        openMode: "人脸,点击开门,密码",
+        openMode: "点击开门,密码",
         remark: "",
         workName: "",
         workClass: "",
@@ -138,8 +124,14 @@ export default {
       },
     };
   },
+  computed: {
+    /** 左下角 SN:直接读设备信息,无需单独同步方法 */
+    deviceSerial() {
+      const info = sysPlugins.getDeviceInfo && sysPlugins.getDeviceInfo();
+      return info && info.serial != null ? String(info.serial) : "";
+    },
+  },
   onReady() {
-    syncDeviceSerial();
     clockFunc.start(this);
     initDoorData(this);
     IntervalFunc.open(this, "ipAddress");
@@ -149,7 +141,6 @@ export default {
     });
   },
   onShow() {
-    syncDeviceSerial();
     clockFunc.start(this);
     IntervalFunc.open(this, "ipAddress");
     IntervalFunc.open(this, "door");
@@ -276,25 +267,20 @@ export default {
   height: 300px;
   border-radius: 150px;
 }
+.cameraHidden {
+  opacity: 0;
+}
 .homeFooter {
   flex-direction: row;
   justify-content: center;
-  padding: 18px 22px;
-}
-.btnArea {
-  flex-direction: row;
   align-items: center;
-  justify-content: center;
+  padding: 18px 22px;
 }
 .iconfont {
   font-family: iconfont;
   font-size: 55px;
   color: #fff;
 }
-.iconfont+.iconfont{
-  margin-left: 26rpx;
-}
-
 .passwordLayer {
   position: fixed;
   left: 0;

+ 0 - 124
src/pages/door/index4.vue

@@ -1,124 +0,0 @@
-<template>
-  <web-view
-    v-show="!controlStore.popup.show"
-    ref="faceView"
-    id="faceView"
-    class="faceView"
-    src="/static/face/door.html"
-    bindmessage="receiveMessage"
-    :webview-styles="webviewStyles"
-    @message="onMessage"
-  >
-  </web-view>
-
-  <u-popup :show="controlStore.popup.show" bgColor="#474747" :zIndex="10074">
-    <oaPassBody :show="controlStore.popup.show" @change="change" @close="close"></oaPassBody>
-  </u-popup>
-</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 oaPassBody from "@/components/oa-passBody/index";
-/*----------------------------------store引入-----------------------------------*/
-import { controlStores } from "@/store/modules/index";
-/*----------------------------------公共方法引入-----------------------------------*/
-const { proxy } = getCurrentInstance();
-const controlStore = controlStores();
-/*----------------------------------公共变量-----------------------------------*/
-const state = reactive({
-  version: computed(() => {
-    return config.appInfo.version;
-  }),
-  webviewStyles: {
-    width: "100%",
-    height: "100%",
-  },
-});
-const { version, webviewStyles } = toRefs(state);
-
-// 初始化
-function init() {
-  controlStore.openInterval("ipAddress"); //定时获取IP地址
-  controlStore.initCamera(); //初始化摄像头
-  // controlStore.initNfc();//初始化NFC
-  controlStore.initData(); //初始化数据
-  controlStore.openInterval("door"); //开启门禁数据定时任务
-}
-
-// 密码开门组件确认事件
-function change(event) {
-  if (event == controlStore.form.door.password) {
-    controlStore.popup.show = false;
-    controlStore.analysisData({ funcName: "点击开门" });
-  } else {
-    controlStore.popup.show = false;
-    proxy.$modal.msg("密码错误");
-  }
-}
-
-// 密码开门组件关闭事件
-function close(event) {
-  controlStore.popup.show = event;
-}
-
-/**
- * @接收子页面传过来的值
- */
-function onMessage(e) {
-  controlStore.analysisData(e.detail.data[0]);
-}
-// #ifdef H5
-window.onmessage = function (event) {
-  controlStore.analysisData(event.data);
-};
-// #endif
-
-onLoad((options) => {});
-
-onShow(() => {
-  init();
-});
-
-onHide(() => {
-  controlStore.clearInterval("door"); //关闭门禁数据定时任务
-});
-
-onUnload(() => {
-  controlStore.clearInterval("door"); //关闭门禁数据定时任务
-});
-</script>
-<style>
-.faceView {
-  width: 100% !important;
-  height: 100% !important;
-}
-
-iframe {
-  width: 100% !important;
-  height: 100% !important;
-  border-width: 0;
-}
-</style>
-<style lang="scss" scoped>
-:deep() {
-  .u-modal {
-    width: 20rem !important;
-
-    &__title {
-      font-size: 18px !important;
-    }
-    &__selector-item {
-      color: black;
-    }
-
-    .slot-content {
-      font-size: 16px;
-      width: 100%;
-    }
-  }
-}
-</style>

+ 0 - 132
src/pages/door/setting/index.scss

@@ -1,132 +0,0 @@
-.faceStorage-container,
-.doorSetting-container,
-.doorSettingOther-container,
-.doorSettingServe-container,
-.doorSettingSystem-container,
-.doorSettingPublic-container {
-    // background: url(@/static/face/img/face_bg.png) no-repeat;
-    // background-size: 100% 100%;
-    background-color: #303133;
-    color: #ffffff;
-    padding: 16px;
-
-    .iconfont,
-    .oaIcon-left {
-        display: block;
-        margin-bottom: 30px;
-    }
-
-    .Grid {
-        display: flex;
-        justify-content: left;
-        /* 左对齐 */
-        flex-wrap: wrap;
-        /* 换行 */
-
-        &-item {
-            flex: 0 0 25%;
-            margin-bottom: 20px;
-
-            &-image {
-                display: block;
-                width: 40px;
-                height: 40px;
-                margin: auto;
-            }
-
-            &-lable {
-                text-align: center;
-                margin: auto;
-                margin-top: 16px;
-            }
-        }
-    }
-
-    :deep() {
-        .uni-select {
-            .uni-icons {
-                color: white !important;
-                font-size: 18px !important;
-            }
-        }
-
-
-        .u-cell__value,
-        .u-cell__title-text {
-            color: white;
-        }
-
-        .u-cell__body {
-            padding: 10px 10px !important;
-            // background-color: #ffffff !important;
-
-            >.iconfont,
-            .u-iconfont {
-                margin: auto 0px;
-                color: white !important;
-                font-size: 16px !important;
-            }
-        }
-    }
-}
-
-@media (min-width: 500px) {
-    .faceStorage-container,
-    .doorSetting-container,
-    .doorSettingOther-container,
-    .doorSettingServe-container,
-    .doorSettingSystem-container,
-    .doorSettingPublic-container {
-        font-size: 0.6rem !important;
-        padding: 1rem !important;
-
-        .oaIcon-exit,
-        .oaIcon-left {
-            font-size: 1.5rem !important;
-            margin-bottom: 1.5rem !important;
-        }
-
-        .Grid {
-            &-item {
-                margin-bottom: 1.5rem !important;
-
-                &-image {
-                    width: 2.5rem;
-                    height: 2.5rem;
-                }
-
-                &-lable {
-                    margin-top: 0.5rem !important;
-                }
-            }
-        }
-
-        :deep() {
-            .uni-select {
-                .uni-icons {
-                    font-size: 0.8rem !important;
-                }
-            }
-
-            .u-cell {
-                .u-cell__title-text {
-                    font-size: 0.8rem !important;
-                }
-
-                .u-cell__value {
-                    font-size: 0.6rem !important;
-                }
-
-                .u-cell__body {
-                    padding: 0.8rem 10px !important;
-
-                    .iconfont,
-                    .u-iconfont {
-                        font-size: 0.8rem !important;
-                    }
-                }
-            }
-
-        }
-    }
-}

+ 0 - 89
src/pages/door/setting/index.vue

@@ -1,89 +0,0 @@
-<template>
-  <oa-scroll
-    customClass="doorSetting-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-exit" @click="handleExit()"></text>
-
-      <view class="Grid">
-        <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>
-        <view class="Grid-item" @click="handle('public')">
-          <image class="Grid-item-image" src="@/static/face/img/sysetmSetting.png"></image>
-          <view class="Grid-item-lable">公共设置</view>
-        </view>
-        <view class="Grid-item" @click="startActivity()">
-          <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>
-</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({});
-const {} = toRefs(state);
-
-function handle(type) {
-  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");
-  }
-}
-
-function handleExit() {
-  proxy.$tab.navigateBack(1);
-}
-
-function startActivity() {
-  proxy.$sys.getEthernetIpAddress();
-
-  //#ifdef APP-PLUS
-  const phPlugin = uni.requireNativePlugin("phPlugin");
-  phPlugin.startActivity();
-  //#endif
-}
-
-onLoad((options) => {
-});
-
-onShow(() => {});
-</script>
-<style lang="scss">
-@import "./index.scss";
-</style>

+ 0 - 34
src/pages/door/setting/public/index.vue

@@ -1,34 +0,0 @@
-<template>
-  <oa-scroll
-    customClass="doorSettingPublic-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>
-    </template>
-  </oa-scroll>
-</template>
-<script setup>
-import { getCurrentInstance } from "vue";
-
-const { proxy } = getCurrentInstance();
-
-function handleExit() {
-  proxy.$tab.navigateBack(1);
-}
-</script>
-<style lang="scss" scoped>
-@import "../index.scss";
-</style>

+ 0 - 142
src/pages/door/setting/system/index.vue

@@ -1,142 +0,0 @@
-<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 || H5 -->
-        <u-cell title="软件版本号" :value="version"></u-cell>
-        <u-cell title="清除缓存" @click="proxy.$setting.clearCache()">
-          <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>
-        </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>
-        </u-cell>
-        <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"
-  />
-</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);
-
-/**
- * @检查更新
- */
-function handleToUpgrade() {
-  proxy.$settingStore.handleToUpgrade({
-    success: (res) => {
-      proxy.$refs["oaUpgradeRef"].openUpgrade({
-        modalArray: res.data,
-      });
-    },
-  });
-}
-
-// 按钮点击事件
-function handleToSetting(type) {
-  if (type == "network") {
-    proxy.$tab.navigateTo("/pages/door/setting/system/network");
-  } else if (type == "senior") {
-    proxy.$tab.navigateTo("/pages/door/setting/system/senior");
-  }
-}
-
-function handleExit() {
-  proxy.$tab.navigateBack(1);
-}
-
-onLoad((options) => {
-});
-</script>
-<style lang="scss" scoped>
-@import "../index.scss";
-</style>

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

@@ -1,76 +0,0 @@
-<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>

+ 0 - 102
src/pages/door/setting/system/senior.vue

@@ -1,102 +0,0 @@
-<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>
-        <u-cell title="测试内置硬件" @click="text()"></u-cell>
-        <u-cell title="继电器">
-          <template #value>
-            <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.$sys.navigationBar"></u-switch>
-          </template>
-        </u-cell>
-        <u-cell title="隐藏状态栏">
-          <template #value>
-            <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.$sys.statusBarDrop"></u-switch>
-          </template>
-        </u-cell>
-        <u-cell title="退出应用" @click="Cancel()">
-          <template #value> <u-icon class="iconfont" name="arrow-rightward"></u-icon></template>
-        </u-cell>
-      </u-cell-group>
-    </template>
-  </oa-scroll>
-</template>
-<script setup>
-/*----------------------------------依赖引入-----------------------------------*/
-import config from "@/config";
-import { onLoad, onShow } from "@dcloudio/uni-app";
-import { ref, reactive, computed, getCurrentInstance, toRefs, inject, nextTick, watch } from "vue";
-/*----------------------------------接口引入-----------------------------------*/
-/*----------------------------------组件引入-----------------------------------*/
-/*----------------------------------store引入-----------------------------------*/
-import { controlStores, commonStores } from "@/store/modules/index";
-/*----------------------------------公共方法引入-----------------------zhe------------*/
-const { proxy } = getCurrentInstance();
-const controlStore = controlStores();
-const commonStore = commonStores();
-/*----------------------------------公共变量-----------------------------------*/
-const state = reactive({
-  version: computed(() => {
-    return config.appInfo.version;
-  }),
-});
-const { version } = toRefs(state);
-
-function text() {
-  const phPlugin = uni.requireNativePlugin("phPlugin");
-  phPlugin.text();
-}
-
-function handleExit() {
-  proxy.$tab.navigateBack(1);
-}
-
-// 应用退出按钮事件
-function Cancel() {
-  //#ifdef APP-PLUS
-  proxy.$keyListen.quitApp();
-  //#endif
-}
-
-/** 从系统读取状态栏是否可见,同步「隐藏状态栏」开关(可见则关隐藏,不可见则开隐藏) */
-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>
-@import "../index.scss";
-</style>

+ 0 - 68
src/pages/meeting/index.vue

@@ -1,68 +0,0 @@
-<template>
-  <web-view ref="faceView" id="faceView" class="faceView" :src="controlStore.meetingTemplateSrc" bindmessage="receiveMessage" :webview-styles="webviewStyles" @message="onMessage"> </web-view>
-</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({
-  webviewStyles: {
-    width: "100%",
-    height: "100%",
-  },
-});
-const { webviewStyles } = toRefs(state);
-
-// 初始化
-function init() {
-  controlStore.openInterval("ipAddress"); //定时获取IP地址
-  controlStore.initCamera(); //初始化摄像头
-  controlStore.initNfc(); //初始化NFC
-  controlStore.initData(); //初始化数据
-  controlStore.openInterval("meeting"); //定时获取会议信息
-
-  proxy.$sys.relayControl(false); // 关闭门禁(继电器)
-}
-
-/**
- * @接收子页面传过来的值
- */
-function onMessage(e) {
-  controlStore.analysisData(e.detail.data[0]);
-}
-// #ifdef H5
-window.onmessage = function (event) {
-  controlStore.analysisData(event.data);
-};
-// #endif
-
-onLoad((options) => {
-  init();
-});
-
-onShow(() => {});
-
-onUnload(() => {
-  controlStore.clearInterval("meeting");
-});
-</script>
-<style>
-.faceView {
-  width: 100% !important;
-  height: 100% !important;
-}
-
-iframe {
-  width: 100% !important;
-  height: 100% !important;
-  border-width: 0;
-}
-</style>

+ 0 - 168
src/pages/meeting/setting/index.vue

@@ -1,168 +0,0 @@
-<template>
-  <oa-scroll
-    customClass="meetingSetting-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>
-      <view class="setting-header">
-        <text class="iconfont oaIcon-exit" @click="handleExit()"></text>
-        <view class="setting-title">会议配置</view>
-      </view>
-
-      <view class="setting-content">
-        <u-cell-group>
-          <u-cell title="门禁开关">
-            <template #value>
-              <u-switch v-model="setting.doorStatus" size="15" @change="proxy.$sys.relayControl"></u-switch>
-            </template>
-          </u-cell>
-          <u-cell title="导航栏显示">
-            <template #value>
-              <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>
-          <u-cell title="设备序列号" :value="sysPlugins.getDeviceInfo().serial || '-'"></u-cell>
-          <u-cell title="检查更新" @click="handleToUpgrade()">
-            <template #value>
-              <view class="iconfont oaIcon-jianchagengxin menu-item-icon mr2" style="color: #2979ff"></view>
-            </template>
-          </u-cell>
-          <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>
-          </u-cell>
-        </u-cell-group>
-      </view>
-    </template>
-  </oa-scroll>
-
-  <oa-upgrade ref="oaUpgradeRef" :themesColor="proxy.$settingStore.themeColor.color" />
-</template>
-
-<script setup>
-/*----------------------------------依赖引入-----------------------------------*/
-import config from "@/config";
-import { onLoad, onShow, onUnload } from "@dcloudio/uni-app";
-import { ref, reactive, computed, getCurrentInstance, toRefs } from "vue";
-/*----------------------------------组件引入-----------------------------------*/
-/*----------------------------------store引入-----------------------------------*/
-import { controlStores } from "@/store/modules/index";
-/*----------------------------------公共方法引入-----------------------------------*/
-import sysPlugins from "@/plugins/device/sys.plugins";
-/*----------------------------------公共变量-----------------------------------*/
-const { proxy } = getCurrentInstance();
-const controlStore = controlStores();
-const oaUpgradeRef = ref(null);
-/*----------------------------------公共变量-----------------------------------*/
-const state = reactive({
-  version: computed(() => {
-    return config.appInfo.version;
-  }),
-  setting: {
-    doorStatus: false,
-    navBarNew: true,
-    ipAddress: "",
-  },
-});
-const { version, setting } = toRefs(state);
-
-function SerialPortPage() {
-  proxy.$tab.navigateTo("/pages/meeting/setting/SerialPort");
-}
-
-function CardReaderTestPage() {
-  proxy.$tab.navigateTo("/pages/meeting/setting/CardReaderTest");
-}
-
-/**
- * @退出
- */
-function handleExit() {
-  proxy.$tab.navigateBack(2);
-  controlStore.initCamera(); //初始化摄像头
-}
-
-// 隐藏导航栏
-function navBarNewChange(value) {
-  proxy.$yx.navBarNewChange(value);
-}
-
-/**
- * @检查更新
- */
-function handleToUpgrade() {
-  proxy.$settingStore.handleToUpgrade({
-    success: (res) => {
-      proxy.$refs["oaUpgradeRef"].openUpgrade({
-        modalArray: res.data,
-      });
-    },
-  });
-}
-
-onLoad((options) => {});
-
-onShow(() => {});
-
-onUnload(() => {});
-</script>
-
-<style lang="scss" scoped>
-.meetingSetting-container {
-  background: white;
-}
-
-.setting-header {
-  display: flex;
-  align-items: center;
-  padding: 20px;
-  background: #fff;
-  border-bottom: 1px solid #e5e5e5;
-
-  .iconfont {
-    font-size: 24px;
-    color: #333;
-    margin-right: 15px;
-    cursor: pointer;
-  }
-
-  .setting-title {
-    font-size: 20px;
-    font-weight: 600;
-    color: #333;
-  }
-}
-
-.setting-content {
-  padding: 20px;
-}
-
-:deep() {
-  .u-cell__body {
-    padding-left: 0 !important;
-    padding-right: 0 !important;
-  }
-
-  .u-line {
-    border-bottom: 1px solid #dadbde !important;
-  }
-}
-</style>

+ 0 - 117
src/pages/meeting/setting/password.vue

@@ -1,117 +0,0 @@
-<template>
-  <view class="password-container">
-    <view class="password-box">
-      <view class="password-title">请输入配置密码</view>
-      <view class="password-input-box">
-        <u-input v-model="password" type="password" :placeholder="'请输入密码'" border="none" :clearable="true" :maxlength="20" @confirm="handleConfirm"></u-input>
-      </view>
-      <view class="password-tip" v-if="errorTip">{{ errorTip }}</view>
-      <view class="password-buttons">
-        <u-button type="primary" :loading="loading" @click="handleConfirm" class="password-button"> 确认 </u-button>
-        <u-button type="default" @click="handleCancel" class="password-button"> 取消 </u-button>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup>
-/*----------------------------------依赖引入-----------------------------------*/
-import { ref, reactive } from "vue";
-import { onLoad } from "@dcloudio/uni-app";
-import { getCurrentInstance } from "vue";
-import { controlStores } from "@/store/modules/index";
-/*----------------------------------公共变量-----------------------------------*/
-const { proxy } = getCurrentInstance();
-const controlStore = controlStores();
-const password = ref("");
-const errorTip = ref("");
-const loading = ref(false);
-
-// 默认密码,可以从配置或存储中读取
-const defaultPassword = "123456";
-
-/**
- * @确认密码
- */
-function handleConfirm() {
-  if (!password.value) {
-    errorTip.value = "请输入密码";
-    return;
-  }
-
-  loading.value = true;
-  errorTip.value = "";
-
-  // 模拟密码验证(实际可以从存储或服务器验证)
-  setTimeout(() => {
-    loading.value = false;
-
-    if (password.value === defaultPassword) {
-      // 密码正确,跳转到配置页面
-      proxy.$tab.navigateTo("/pages/meeting/setting/index");
-    } else {
-      errorTip.value = "密码错误,请重新输入";
-      password.value = "";
-    }
-  }, 300);
-}
-
-/**
- * @取消
- */
-function handleCancel() {
-  proxy.$tab.navigateBack(1);
-  controlStore.initCamera(); //初始化摄像头
-}
-
-onLoad(() => {});
-</script>
-
-<style lang="scss" scoped>
-.password-container {
-  width: 100%;
-  height: 100vh;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-}
-
-.password-box {
-  width: 90%;
-  max-width: 400px;
-  background: #fff;
-  border-radius: 16px;
-  padding: 40px 30px;
-  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
-}
-
-.password-title {
-  font-size: 24px;
-  font-weight: 600;
-  color: #333;
-  text-align: center;
-  margin-bottom: 30px;
-}
-
-.password-input-box {
-  margin-bottom: 20px;
-}
-
-.password-tip {
-  color: #fa3534;
-  font-size: 14px;
-  text-align: center;
-  margin-bottom: 20px;
-  min-height: 20px;
-}
-
-.password-buttons {
-  display: flex;
-  gap: 15px;
-}
-
-.password-button {
-  flex: 1;
-}
-</style>

+ 1 - 1
src/permission.js

@@ -7,7 +7,7 @@ const loginPage = "/pages/login";
 const whiteList = [
   "/pages/meeting/index",//会议系统
   "/pages/door/index",//门禁识别
-  "/pages/door/setting/index",//门禁配置
+  "/pages/setting/index",//门禁配置
 ];
 
 // 检查地址白名单

+ 0 - 60
src/plugins/auth.plugins.js

@@ -1,60 +0,0 @@
-import store from '@/store'
-
-function authPermission(permission) {
-  const all_permission = "*:*:*"
-  const permissions = store.getters && store.getters.permissions
-  if (permission && permission.length > 0) {
-    return permissions.some(v => {
-      return all_permission === v || v === permission
-    })
-  } else {
-    return false
-  }
-}
-
-function authRole(role) {
-  const super_admin = "admin"
-  const roles = store.getters && store.getters.roles
-  if (role && role.length > 0) {
-    return roles.some(v => {
-      return super_admin === v || v === role
-    })
-  } else {
-    return false
-  }
-}
-
-export default {
-  // 验证用户是否具备某权限
-  hasPermi(permission) {
-    return authPermission(permission)
-  },
-  // 验证用户是否含有指定权限,只需包含其中一个
-  hasPermiOr(permissions) {
-    return permissions.some(item => {
-      return authPermission(item)
-    })
-  },
-  // 验证用户是否含有指定权限,必须全部拥有
-  hasPermiAnd(permissions) {
-    return permissions.every(item => {
-      return authPermission(item)
-    })
-  },
-  // 验证用户是否具备某角色
-  hasRole(role) {
-    return authRole(role)
-  },
-  // 验证用户是否含有指定角色,只需包含其中一个
-  hasRoleOr(roles) {
-    return roles.some(item => {
-      return authRole(item)
-    })
-  },
-  // 验证用户是否含有指定角色,必须全部拥有
-  hasRoleAnd(roles) {
-    return roles.every(item => {
-      return authRole(item)
-    })
-  }
-}

+ 19 - 1
src/plugins/device/sys.plugins.js

@@ -210,5 +210,23 @@ export default {
             version: this.getVersion(),//获取系统版本
             Sdk: this.getSdk(),//获取SDK版本
         }
-    }
+    },
+    /**
+     * @播放按键音
+     */
+    playClickSound() {
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        sysPlugin?.playClickSound?.();
+        //#endif
+    },
+    /**
+     * @释放按键音资源
+     */
+    destroyClickSound() {
+        //#ifdef APP-PLUS
+        const sysPlugin = uni.requireNativePlugin("sysPlugin");
+        sysPlugin?.destroyClickSound?.();
+        //#endif
+    },
 };

+ 1 - 1
src/plugins/device/yx.plugins.js

@@ -29,5 +29,5 @@ export default {
         const yxPlugin = uni.requireNativePlugin("yxPlugin");
         yxPlugin.setNavBarNew(value);
         //#endif
-    }
+    },
 };

+ 12 - 7
src/plugins/index.js

@@ -1,5 +1,4 @@
 import tab from "./tab.plugins";
-import auth from "./auth.plugins";
 import modal from "./modal.plugins";
 import common from "./common.plugins";
 import setting from "./setting.plugins";
@@ -11,9 +10,11 @@ import permission from "./permission.plugins.js";
 import yx from "./device/yx.plugins.js";
 import ph from "./device/ph.plugins.js";
 import sys from "./device/sys.plugins.js";
+import mqtt from "./device/mqtt.plugins.js";
+import upgrade from "./device/upgrade.plugins.js";
+import appGuard from "./device/appGuard.plugins.js";
 
 import config from "@/config"; // config
-import { useDict } from '@/utils/dict'
 import { settingStores } from "@/store/modules/index";
 
 import dayjs from 'dayjs'
@@ -35,9 +36,6 @@ export default {
     // 页签操作
     app.provide("$tab", tab);
     app.config.globalProperties.$tab = tab;
-    // 认证对象
-    app.provide("$auth", auth);
-    app.config.globalProperties.$auth = auth;
     // 模态框对象
     app.provide("$modal", modal);
     app.config.globalProperties.$modal = modal;
@@ -62,8 +60,6 @@ export default {
     // 公共时间处理方法dayjs
     app.provide("$dayjs", dayjs);
     app.config.globalProperties.$dayjs = dayjs;
-    // 公共字典模块
-    app.config.globalProperties.useDict = useDict
     // 公共物理按钮监听
     app.provide("$keyListen", keyListen);
     app.config.globalProperties.$keyListen = keyListen;
@@ -83,5 +79,14 @@ export default {
     // 系统硬件api
     app.provide("$sys", sys);
     app.config.globalProperties.$sys = sys;
+    // 原生MQTT插件api
+    app.provide("$mqtt", mqtt);
+    app.config.globalProperties.$mqtt = mqtt;
+    // Root 静默安装
+    app.provide("$upgrade", upgrade);
+    app.config.globalProperties.$upgrade = upgrade;
+    // 应用守护(退后台/进程死亡拉起)
+    app.provide("$appGuard", appGuard);
+    app.config.globalProperties.$appGuard = appGuard;
   },
 };

+ 0 - 72
src/plugins/jsencrypt.js

@@ -1,72 +0,0 @@
-import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
-
-// 密钥对生成 http://web.chacuo.net/netrsakeypair
-
-const publicKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxPWP0HTBE9vEeM34Qx03U8oVm
-C6xIqWPRuI5t8J0zEDQudAgXKPjy8E0Q3cX800UNBTx2gUfRRNrONqALKDnJ1SE6
-qCUDeXOez8sa95GQ9d4BX7pSjZLrPfnCBTBtb5LGkY5zmlmtpG2AV9eJr+kQqhs/
-r0c4njwaDjVG4kF3ZQIDAQAB`
-
-const privateKey = `MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALE9Y/QdMET28R4z
-fhDHTdTyhWYLrEipY9G4jm3wnTMQNC50CBco+PLwTRDdxfzTRQ0FPHaBR9FE2s42
-oAsoOcnVITqoJQN5c57Pyxr3kZD13gFfulKNkus9+cIFMG1vksaRjnOaWa2kbYBX
-14mv6RCqGz+vRziePBoONUbiQXdlAgMBAAECgYBjSDdAXEVYrFdeiouYjHwdyAhP
-pERKo5BFvzMRhJIaM353cwnBJ3NkapVQ2Fn6iMIKTB+VZk+7eu1yTAkUluDfLowd
-REZS4ipOBY5UuNnjbXmSOoUQw6vRnox0X4x6S1vd4FBHgpVe1VkiE7Nz5U7Clyd5
-yw2P1lHwMyB/guAH4QJBAN3dGkMASj0jm23maHOfehp/zlACB8HpMKuV4z/bEg45
-nC9Hw5NloUHrXdzEXP1+S46MCH2THflxDVYtnZTRLO0CQQDMgp3Jrn7kkKtNceZF
-R08hLbVmfNlatgONgFJ5JnR+GTQ6o2gwM6SLyoBkfAIiEDpr6c6nBXTU09GOYxBk
-+h1ZAkB32pXxVBrG5JF20V3j+GcyIZEGz9H5A0xzpUlambIrVRv2vsH8wo5W2hue
-w8Woe629mBCOJgevVU9rGsFiP44RAkEApbTYAQjAjJakFpZJjKzg8vNEXoye2R9N
-9aOaL8v27A2kAjdRPm050IL+UW0hlVQs4i+KYE7NgX03+PVP3WHD0QJBANLo4PRw
-7Y+dLPAzuazsD3/5SYaSh+KSD/+tVbc6CFvLyfFUKp/a4PzzvGaLo/Ky/ffOY5k0
-hmavbHCKcg+r+hg=`
-
-
-const publicKey2 = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9GjiPyz3FfHcEAJu5mgYkaAyt
-xs1kSRIg7j9nFB4cUOKn5flsTwHjn+lR5D0MVc4xS9gtECgOMJsazqsxF35L5TKW
-i0GiX5zw9NhQz6Lv6P30mcm5mJs2UHOyeqr7rtDQn5Uun7Q9cfymQQ+ln7I54rCr
-GhTrkQzMw8+vhTVPKQIDAQAB`
-
-const privateKey2 = `MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAL0aOI/LPcV8dwQA
-m7maBiRoDK3GzWRJEiDuP2cUHhxQ4qfl+WxPAeOf6VHkPQxVzjFL2C0QKA4wmxrO
-qzEXfkvlMpaLQaJfnPD02FDPou/o/fSZybmYmzZQc7J6qvuu0NCflS6ftD1x/KZB
-D6WfsjnisKsaFOuRDMzDz6+FNU8pAgMBAAECgYAsJCzD1AttR+p4MrQnFP8ODIO5
-JMjY+f2TVknSg+2ram4eBx67241zVuadyQs/D+B7vVqO2lCfT1VFYqyvXu6J3YX0
-yagkQRiVLJLSMmP14ZCOlVA6jmit0MnMxG0XZltWnjpWyMmlGiNFnmK1Y84b913W
-k1H0mXKnCV2v5xD0AQJBAPQitqPCEPtLECXzdw3DBKNj8w1ldMNHU61dGKF/xDsQ
-6pbX5UZGeHBueqGKB2VqFFMtND6IlEVZ9kVTN1VIYEECQQDGStc+EMn3Xbixk2Al
-cMyfbGtpRQuGUk77uBf0HP7VMoL5j+t8KW3KflgKnY1AJa2VxoCk/Fx1tsdu2CbY
-qLTpAkAZ9PbmQmP70+dLoa8uz2VW2fGlovvfJ15GOHe99A+xGANZNmsyJZv4mEaB
-lmNi7PxsO4oqe+sH1KDSjh57+s4BAkEAp9YK73aobB7AyFT0iVw7ZikPlS+ivJOI
-VNkSNUYhj/TMFU3yxMoQKtfbz0hhmU0K6v30PzF3VQ9bKH/+CV5qmQJBAKhH5m6E
-af/baa6dQJbCCS8aVeiFe0AAUQKamAjkRdfUa1/W6O71A3ePsr41l0X4TGpXPapG
-dc/Ul5Q8wSTXkYE=`
-
-// 加密
-export function encrypt(txt) {
-    const encryptor = new JSEncrypt()
-    encryptor.setPublicKey(publicKey) // 设置公钥
-    return encryptor.encrypt(txt) // 对数据进行加密
-}
-
-// 解密
-export function decrypt(txt) {
-    const encryptor = new JSEncrypt()
-    encryptor.setPrivateKey(privateKey) // 设置私钥
-    return encryptor.decrypt(txt) // 对数据进行解密
-}
-
-// 加密c++业务(詹)
-export function encrypt2(txt) {
-    const encryptor = new JSEncrypt()
-    encryptor.setPublicKey(publicKey2) // 设置公钥
-    return encryptor.encrypt(txt) // 对数据进行加密
-}
-
-// 解密c++业务(詹)
-export function decrypt2(txt) {
-    const encryptor = new JSEncrypt()
-    encryptor.setPrivateKey(privateKey2) // 设置私钥
-    return encryptor.decrypt(txt) // 对数据进行解密
-}

+ 14 - 17
src/plugins/keyListen.plugins.js

@@ -90,28 +90,25 @@ export default {
         main.stopLockTask()//按返回键恢复
         main.startLockTask()//阻止系统home建和近期任务键
     },
-    // 启动APP
-    launchApp(event) {
-        var isApp = plus.runtime.isApplicationExist({
-            //查看安卓系统手机有没有下载这款app
-            pname: event.packageName //本地浏览器的包名
-        })
-
-        if (isApp) {
-            //安装了app则运行
+    /**
+     * 重启本应用(设默认主屏幕后或 Home 键拉回时使用)
+     * @param {{ packageName?: string }} event
+     */
+    restartApp(event = {}) {
+        const pname = event.packageName;
+        if (pname && plus.runtime.isApplicationExist({ pname })) {
             plus.runtime.launchApplication(
-                {
-                    pname: event.packageName,
-                    newTask: false
-                },
-                (e) => {
-                    console.log("e", e);
-                }
+                { pname, newTask: false },
+                (e) => console.log("[keyListen] launchApplication", e),
             );
         }
-        plus.runtime.launcher('shortcut');
+        plus.runtime.launcher("shortcut");
         plus.runtime.restart();
     },
+    // 启动APP(Home / 多任务键)
+    launchApp(event) {
+        this.restartApp(event);
+    },
     // 退出APP
     quitApp() {
         plus.runtime.quit();

+ 268 - 243
src/plugins/nfc.plugins.js

@@ -1,14 +1,20 @@
 import tab from "./tab.plugins";
 import { settingStores } from "@/store/modules/index";
 
-var NfcAdapter;
-var NdefRecord;
-var NdefMessage;
-var waiting;
-var readyWriteData = false; //开启写
-var readyRead = false; //开启读
-var whetherNFC = false; //是否有NFC
-var techListsArray = [
+// 状态管理
+const state = {
+    readyWriteData: false, // 开启写
+    readyRead: false, // 开启读
+    whetherNFC: false, // 是否有NFC
+    isListeningCard: false, // 是否正在监听卡号
+    cardNumberCallback: null, // 卡号回调函数
+    nfcManager: null, // NFC管理器
+    nfcAdapterInstance: null, // NFC适配器实例
+    newIntentHandler: null, // 事件监听器引用,用于清理
+};
+
+// NFC技术列表
+const TECH_LISTS_ARRAY = [
     ["android.nfc.tech.IsoDep"],
     ["android.nfc.tech.NfcA"],
     ["android.nfc.tech.NfcB"],
@@ -20,279 +26,298 @@ var techListsArray = [
     ["android.nfc.tech.MifareUltralight"],
 ];
 
-export default {
-    initNFC() {
-        if (uni.getSystemInfoSync().platform == "android") {
-            listenNFCStatus()
+/**
+ * 获取运行时主Activity
+ * @returns {Object} Activity实例
+ */
+function getMainActivity() {
+    return plus.android.runtimeMainActivity();
+}
+
+/**
+ * 检查是否为Android平台
+ * @returns {boolean}
+ */
+function isAndroidPlatform() {
+    return uni.getSystemInfoSync().platform === "android";
+}
+
+/**
+ * 处理NFC数据读取
+ * @returns {Promise}
+ */
+function handleNFCData() {
+    return new Promise((resolve, reject) => {
+        try {
+            const main = getMainActivity();
+            const intent = main.getIntent();
+            const action = intent.getAction();
+
+            if (action === "android.nfc.action.TECH_DISCOVERED") {
+                if (state.readyWriteData) {
+                    // 写入逻辑(待实现)
+                    state.readyWriteData = false;
+                    reject(new Error("写入功能暂未实现"));
+                } else if (state.readyRead) {
+                    // 读取逻辑(待实现)
+                    state.readyRead = false;
+                    // 这里应该调用实际的读取函数
+                    resolve({ success: true, data: null });
+                } else {
+                    reject(new Error("NFC未处于读取或写入状态"));
+                }
+            } else {
+                reject(new Error(`不支持的NFC动作: ${action}`));
+            }
+        } catch (error) {
+            console.error("处理NFC数据失败:", error);
+            reject(error);
         }
+    });
+}
+
+/**
+ * 关闭NFC读写
+ */
+function closeReadAndWrite() {
+    state.readyWriteData = false;
+    state.readyRead = false;
+
+    // 移除事件监听器
+    if (state.newIntentHandler) {
+        plus.globalEvent.removeEventListener("newintent", state.newIntentHandler);
+        state.newIntentHandler = null;
+    }
+}
 
-        if (uni.getSystemInfoSync().platform == "ios") {
+export default {
+    /**
+     * 初始化NFC
+     * @returns {boolean} 是否初始化成功
+     */
+    initNFC() {
+        if (!isAndroidPlatform()) {
             uni.showToast({
                 title: "设备不支持NFC!",
                 icon: "none",
             });
-            return;
+            return false;
         }
+
+        return this.systemLevelNfcCheck();
     },
+
+    /**
+     * 读取NFC数据
+     * @param {Function} callback - 回调函数(可选,已废弃,使用Promise)
+     * @returns {Promise} 返回读取的数据
+     */
     readNFC(callback) {
         return new Promise((resolve, reject) => {
-            if (uni.getSystemInfoSync().platform == "android") {
-                plus.globalEvent.addEventListener("newintent", function () {
-                    readyRead = true; //开启读
-                    plus.device.vibrate(500); //调用手机震动
-                    settingStores().nfcWaiting = '请将手机靠近NFC标签'
-                    handleNFCData().then((event) => {
-                        resolve(event)
-                    })
-                }, false);
+            if (!isAndroidPlatform()) {
+                reject(new Error("当前平台不支持NFC"));
+                return;
+            }
+
+            // 如果已有监听器,先移除
+            if (state.newIntentHandler) {
+                plus.globalEvent.removeEventListener("newintent", state.newIntentHandler);
             }
-        })
+
+            // 创建事件处理函数
+            state.newIntentHandler = () => {
+                try {
+                    state.readyRead = true;
+                    plus.device.vibrate(500); // 调用手机震动
+                    settingStores().nfcWaiting = '请将手机靠近NFC标签';
+
+                    handleNFCData()
+                        .then((event) => {
+                            resolve(event);
+                            // 读取完成后清理
+                            closeReadAndWrite();
+                        })
+                        .catch((error) => {
+                            reject(error);
+                            closeReadAndWrite();
+                        });
+                } catch (error) {
+                    console.error("读取NFC数据异常:", error);
+                    reject(error);
+                    closeReadAndWrite();
+                }
+            };
+
+            // 添加事件监听器
+            plus.globalEvent.addEventListener("newintent", state.newIntentHandler, false);
+        });
     },
+
+    /**
+     * 关闭NFC
+     */
     closeNFC() {
-        if (uni.getSystemInfoSync().platform == "android") {
+        if (isAndroidPlatform()) {
             closeReadAndWrite();
         }
-    }
-}
+    },
 
-function listenNFCStatus() {
-    try {
-        var main = plus.android.runtimeMainActivity();
-        var Intent = plus.android.importClass("android.content.Intent");
-        var Activity = plus.android.importClass("android.app.Activity");
-        var PendingIntent = plus.android.importClass("android.app.PendingIntent");
-        var IntentFilter = plus.android.importClass("android.content.IntentFilter");
-        NfcAdapter = plus.android.importClass("android.nfc.NfcAdapter");
-        var nfcAdapter = NfcAdapter.getDefaultAdapter(main);
-
-        if (nfcAdapter == null) {
-            uni.showToast({
-                title: "设备不支持NFC!",
-                icon: "none",
-            });
-            return;
-        }
+    /**
+     * 系统级NFC检测
+     * @returns {boolean} 检测是否通过
+     */
+    systemLevelNfcCheck() {
+        try {
+            const main = getMainActivity();
+            const Context = plus.android.importClass("android.content.Context");
+            const NfcManager = plus.android.importClass("android.nfc.NfcManager");
+
+            console.log("NfcManager", NfcManager)
+
+            const nfcManager = main.getSystemService(Context.NFC_SERVICE);
+            if (!nfcManager) {
+                console.warn("NFC服务不可用");
+                return false;
+            }
+
+            // 检查系统属性(可选,用于调试)
+            try {
+                const SystemProperties = plus.android.importClass("android.os.SystemProperties");
+                const nfcProperty = SystemProperties.get("ro.nfc.port", "");
+                console.log("NFC系统属性:", nfcProperty);
+            } catch (e) {
+                console.log("无法读取NFC系统属性(非关键错误)");
+            }
 
-        if (!nfcAdapter.isEnabled()) {
+            // 监听NFC状态
+            return this.listenNFCStatus();
+        } catch (error) {
+            console.error("系统级NFC检测失败:", error);
             uni.showToast({
-                title: "请在系统设置中先启用NFC功能!",
-                icon: "none",
+                title: "NFC检测失败",
+                icon: "none"
             });
-            return;
+            return false;
         }
+    },
 
-        var intent = new Intent(main, main.getClass());
-        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
-        var pendingIntent = PendingIntent.getActivity(main, 0, intent, 0);
-        var ndef = new IntentFilter("android.nfc.action.TECH_DISCOVERED");
-        ndef.addDataType("*/*");
-        var intentFiltersArray = [ndef];
-
-        plus.globalEvent.addEventListener(
-            "pause",
-            function (e) {
-                if (nfcAdapter) {
-                    //关闭前台调度系统
-                    //恢复默认状态
-                    nfcAdapter.disableForegroundDispatch(main);
-                }
-            },
-            false
-        );
-        plus.globalEvent.addEventListener(
-            "resume",
-            function (e) {
-                if (nfcAdapter) {
-                    //开启前台调度系统
-                    // 优于所有其他NFC
-                    nfcAdapter.enableForegroundDispatch(main, pendingIntent, intentFiltersArray, techListsArray);
-                }
-            },
-            false
-        );
-        nfcAdapter.enableForegroundDispatch(main, pendingIntent, intentFiltersArray, techListsArray);
-    } catch (e) {
-        console.error(e);
-    }
-}
+    /**
+     * 监听NFC状态
+     * @returns {boolean} 状态是否正常
+     */
+    listenNFCStatus() {
+        try {
+            const main = getMainActivity();
+            const NfcAdapter = plus.android.importClass("android.nfc.NfcAdapter");
+            const nfcAdapter = NfcAdapter.getDefaultAdapter(main);
 
-function handleNFCData() {
-    return new Promise((resolve, reject) => {
-        NdefRecord = plus.android.importClass("android.nfc.NdefRecord");
-        NdefMessage = plus.android.importClass("android.nfc.NdefMessage");
-        var main = plus.android.runtimeMainActivity();
-        var intent = main.getIntent();
-        if ("android.nfc.action.TECH_DISCOVERED" == intent.getAction()) {
-            if (readyWriteData) {
-                //__write(intent);
-                readyWriteData = false;
-            } else if (readyRead) {
-                __read(intent).then((event) => {
-                    resolve(event)
+            console.log("NfcAdapter", nfcAdapter)
+
+            // 检查设备是否支持NFC
+            if (nfcAdapter === null) {
+                uni.showModal({
+                    title: "NFC不支持",
+                    content: "当前设备不支持NFC功能,无法使用刷卡功能",
+                    showCancel: false
                 });
-                readyRead = false;
+                state.whetherNFC = false;
+                return false;
             }
-        }
-    })
-}
 
-/**
- * @NFC写入
- * @param {*} intent
- * @returns
- */
-function __write(intent) {
-    try {
-        waiting.setTitle("请勿移开标签\n正在写入...");
-        var text = document.getElementById("text").value;
-        console.log("text=" + text);
-
-        var textBytes = plus.android.invoke(text, "getBytes");
-        var textRecord = new NdefRecord(NdefRecord.TNF_MIME_MEDIA, plus.android.invoke("text/plain", "getBytes"), plus.android.invoke("", "getBytes"), textBytes);
-        var message = new NdefMessage([textRecord]);
-        var Ndef = plus.android.importClass("android.nfc.tech.Ndef");
-        var NdefFormatable = plus.android.importClass("android.nfc.tech.NdefFormatable");
-        var tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
-        var ndef = Ndef.get(tag);
-        if (ndef != null) {
-            var size = message.toByteArray().length;
-            console.log("size=" + size);
-            ndef.connect();
-            if (!ndef.isWritable()) {
-                showToast("tag不允许写入");
-                waiting.close();
-                return;
-            }
-            if (ndef.getMaxSize() < size) {
-                showToast("文件大小超出容量");
-                waiting.close();
-                return;
+            // 检查NFC是否启用
+            if (!nfcAdapter.isEnabled()) {
+                uni.showModal({
+                    title: "NFC未开启",
+                    content: "请在系统设置中开启NFC功能后重试",
+                    confirmText: "去设置",
+                    success: (res) => {
+                        if (res.confirm) {
+                            // 跳转到NFC设置页面
+                            const Intent = plus.android.importClass("android.content.Intent");
+                            const Settings = plus.android.importClass("android.provider.Settings");
+                            const intent = new Intent(Settings.ACTION_NFC_SETTINGS);
+                            main.startActivity(intent);
+                        }
+                    }
+                });
+                state.whetherNFC = false;
+                return false;
             }
 
-            ndef.writeNdefMessage(message);
-            waiting.close();
-            showToast("写入数据成功.");
-            return;
-        } else {
-            var format = NdefFormatable.get(tag);
-            if (format != null) {
-                try {
-                    format.connect();
-                    format.format(message);
-                    showToast("格式化tag并且写入message");
-                    waiting.close();
-                    return;
-                } catch (e) {
-                    showToast("格式化tag失败.");
-                    waiting.close();
-                    return;
-                }
-            } else {
-                showToast("Tag不支持NDEF");
-                waiting.close();
-                return;
-            }
-        }
-    } catch (e) {
-        console.log("error=" + e);
-        waiting.close();
-        alert("写入失败");
-    }
-}
+            // NFC正常,设置前台调度
+            state.whetherNFC = true;
+            this.setupNfcForegroundDispatch();
+            return true;
 
-/**
- * @NFC读取
- * @param {*} intent
- * @returns
- */
-function __read(intent) {
-    return new Promise((resolve, reject) => {
-        try {
-            settingStores().nfcWaiting = '请勿移开标签\n正在读取数据...'
-            // waiting.setTitle("请勿移开标签\n正在读取数据...");
-            var tag = plus.android.importClass("android.nfc.Tag");
-            tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
-            var bytesId = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
-            // waiting.close();
-            var tagid = bytesToHexString(tag.getId());
-
-            resolve(tagid)
-        } catch (e) {
+        } catch (error) {
+            console.error("NFC检测异常:", error);
             uni.showToast({
-                title: e,
-                icon: "none",
+                title: "NFC功能异常",
+                icon: "none"
             });
+            state.whetherNFC = false;
+            return false;
         }
-    })
-}
+    },
 
-/**
- * @十六进制转换
- * @param { 值 } inarray 
- * @returns 
- */
-function bytesToHexString(inarray) {
-    var i, j, x;
-    var hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
-    var out = "";
-    for (j = 0; j < inarray.length; ++j) {
-        x = parseInt(inarray[j]) & 0xff;
-        i = (x >> 4) & 0x0f;
-        out += hex[i];
-        i = x & 0x0f;
-        out += hex[i];
-    }
-    return out;
-}
+    /**
+     * 设置NFC前台调度
+     * @returns {boolean} 设置是否成功
+     */
+    setupNfcForegroundDispatch() {
+        try {
+            const main = getMainActivity();
+            const NfcAdapter = plus.android.importClass("android.nfc.NfcAdapter");
+            const Intent = plus.android.importClass("android.content.Intent");
+            const PendingIntent = plus.android.importClass("android.app.PendingIntent");
+            const IntentFilter = plus.android.importClass("android.content.IntentFilter");
 
-function reverseTwo(str) {
-    var str1 = "";
-    for (var i = 1; i <= str.length; i++) {
-        str1 += str[i - 1];
-        if (i % 2 == 0) {
-            if (i == str.length) {
-                break;
+            const nfcAdapter = NfcAdapter.getDefaultAdapter(main);
+            if (!nfcAdapter) {
+                console.error("无法获取NFC适配器");
+                return false;
             }
-            str1 += ":";
-        }
-    }
-    var str2 = "";
-    for (var i = str1.split(":").length - 1; i >= 0; i--) {
-        str2 += str1.split(":")[i];
-    }
-    return str2;
-}
 
-if (uni.getSystemInfoSync().platform == "android") {
-    //plus.globalEvent.addEventListener('plusready', listenNFCStatus, false);
-}
+            // 创建PendingIntent
+            const intent = new Intent(main, main.getClass());
+            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+            const pendingIntent = PendingIntent.getActivity(main, 0, intent, 0);
 
-function writeData() {
-    var textEle = plus.globalEvent.getElementById("text");
-    if (!textEle.value) {
-        uni.showToast({
-            title: "请输入要写入的内容!",
-            icon: "none",
-        });
-        return;
-    }
-    readyWriteData = true;
-    settingStores().nfcWaiting = '请将手机靠近NFC标签'
-    // waiting = plus.nativeUI.showWaiting("请将手机靠近NFC标签!");
-}
+            // 创建Intent过滤器
+            const ndefFilter = new IntentFilter("android.nfc.action.NDEF_DISCOVERED");
+            const tagFilter = new IntentFilter("android.nfc.action.TAG_DISCOVERED");
+            const techFilter = new IntentFilter("android.nfc.action.TECH_DISCOVERED");
 
-function showToast(msg) {
-    plus.nativeUI.toast(msg);
-}
+            try {
+                ndefFilter.addDataType("*/*");
+            } catch (e) {
+                console.warn("NDEF过滤器设置失败(非关键错误):", e);
+            }
 
-/**
- * @关闭NFC
- */
-function closeReadAndWrite() {
-    readyWriteData = false;
-    readyRead = false;
+            const intentFiltersArray = [ndefFilter, tagFilter, techFilter];
+
+            // 启用前台调度
+            nfcAdapter.enableForegroundDispatch(main, pendingIntent, intentFiltersArray, null);
+
+            console.log("NFC前台调度设置成功");
+            return true;
+
+        } catch (error) {
+            console.error("设置NFC前台调度失败:", error);
+            return false;
+        }
+    },
 
-    if (waiting) {
-        waiting.close();
+    /**
+     * 获取NFC状态
+     * @returns {Object} NFC状态信息
+     */
+    getNFCState() {
+        return {
+            whetherNFC: state.whetherNFC,
+            readyRead: state.readyRead,
+            readyWriteData: state.readyWriteData,
+            isListeningCard: state.isListeningCard
+        };
     }
 }

+ 0 - 54
src/static/face/css/door_homeCard.css

@@ -1,54 +0,0 @@
-@font-face {
-    font-family: 'Alibaba PuHuiTi';
-    src: url('../font/Alibaba-PuHuiTi-Medium.ttf') format('truetype');
-    font-weight: normal;
-    font-style: normal;
-}
-
-@font-face {
-    font-family: 'Bebas Neue';
-    src: url('../font/BebasNeue.otf');
-    font-weight: normal;
-    font-style: normal;
-}
-
-html,
-body {
-    font-family: Alibaba PuHuiTi;
-    width: 100%;
-    height: 100%;
-    margin: 0;
-    /* Safari */
-    -webkit-user-select: none;
-    /* Firefox */
-    -moz-user-select: none;
-    /* IE10+/Edge */
-    -ms-user-select: none;
-    /* Standard syntax */
-    user-select: none;
-
-    ::-webkit-scrollbar {
-        display: none;
-    }
-}
-
-.home-card {
-    color: #fff;
-    width: 100%;
-    height: 100%;
-    text-align: center;
-    background-color: #fff;
-}
-
-.home-card-image {
-    position: fixed;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    width: 100%;
-    height: 100%;
-    background: url(../img/face_bg.png) no-repeat;
-    background-size: 100% 100%;
-    z-index: 1;
-}

+ 0 - 61
src/static/face/css/door_homeCardCenter.css

@@ -1,61 +0,0 @@
-.home-card-center,
-.home-card-center>.face {
-    position: relative;
-    width: 100%;
-    height: 100%;
-    z-index: 10;
-}
-
-.home-card-center>.date {
-    font-size: 1.5rem;
-    position: relative;
-    top: calc(40% - 20vh / 2);
-    display: flex;
-    flex-wrap: wrap;
-}
-
-.home-card-center>.date .title {
-    width: 100%;
-    text-align: center;
-    margin-bottom: 1.5rem;
-}
-
-.home-card-center>.date .remark {
-    width: 100%;
-    font-size: 1rem;
-    text-align: center;
-    margin: 0 auto 1.5rem auto;
-    padding: 0 2rem;
-}
-
-.home-card-center>.date .workStatus {
-    font-size: 1.5rem;
-    margin: 0 auto;
-    padding: 5px 15px;
-    color: white;
-    border-radius: 10px;
-}
-
-video,
-canvas {
-    width: 40vh;
-    height: 40vh;
-    position: absolute;
-    top: calc(40% - 40vh / 2);
-    left: calc(50% - 40vh / 2);
-    border-radius: 100%;
-    /* 视频内容填充元素的整个内容框,保持视频的宽高比并将视频内容铺满整个内容框,如果视频的宽高比和内容框不一致,视频会被裁剪 */
-    object-fit: cover;
-}
-
-.specialEffects {
-    width: 40vh;
-    height: 40vh;
-    position: absolute;
-    z-index: 1007 !important;
-    transform: translate(-50%, -50%);
-    top: 40%;
-    left: 50%;
-    background: url(../img/face_vef.png) no-repeat;
-    background-size: 40vh 40vh;
-}

+ 0 - 24
src/static/face/css/door_homeCardFooter.css

@@ -1,24 +0,0 @@
-.home-card-footer {
-    font-family: Bebas Neue;
-    position: absolute;
-    bottom: 0;
-    z-index: 1008;
-    display: flex;
-    width: 100%;
-    font-size: 1rem;
-    margin-bottom: 2rem;
-}
-
-.home-card-footer .qrCode {
-    /* width: 40%; */
-}
-
-.home-card-footer .qrCode .buttom {
-    padding: 0.7rem;
-    border-radius: 5px;
-    background-color: #1A5FDE;
-}
-
-.home-card-footer .btnArea {
-    margin: 0 auto;
-}

+ 0 - 26
src/static/face/css/door_homeCardTop.css

@@ -1,26 +0,0 @@
-.home-card-top {
-    font-family: Bebas Neue;
-    display: flex;
-    position: absolute;
-    width: calc(100% - (0.5rem * 4));
-    margin: 0.5rem;
-    padding: 1rem 0.5rem;
-    /* background: #0c2f61; */
-    border-radius: 15px;
-    z-index: 1008;
-}
-
-.home-card-top .date {
-    margin: auto auto auto 0;
-}
-
-.home-card-top .date .time1 {
-    margin-right: 0.5rem;
-    font-size: 2rem;
-}
-
-.home-card-top .title {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-}

+ 0 - 454
src/static/face/door.html

@@ -1,454 +0,0 @@
-<!doctype html>
-<html>
-
-<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>
-    <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_homeCardTop.css">
-    <!-- 中心区域样式 -->
-    <link rel="stylesheet" href="./css/door_homeCardCenter.css">
-    <!-- 底部区域样式 -->
-    <link rel="stylesheet" href="./css/door_homeCardFooter.css">
-    <!-- 引入阿里iconfont库 -->
-    <link rel="stylesheet" href="https://at.alicdn.com/t/c/font_4400427_yp2hzj2yvbf.css">
-    <style>
-        .home-card-footer .btnArea .iconfont {
-            font-family: "iconfont" !important;
-            font-size: 2rem;
-            font-style: normal;
-            -webkit-font-smoothing: antialiased;
-            -webkit-text-stroke-width: 0.2px;
-            -moz-osx-font-smoothing: grayscale;
-            color: white;
-        }
-
-        .home-card-footer .btnArea .iconfont:nth-child(2n) {
-            margin-left: 1rem;
-        }
-
-        .workRed {
-            background-color: #f11e16;
-        }
-
-        .workYellow {
-            background-color: #E6A23C;
-        }
-
-        .workGreen {
-            background-color: #67C23A;
-        }
-
-        @media (min-width: 768px) {
-            .home-card-top {
-                width: calc(100% - 5rem) !important;
-                margin: 1rem !important;
-                padding: 2rem 1.5rem !important;
-            }
-
-            .home-card-top .date .time1 {
-                margin-right: 1.5rem !important;
-                font-size: 5rem !important;
-            }
-
-            .home-card-top .date .time2 {
-                font-size: 2.5rem !important;
-            }
-
-            .home-card-top .title {
-                font-size: 2rem !important;
-            }
-
-            .home-card-center>.date {
-                font-size: 4rem;
-            }
-
-            .home-card-center>.date .title {
-                margin-bottom: 5rem;
-            }
-
-            .home-card-center>.date .remark {
-                font-size: 2rem;
-                margin-bottom: 5rem;
-            }
-
-            .home-card-center>.date .workStatus {
-                font-size: 3rem;
-                padding: 10px 30px;
-            }
-
-            .home-card-footer {
-                font-size: 2.5rem !important;
-                margin-bottom: 4rem !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;
-            }
-
-            .home-card-footer .btnArea .iconfont {
-                font-size: 5.5rem;
-            }
-
-            .home-card-footer .btnArea .iconfont:nth-child(2n) {
-                margin-left: 3rem;
-            }
-        }
-    </style>
-    <script>
-        // 提前占位,防止宿主在页面初始化前调用 receiveData 导致未定义
-        window.__pendingReceiveData = [];
-        window.receiveData = function (msg) {
-            window.__pendingReceiveData.push(msg);
-        };
-    </script>
-</head>
-
-<body>
-    <div id="face-container" class="face-container home-card">
-        <!-- 背景图片 -->
-        <div class="home-card-image"
-            :style="{backgroundImage: `url('${state.imgPath || 'img/face_bg.png'}')`, backgroundSize: '100% 100%', backgroundRepeat: 'no-repeat'}">
-        </div>
-        <!-- 顶部内容区域 -->
-        <div class="home-card-top">
-            <div class="date" id="configDialog2">
-                <span class="time1">{{ state.dateTime }}</span>
-                <span class="time2">{{ state.date }}</span>
-            </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 || !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" v-if="state.workName">{{ state.workName }}</div>
-            </div>
-            <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>
-                <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="btnArea">
-                <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>
-    <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: "",
-                        remark: "",
-                        workName: "",
-                        workClass: ""
-                    },
-                    timeOutEvent: 0,
-                    inter: {
-                        dateDom: null
-                    },
-                    workStatusData: [
-                        { label: "办公", value: 1, class: 'workRed' },
-                        { label: "会客", value: 2, class: 'workRed' },
-                        { label: "外出", value: 3, class: 'workGreen' },
-                        { label: "勿扰", value: 4, class: 'workRed' },
-                    ],
-                };
-            },
-            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
-                    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 && event.openMode.includes("人脸")) {
-                        that.tracker == null ? that.initVido() : undefined
-                    } else {
-                        that.tracker != null ? that.closeFace() : undefined
-                    }
-                },
-                // 初始化事件
-                initHandle() {
-                    var that = this;
-
-                    $("#configDialog1,#configDialog2").on({
-                        touchstart: function (e) {
-                            that.timeOutEvent = setTimeout(() => {
-                                that.parentMessage('打开门禁配置')
-                                that.timeOutEvent = 0
-                            }, 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 == "开启摄像头" && document.getElementById("video")) {
-                            this.initVido();//调用初始化摄像头
-                        } else if (event.funcName == "关闭摄像头") {
-                            this.closeFace();
-                        } else if (event.funcName == "人脸冷却") {
-                            this.faceCooling();
-                        }
-                    }
-                },
-                // 监听页面是否隐藏
-                handleVisibilityChange() {
-                    if (document.visibilityState === 'visible') {
-                        // 页面变为可见时的处理逻辑
-                        console.log('页面变为可见');
-                        this.tracker == null && document.getElementById("video") ? this.initVido() : undefined
-                    } else if (document.visibilityState === 'hidden') {
-                        // 页面变为不可见时的处理逻辑
-                        console.log('页面变为不可见');
-                        this.tracker != null && document.getElementById("video") ? 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);
-                }
-
-                this.initHandle();//初始化事件dom
-            },
-            beforeDestroy() {
-                // 移除window方法
-                window.receiveData = null;
-                clearInterval(inter.dateDom); //销毁之前定时器
-            },
-            watch: {},
-        }).mount('#face-container');
-    </script>
-</body>
-
-</html>

BIN
src/static/face/font/Alibaba-PuHuiTi-Medium.ttf


BIN
src/static/face/font/BebasNeue.otf


BIN
src/static/face/img/face.gif


BIN
src/static/face/img/face_detection.gif


BIN
src/static/face/img/face_vef.gif


BIN
src/static/face/img/face_vef.png


BIN
src/static/face/img/otherSetting.png


BIN
src/static/face/img/serveSetting.png


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 7
src/static/face/js/face_data/eye-min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 7
src/static/face/js/face_data/eye.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 7
src/static/face/js/face_data/face.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 7
src/static/face/js/face_data/mouth-min.js


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 7
src/static/face/js/face_data/mouth.js


+ 0 - 3111
src/static/face/js/tracking.js

@@ -1,3111 +0,0 @@
-/**
- * tracking - A modern approach for Computer Vision on the web.
- * @author Eduardo Lundgren <edu@rdo.io>
- * @version v1.1.3
- * @link http://trackingjs.com
- * @license BSD
- */
-(function(window, undefined) {
-  window.tracking = window.tracking || {};
-
-  /**
-   * Inherit the prototype methods from one constructor into another.
-   *
-   * Usage:
-   * <pre>
-   * function ParentClass(a, b) { }
-   * ParentClass.prototype.foo = function(a) { }
-   *
-   * function ChildClass(a, b, c) {
-   *   tracking.base(this, a, b);
-   * }
-   * tracking.inherits(ChildClass, ParentClass);
-   *
-   * var child = new ChildClass('a', 'b', 'c');
-   * child.foo();
-   * </pre>
-   *
-   * @param {Function} childCtor Child class.
-   * @param {Function} parentCtor Parent class.
-   */
-  tracking.inherits = function(childCtor, parentCtor) {
-    function TempCtor() {
-    }
-    TempCtor.prototype = parentCtor.prototype;
-    childCtor.superClass_ = parentCtor.prototype;
-    childCtor.prototype = new TempCtor();
-    childCtor.prototype.constructor = childCtor;
-
-    /**
-     * Calls superclass constructor/method.
-     *
-     * This function is only available if you use tracking.inherits to express
-     * inheritance relationships between classes.
-     *
-     * @param {!object} me Should always be "this".
-     * @param {string} methodName The method name to call. Calling superclass
-     *     constructor can be done with the special string 'constructor'.
-     * @param {...*} var_args The arguments to pass to superclass
-     *     method/constructor.
-     * @return {*} The return value of the superclass method/constructor.
-     */
-    childCtor.base = function(me, methodName) {
-      var args = Array.prototype.slice.call(arguments, 2);
-      return parentCtor.prototype[methodName].apply(me, args);
-    };
-  };
-
-  /**
-   * Captures the user camera when tracking a video element and set its source
-   * to the camera stream.
-   * @param {HTMLVideoElement} element Canvas element to track.
-   * @param {object} opt_options Optional configuration to the tracker.
-   */
-  tracking.initUserMedia_ = function(element, opt_options) {
-    window.navigator.mediaDevices.getUserMedia({
-      video: true,
-      audio: (opt_options && opt_options.audio) ? true : false,
-    }).then(function(stream) {
-      element.srcObject = stream;
-    }).catch(function(err) {
-      throw Error('Cannot capture user camera.');
-    });
-  };
-
-  /**
-   * Tests whether the object is a dom node.
-   * @param {object} o Object to be tested.
-   * @return {boolean} True if the object is a dom node.
-   */
-  tracking.isNode = function(o) {
-    return o.nodeType || this.isWindow(o);
-  };
-
-  /**
-   * Tests whether the object is the `window` object.
-   * @param {object} o Object to be tested.
-   * @return {boolean} True if the object is the `window` object.
-   */
-  tracking.isWindow = function(o) {
-    return !!(o && o.alert && o.document);
-  };
-
-  /**
-   * Selects a dom node from a CSS3 selector using `document.querySelector`.
-   * @param {string} selector
-   * @param {object} opt_element The root element for the query. When not
-   *     specified `document` is used as root element.
-   * @return {HTMLElement} The first dom element that matches to the selector.
-   *     If not found, returns `null`.
-   */
-  tracking.one = function(selector, opt_element) {
-    if (this.isNode(selector)) {
-      return selector;
-    }
-    return (opt_element || document).querySelector(selector);
-  };
-
-  /**
-   * Tracks a canvas, image or video element based on the specified `tracker`
-   * instance. This method extract the pixel information of the input element
-   * to pass to the `tracker` instance. When tracking a video, the
-   * `tracker.track(pixels, width, height)` will be in a
-   * `requestAnimationFrame` loop in order to track all video frames.
-   *
-   * Example:
-   * var tracker = new tracking.ColorTracker();
-   *
-   * tracking.track('#video', tracker);
-   * or
-   * tracking.track('#video', tracker, { camera: true });
-   *
-   * tracker.on('track', function(event) {
-   *   // console.log(event.data[0].x, event.data[0].y)
-   * });
-   *
-   * @param {HTMLElement} element The element to track, canvas, image or
-   *     video.
-   * @param {tracking.Tracker} tracker The tracker instance used to track the
-   *     element.
-   * @param {object} opt_options Optional configuration to the tracker.
-   */
-  tracking.track = function(element, tracker, opt_options) {
-    element = tracking.one(element);
-    if (!element) {
-      throw new Error('Element not found, try a different element or selector.');
-    }
-    if (!tracker) {
-      throw new Error('Tracker not specified, try `tracking.track(element, new tracking.FaceTracker())`.');
-    }
-
-    switch (element.nodeName.toLowerCase()) {
-      case 'canvas':
-        return this.trackCanvas_(element, tracker, opt_options);
-      case 'img':
-        return this.trackImg_(element, tracker, opt_options);
-      case 'video':
-        if (opt_options) {
-          if (opt_options.camera) {
-            this.initUserMedia_(element, opt_options);
-          }
-        }
-        return this.trackVideo_(element, tracker, opt_options);
-      default:
-        throw new Error('Element not supported, try in a canvas, img, or video.');
-    }
-  };
-
-  /**
-   * Tracks a canvas element based on the specified `tracker` instance and
-   * returns a `TrackerTask` for this track.
-   * @param {HTMLCanvasElement} element Canvas element to track.
-   * @param {tracking.Tracker} tracker The tracker instance used to track the
-   *     element.
-   * @param {object} opt_options Optional configuration to the tracker.
-   * @return {tracking.TrackerTask}
-   * @private
-   */
-  tracking.trackCanvas_ = function(element, tracker) {
-    var self = this;
-    var task = new tracking.TrackerTask(tracker);
-    task.on('run', function() {
-      self.trackCanvasInternal_(element, tracker);
-    });
-    return task.run();
-  };
-
-  /**
-   * Tracks a canvas element based on the specified `tracker` instance. This
-   * method extract the pixel information of the input element to pass to the
-   * `tracker` instance.
-   * @param {HTMLCanvasElement} element Canvas element to track.
-   * @param {tracking.Tracker} tracker The tracker instance used to track the
-   *     element.
-   * @param {object} opt_options Optional configuration to the tracker.
-   * @private
-   */
-  tracking.trackCanvasInternal_ = function(element, tracker) {
-    var width = element.width;
-    var height = element.height;
-    var context = element.getContext('2d');
-    var imageData = context.getImageData(0, 0, width, height);
-    tracker.track(imageData.data, width, height);
-  };
-
-  /**
-   * Tracks a image element based on the specified `tracker` instance. This
-   * method extract the pixel information of the input element to pass to the
-   * `tracker` instance.
-   * @param {HTMLImageElement} element Canvas element to track.
-   * @param {tracking.Tracker} tracker The tracker instance used to track the
-   *     element.
-   * @param {object} opt_options Optional configuration to the tracker.
-   * @private
-   */
-  tracking.trackImg_ = function(element, tracker) {
-    var width = element.width;
-    var height = element.height;
-    var canvas = document.createElement('canvas');
-
-    canvas.width = width;
-    canvas.height = height;
-
-    var task = new tracking.TrackerTask(tracker);
-    task.on('run', function() {
-      tracking.Canvas.loadImage(canvas, element.src, 0, 0, width, height, function() {
-        tracking.trackCanvasInternal_(canvas, tracker);
-      });
-    });
-    return task.run();
-  };
-
-  /**
-   * Tracks a video element based on the specified `tracker` instance. This
-   * method extract the pixel information of the input element to pass to the
-   * `tracker` instance. The `tracker.track(pixels, width, height)` will be in
-   * a `requestAnimationFrame` loop in order to track all video frames.
-   * @param {HTMLVideoElement} element Canvas element to track.
-   * @param {tracking.Tracker} tracker The tracker instance used to track the
-   *     element.
-   * @param {object} opt_options Optional configuration to the tracker.
-   * @private
-   */
-  tracking.trackVideo_ = function(element, tracker) {
-    var canvas = document.createElement('canvas');
-    var context = canvas.getContext('2d');
-    var width;
-    var height;
-
-    var resizeCanvas_ = function() {
-      width = element.offsetWidth;
-      height = element.offsetHeight;
-      canvas.width = width;
-      canvas.height = height;
-    };
-    resizeCanvas_();
-    element.addEventListener('resize', resizeCanvas_);
-
-    var requestId;
-    var requestAnimationFrame_ = function() {
-      requestId = window.requestAnimationFrame(function() {
-        if (element.readyState === element.HAVE_ENOUGH_DATA) {
-          try {
-            // Firefox v~30.0 gets confused with the video readyState firing an
-            // erroneous HAVE_ENOUGH_DATA just before HAVE_CURRENT_DATA state,
-            // hence keep trying to read it until resolved.
-            context.drawImage(element, 0, 0, width, height);
-          } catch (err) {}
-          tracking.trackCanvasInternal_(canvas, tracker);
-        }
-        requestAnimationFrame_();
-      });
-    };
-
-    var task = new tracking.TrackerTask(tracker);
-    task.on('stop', function() {
-      window.cancelAnimationFrame(requestId);
-    });
-    task.on('run', function() {
-      requestAnimationFrame_();
-    });
-    return task.run();
-  };
-
-  // Browser polyfills
-  //===================
-
-  if (!window.URL) {
-    window.URL = window.URL || window.webkitURL || window.msURL || window.oURL;
-  }
-
-  if (!navigator.getUserMedia) {
-    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
-    navigator.mozGetUserMedia || navigator.msGetUserMedia;
-  }
-}(window));
-
-(function() {
-  /**
-   * EventEmitter utility.
-   * @constructor
-   */
-  tracking.EventEmitter = function() {};
-
-  /**
-   * Holds event listeners scoped by event type.
-   * @type {object}
-   * @private
-   */
-  tracking.EventEmitter.prototype.events_ = null;
-
-  /**
-   * Adds a listener to the end of the listeners array for the specified event.
-   * @param {string} event
-   * @param {function} listener
-   * @return {object} Returns emitter, so calls can be chained.
-   */
-  tracking.EventEmitter.prototype.addListener = function(event, listener) {
-    if (typeof listener !== 'function') {
-      throw new TypeError('Listener must be a function');
-    }
-    if (!this.events_) {
-      this.events_ = {};
-    }
-
-    this.emit('newListener', event, listener);
-
-    if (!this.events_[event]) {
-      this.events_[event] = [];
-    }
-
-    this.events_[event].push(listener);
-
-    return this;
-  };
-
-  /**
-   * Returns an array of listeners for the specified event.
-   * @param {string} event
-   * @return {array} Array of listeners.
-   */
-  tracking.EventEmitter.prototype.listeners = function(event) {
-    return this.events_ && this.events_[event];
-  };
-
-  /**
-   * Execute each of the listeners in order with the supplied arguments.
-   * @param {string} event
-   * @param {*} opt_args [arg1], [arg2], [...]
-   * @return {boolean} Returns true if event had listeners, false otherwise.
-   */
-  tracking.EventEmitter.prototype.emit = function(event) {
-    var listeners = this.listeners(event);
-    if (listeners) {
-      var args = Array.prototype.slice.call(arguments, 1);
-      for (var i = 0; i < listeners.length; i++) {
-        if (listeners[i]) {
-          listeners[i].apply(this, args);
-        }
-      }
-      return true;
-    }
-    return false;
-  };
-
-  /**
-   * Adds a listener to the end of the listeners array for the specified event.
-   * @param {string} event
-   * @param {function} listener
-   * @return {object} Returns emitter, so calls can be chained.
-   */
-  tracking.EventEmitter.prototype.on = tracking.EventEmitter.prototype.addListener;
-
-  /**
-   * Adds a one time listener for the event. This listener is invoked only the
-   * next time the event is fired, after which it is removed.
-   * @param {string} event
-   * @param {function} listener
-   * @return {object} Returns emitter, so calls can be chained.
-   */
-  tracking.EventEmitter.prototype.once = function(event, listener) {
-    var self = this;
-    self.on(event, function handlerInternal() {
-      self.removeListener(event, handlerInternal);
-      listener.apply(this, arguments);
-    });
-  };
-
-  /**
-   * Removes all listeners, or those of the specified event. It's not a good
-   * idea to remove listeners that were added elsewhere in the code,
-   * especially when it's on an emitter that you didn't create.
-   * @param {string} event
-   * @return {object} Returns emitter, so calls can be chained.
-   */
-  tracking.EventEmitter.prototype.removeAllListeners = function(opt_event) {
-    if (!this.events_) {
-      return this;
-    }
-    if (opt_event) {
-      delete this.events_[opt_event];
-    } else {
-      delete this.events_;
-    }
-    return this;
-  };
-
-  /**
-   * Remove a listener from the listener array for the specified event.
-   * Caution: changes array indices in the listener array behind the listener.
-   * @param {string} event
-   * @param {function} listener
-   * @return {object} Returns emitter, so calls can be chained.
-   */
-  tracking.EventEmitter.prototype.removeListener = function(event, listener) {
-    if (typeof listener !== 'function') {
-      throw new TypeError('Listener must be a function');
-    }
-    if (!this.events_) {
-      return this;
-    }
-
-    var listeners = this.listeners(event);
-    if (Array.isArray(listeners)) {
-      var i = listeners.indexOf(listener);
-      if (i < 0) {
-        return this;
-      }
-      listeners.splice(i, 1);
-    }
-
-    return this;
-  };
-
-  /**
-   * By default EventEmitters will print a warning if more than 10 listeners
-   * are added for a particular event. This is a useful default which helps
-   * finding memory leaks. Obviously not all Emitters should be limited to 10.
-   * This function allows that to be increased. Set to zero for unlimited.
-   * @param {number} n The maximum number of listeners.
-   */
-  tracking.EventEmitter.prototype.setMaxListeners = function() {
-    throw new Error('Not implemented');
-  };
-
-}());
-
-(function() {
-  /**
-   * Canvas utility.
-   * @static
-   * @constructor
-   */
-  tracking.Canvas = {};
-
-  /**
-   * Loads an image source into the canvas.
-   * @param {HTMLCanvasElement} canvas The canvas dom element.
-   * @param {string} src The image source.
-   * @param {number} x The canvas horizontal coordinate to load the image.
-   * @param {number} y The canvas vertical coordinate to load the image.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {function} opt_callback Callback that fires when the image is loaded
-   *     into the canvas.
-   * @static
-   */
-  tracking.Canvas.loadImage = function(canvas, src, x, y, width, height, opt_callback) {
-    var instance = this;
-    var img = new window.Image();
-    img.crossOrigin = '*';
-    img.onload = function() {
-      var context = canvas.getContext('2d');
-      canvas.width = width;
-      canvas.height = height;
-      context.drawImage(img, x, y, width, height);
-      if (opt_callback) {
-        opt_callback.call(instance);
-      }
-      img = null;
-    };
-    img.src = src;
-  };
-}());
-
-(function() {
-  /**
-   * DisjointSet utility with path compression. Some applications involve
-   * grouping n distinct objects into a collection of disjoint sets. Two
-   * important operations are then finding which set a given object belongs to
-   * and uniting the two sets. A disjoint set data structure maintains a
-   * collection S={ S1 , S2 ,..., Sk } of disjoint dynamic sets. Each set is
-   * identified by a representative, which usually is a member in the set.
-   * @static
-   * @constructor
-   */
-  tracking.DisjointSet = function(length) {
-    if (length === undefined) {
-      throw new Error('DisjointSet length not specified.');
-    }
-    this.length = length;
-    this.parent = new Uint32Array(length);
-    for (var i = 0; i < length; i++) {
-      this.parent[i] = i;
-    }
-  };
-
-  /**
-   * Holds the length of the internal set.
-   * @type {number}
-   */
-  tracking.DisjointSet.prototype.length = null;
-
-  /**
-   * Holds the set containing the representative values.
-   * @type {Array.<number>}
-   */
-  tracking.DisjointSet.prototype.parent = null;
-
-  /**
-   * Finds a pointer to the representative of the set containing i.
-   * @param {number} i
-   * @return {number} The representative set of i.
-   */
-  tracking.DisjointSet.prototype.find = function(i) {
-    if (this.parent[i] === i) {
-      return i;
-    } else {
-      return (this.parent[i] = this.find(this.parent[i]));
-    }
-  };
-
-  /**
-   * Unites two dynamic sets containing objects i and j, say Si and Sj, into
-   * a new set that Si ∪ Sj, assuming that Si ∩ Sj = ∅;
-   * @param {number} i
-   * @param {number} j
-   */
-  tracking.DisjointSet.prototype.union = function(i, j) {
-    var iRepresentative = this.find(i);
-    var jRepresentative = this.find(j);
-    this.parent[iRepresentative] = jRepresentative;
-  };
-
-}());
-
-(function() {
-  /**
-   * Image utility.
-   * @static
-   * @constructor
-   */
-  tracking.Image = {};
-
-  /**
-   * Computes gaussian blur. Adapted from
-   * https://github.com/kig/canvasfilters.
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {number} diameter Gaussian blur diameter, must be greater than 1.
-   * @return {array} The edge pixels in a linear [r,g,b,a,...] array.
-   */
-  tracking.Image.blur = function(pixels, width, height, diameter) {
-    diameter = Math.abs(diameter);
-    if (diameter <= 1) {
-      throw new Error('Diameter should be greater than 1.');
-    }
-    var radius = diameter / 2;
-    var len = Math.ceil(diameter) + (1 - (Math.ceil(diameter) % 2));
-    var weights = new Float32Array(len);
-    var rho = (radius + 0.5) / 3;
-    var rhoSq = rho * rho;
-    var gaussianFactor = 1 / Math.sqrt(2 * Math.PI * rhoSq);
-    var rhoFactor = -1 / (2 * rho * rho);
-    var wsum = 0;
-    var middle = Math.floor(len / 2);
-    for (var i = 0; i < len; i++) {
-      var x = i - middle;
-      var gx = gaussianFactor * Math.exp(x * x * rhoFactor);
-      weights[i] = gx;
-      wsum += gx;
-    }
-    for (var j = 0; j < weights.length; j++) {
-      weights[j] /= wsum;
-    }
-    return this.separableConvolve(pixels, width, height, weights, weights, false);
-  };
-
-  /**
-   * Computes the integral image for summed, squared, rotated and sobel pixels.
-   * @param {array} pixels The pixels in a linear [r,g,b,a,...] array to loop
-   *     through.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {array} opt_integralImage Empty array of size `width * height` to
-   *     be filled with the integral image values. If not specified compute sum
-   *     values will be skipped.
-   * @param {array} opt_integralImageSquare Empty array of size `width *
-   *     height` to be filled with the integral image squared values. If not
-   *     specified compute squared values will be skipped.
-   * @param {array} opt_tiltedIntegralImage Empty array of size `width *
-   *     height` to be filled with the rotated integral image values. If not
-   *     specified compute sum values will be skipped.
-   * @param {array} opt_integralImageSobel Empty array of size `width *
-   *     height` to be filled with the integral image of sobel values. If not
-   *     specified compute sobel filtering will be skipped.
-   * @static
-   */
-  tracking.Image.computeIntegralImage = function(pixels, width, height, opt_integralImage, opt_integralImageSquare, opt_tiltedIntegralImage, opt_integralImageSobel) {
-    if (arguments.length < 4) {
-      throw new Error('You should specify at least one output array in the order: sum, square, tilted, sobel.');
-    }
-    var pixelsSobel;
-    if (opt_integralImageSobel) {
-      pixelsSobel = tracking.Image.sobel(pixels, width, height);
-    }
-    for (var i = 0; i < height; i++) {
-      for (var j = 0; j < width; j++) {
-        var w = i * width * 4 + j * 4;
-        var pixel = ~~(pixels[w] * 0.299 + pixels[w + 1] * 0.587 + pixels[w + 2] * 0.114);
-        if (opt_integralImage) {
-          this.computePixelValueSAT_(opt_integralImage, width, i, j, pixel);
-        }
-        if (opt_integralImageSquare) {
-          this.computePixelValueSAT_(opt_integralImageSquare, width, i, j, pixel * pixel);
-        }
-        if (opt_tiltedIntegralImage) {
-          var w1 = w - width * 4;
-          var pixelAbove = ~~(pixels[w1] * 0.299 + pixels[w1 + 1] * 0.587 + pixels[w1 + 2] * 0.114);
-          this.computePixelValueRSAT_(opt_tiltedIntegralImage, width, i, j, pixel, pixelAbove || 0);
-        }
-        if (opt_integralImageSobel) {
-          this.computePixelValueSAT_(opt_integralImageSobel, width, i, j, pixelsSobel[w]);
-        }
-      }
-    }
-  };
-
-  /**
-   * Helper method to compute the rotated summed area table (RSAT) by the
-   * formula:
-   *
-   * RSAT(x, y) = RSAT(x-1, y-1) + RSAT(x+1, y-1) - RSAT(x, y-2) + I(x, y) + I(x, y-1)
-   *
-   * @param {number} width The image width.
-   * @param {array} RSAT Empty array of size `width * height` to be filled with
-   *     the integral image values. If not specified compute sum values will be
-   *     skipped.
-   * @param {number} i Vertical position of the pixel to be evaluated.
-   * @param {number} j Horizontal position of the pixel to be evaluated.
-   * @param {number} pixel Pixel value to be added to the integral image.
-   * @static
-   * @private
-   */
-  tracking.Image.computePixelValueRSAT_ = function(RSAT, width, i, j, pixel, pixelAbove) {
-    var w = i * width + j;
-    RSAT[w] = (RSAT[w - width - 1] || 0) + (RSAT[w - width + 1] || 0) - (RSAT[w - width - width] || 0) + pixel + pixelAbove;
-  };
-
-  /**
-   * Helper method to compute the summed area table (SAT) by the formula:
-   *
-   * SAT(x, y) = SAT(x, y-1) + SAT(x-1, y) + I(x, y) - SAT(x-1, y-1)
-   *
-   * @param {number} width The image width.
-   * @param {array} SAT Empty array of size `width * height` to be filled with
-   *     the integral image values. If not specified compute sum values will be
-   *     skipped.
-   * @param {number} i Vertical position of the pixel to be evaluated.
-   * @param {number} j Horizontal position of the pixel to be evaluated.
-   * @param {number} pixel Pixel value to be added to the integral image.
-   * @static
-   * @private
-   */
-  tracking.Image.computePixelValueSAT_ = function(SAT, width, i, j, pixel) {
-    var w = i * width + j;
-    SAT[w] = (SAT[w - width] || 0) + (SAT[w - 1] || 0) + pixel - (SAT[w - width - 1] || 0);
-  };
-
-  /**
-   * Converts a color from a colorspace based on an RGB color model to a
-   * grayscale representation of its luminance. The coefficients represent the
-   * measured intensity perception of typical trichromat humans, in
-   * particular, human vision is most sensitive to green and least sensitive
-   * to blue.
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {boolean} fillRGBA If the result should fill all RGBA values with the gray scale
-   *  values, instead of returning a single value per pixel.
-   * @param {Uint8ClampedArray} The grayscale pixels in a linear array ([p,p,p,a,...] if fillRGBA
-   *  is true and [p1, p2, p3, ...] if fillRGBA is false).
-   * @static
-   */
-  tracking.Image.grayscale = function(pixels, width, height, fillRGBA) {
-    var gray = new Uint8ClampedArray(fillRGBA ? pixels.length : pixels.length >> 2);
-    var p = 0;
-    var w = 0;
-    for (var i = 0; i < height; i++) {
-      for (var j = 0; j < width; j++) {
-        var value = pixels[w] * 0.299 + pixels[w + 1] * 0.587 + pixels[w + 2] * 0.114;
-        gray[p++] = value;
-
-        if (fillRGBA) {
-          gray[p++] = value;
-          gray[p++] = value;
-          gray[p++] = pixels[w + 3];
-        }
-
-        w += 4;
-      }
-    }
-    return gray;
-  };
-
-  /**
-   * Fast horizontal separable convolution. A point spread function (PSF) is
-   * said to be separable if it can be broken into two one-dimensional
-   * signals: a vertical and a horizontal projection. The convolution is
-   * performed by sliding the kernel over the image, generally starting at the
-   * top left corner, so as to move the kernel through all the positions where
-   * the kernel fits entirely within the boundaries of the image. Adapted from
-   * https://github.com/kig/canvasfilters.
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {array} weightsVector The weighting vector, e.g [-1,0,1].
-   * @param {number} opaque
-   * @return {array} The convoluted pixels in a linear [r,g,b,a,...] array.
-   */
-  tracking.Image.horizontalConvolve = function(pixels, width, height, weightsVector, opaque) {
-    var side = weightsVector.length;
-    var halfSide = Math.floor(side / 2);
-    var output = new Float32Array(width * height * 4);
-    var alphaFac = opaque ? 1 : 0;
-
-    for (var y = 0; y < height; y++) {
-      for (var x = 0; x < width; x++) {
-        var sy = y;
-        var sx = x;
-        var offset = (y * width + x) * 4;
-        var r = 0;
-        var g = 0;
-        var b = 0;
-        var a = 0;
-        for (var cx = 0; cx < side; cx++) {
-          var scy = sy;
-          var scx = Math.min(width - 1, Math.max(0, sx + cx - halfSide));
-          var poffset = (scy * width + scx) * 4;
-          var wt = weightsVector[cx];
-          r += pixels[poffset] * wt;
-          g += pixels[poffset + 1] * wt;
-          b += pixels[poffset + 2] * wt;
-          a += pixels[poffset + 3] * wt;
-        }
-        output[offset] = r;
-        output[offset + 1] = g;
-        output[offset + 2] = b;
-        output[offset + 3] = a + alphaFac * (255 - a);
-      }
-    }
-    return output;
-  };
-
-  /**
-   * Fast vertical separable convolution. A point spread function (PSF) is
-   * said to be separable if it can be broken into two one-dimensional
-   * signals: a vertical and a horizontal projection. The convolution is
-   * performed by sliding the kernel over the image, generally starting at the
-   * top left corner, so as to move the kernel through all the positions where
-   * the kernel fits entirely within the boundaries of the image. Adapted from
-   * https://github.com/kig/canvasfilters.
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {array} weightsVector The weighting vector, e.g [-1,0,1].
-   * @param {number} opaque
-   * @return {array} The convoluted pixels in a linear [r,g,b,a,...] array.
-   */
-  tracking.Image.verticalConvolve = function(pixels, width, height, weightsVector, opaque) {
-    var side = weightsVector.length;
-    var halfSide = Math.floor(side / 2);
-    var output = new Float32Array(width * height * 4);
-    var alphaFac = opaque ? 1 : 0;
-
-    for (var y = 0; y < height; y++) {
-      for (var x = 0; x < width; x++) {
-        var sy = y;
-        var sx = x;
-        var offset = (y * width + x) * 4;
-        var r = 0;
-        var g = 0;
-        var b = 0;
-        var a = 0;
-        for (var cy = 0; cy < side; cy++) {
-          var scy = Math.min(height - 1, Math.max(0, sy + cy - halfSide));
-          var scx = sx;
-          var poffset = (scy * width + scx) * 4;
-          var wt = weightsVector[cy];
-          r += pixels[poffset] * wt;
-          g += pixels[poffset + 1] * wt;
-          b += pixels[poffset + 2] * wt;
-          a += pixels[poffset + 3] * wt;
-        }
-        output[offset] = r;
-        output[offset + 1] = g;
-        output[offset + 2] = b;
-        output[offset + 3] = a + alphaFac * (255 - a);
-      }
-    }
-    return output;
-  };
-
-  /**
-   * Fast separable convolution. A point spread function (PSF) is said to be
-   * separable if it can be broken into two one-dimensional signals: a
-   * vertical and a horizontal projection. The convolution is performed by
-   * sliding the kernel over the image, generally starting at the top left
-   * corner, so as to move the kernel through all the positions where the
-   * kernel fits entirely within the boundaries of the image. Adapted from
-   * https://github.com/kig/canvasfilters.
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {array} horizWeights The horizontal weighting vector, e.g [-1,0,1].
-   * @param {array} vertWeights The vertical vector, e.g [-1,0,1].
-   * @param {number} opaque
-   * @return {array} The convoluted pixels in a linear [r,g,b,a,...] array.
-   */
-  tracking.Image.separableConvolve = function(pixels, width, height, horizWeights, vertWeights, opaque) {
-    var vertical = this.verticalConvolve(pixels, width, height, vertWeights, opaque);
-    return this.horizontalConvolve(vertical, width, height, horizWeights, opaque);
-  };
-
-  /**
-   * Compute image edges using Sobel operator. Computes the vertical and
-   * horizontal gradients of the image and combines the computed images to
-   * find edges in the image. The way we implement the Sobel filter here is by
-   * first grayscaling the image, then taking the horizontal and vertical
-   * gradients and finally combining the gradient images to make up the final
-   * image. Adapted from https://github.com/kig/canvasfilters.
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @return {array} The edge pixels in a linear [r,g,b,a,...] array.
-   */
-  tracking.Image.sobel = function(pixels, width, height) {
-    pixels = this.grayscale(pixels, width, height, true);
-    var output = new Float32Array(width * height * 4);
-    var sobelSignVector = new Float32Array([-1, 0, 1]);
-    var sobelScaleVector = new Float32Array([1, 2, 1]);
-    var vertical = this.separableConvolve(pixels, width, height, sobelSignVector, sobelScaleVector);
-    var horizontal = this.separableConvolve(pixels, width, height, sobelScaleVector, sobelSignVector);
-
-    for (var i = 0; i < output.length; i += 4) {
-      var v = vertical[i];
-      var h = horizontal[i];
-      var p = Math.sqrt(h * h + v * v);
-      output[i] = p;
-      output[i + 1] = p;
-      output[i + 2] = p;
-      output[i + 3] = 255;
-    }
-
-    return output;
-  };
-
-  /**
-   * Equalizes the histogram of a grayscale image, normalizing the
-   * brightness and increasing the contrast of the image.
-   * @param {pixels} pixels The grayscale pixels in a linear array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @return {array} The equalized grayscale pixels in a linear array.
-   */
-  tracking.Image.equalizeHist = function(pixels, width, height){
-    var equalized = new Uint8ClampedArray(pixels.length);
-
-    var histogram = new Array(256);
-    for(var i=0; i < 256; i++) histogram[i] = 0;
-
-    for(var i=0; i < pixels.length; i++){
-      equalized[i] = pixels[i];
-      histogram[pixels[i]]++;
-    }
-
-    var prev = histogram[0];
-    for(var i=0; i < 256; i++){
-      histogram[i] += prev;
-      prev = histogram[i];
-    }
-
-    var norm = 255 / pixels.length;
-    for(var i=0; i < pixels.length; i++)
-      equalized[i] = (histogram[pixels[i]] * norm + 0.5) | 0;
-
-    return equalized;
-  }
-
-}());
-
-(function() {
-  /**
-   * ViolaJones utility.
-   * @static
-   * @constructor
-   */
-  tracking.ViolaJones = {};
-
-  /**
-   * Holds the minimum area of intersection that defines when a rectangle is
-   * from the same group. Often when a face is matched multiple rectangles are
-   * classified as possible rectangles to represent the face, when they
-   * intersects they are grouped as one face.
-   * @type {number}
-   * @default 0.5
-   * @static
-   */
-  tracking.ViolaJones.REGIONS_OVERLAP = 0.5;
-
-  /**
-   * Holds the HAAR cascade classifiers converted from OpenCV training.
-   * @type {array}
-   * @static
-   */
-  tracking.ViolaJones.classifiers = {};
-
-  /**
-   * Detects through the HAAR cascade data rectangles matches.
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {number} initialScale The initial scale to start the block
-   *     scaling.
-   * @param {number} scaleFactor The scale factor to scale the feature block.
-   * @param {number} stepSize The block step size.
-   * @param {number} edgesDensity Percentage density edges inside the
-   *     classifier block. Value from [0.0, 1.0], defaults to 0.2. If specified
-   *     edge detection will be applied to the image to prune dead areas of the
-   *     image, this can improve significantly performance.
-   * @param {number} data The HAAR cascade data.
-   * @return {array} Found rectangles.
-   * @static
-   */
-  tracking.ViolaJones.detect = function(pixels, width, height, initialScale, scaleFactor, stepSize, edgesDensity, data) {
-    var total = 0;
-    var rects = [];
-    var integralImage = new Int32Array(width * height);
-    var integralImageSquare = new Int32Array(width * height);
-    var tiltedIntegralImage = new Int32Array(width * height);
-
-    var integralImageSobel;
-    if (edgesDensity > 0) {
-      integralImageSobel = new Int32Array(width * height);
-    }
-
-    tracking.Image.computeIntegralImage(pixels, width, height, integralImage, integralImageSquare, tiltedIntegralImage, integralImageSobel);
-
-    var minWidth = data[0];
-    var minHeight = data[1];
-    var scale = initialScale * scaleFactor;
-    var blockWidth = (scale * minWidth) | 0;
-    var blockHeight = (scale * minHeight) | 0;
-
-    while (blockWidth < width && blockHeight < height) {
-      var step = (scale * stepSize + 0.5) | 0;
-      for (var i = 0; i < (height - blockHeight); i += step) {
-        for (var j = 0; j < (width - blockWidth); j += step) {
-
-          if (edgesDensity > 0) {
-            if (this.isTriviallyExcluded(edgesDensity, integralImageSobel, i, j, width, blockWidth, blockHeight)) {
-              continue;
-            }
-          }
-
-          if (this.evalStages_(data, integralImage, integralImageSquare, tiltedIntegralImage, i, j, width, blockWidth, blockHeight, scale)) {
-            rects[total++] = {
-              width: blockWidth,
-              height: blockHeight,
-              x: j,
-              y: i
-            };
-          }
-        }
-      }
-
-      scale *= scaleFactor;
-      blockWidth = (scale * minWidth) | 0;
-      blockHeight = (scale * minHeight) | 0;
-    }
-    return this.mergeRectangles_(rects);
-  };
-
-  /**
-   * Fast check to test whether the edges density inside the block is greater
-   * than a threshold, if true it tests the stages. This can improve
-   * significantly performance.
-   * @param {number} edgesDensity Percentage density edges inside the
-   *     classifier block.
-   * @param {array} integralImageSobel The integral image of a sobel image.
-   * @param {number} i Vertical position of the pixel to be evaluated.
-   * @param {number} j Horizontal position of the pixel to be evaluated.
-   * @param {number} width The image width.
-   * @return {boolean} True whether the block at position i,j can be skipped,
-   *     false otherwise.
-   * @static
-   * @protected
-   */
-  tracking.ViolaJones.isTriviallyExcluded = function(edgesDensity, integralImageSobel, i, j, width, blockWidth, blockHeight) {
-    var wbA = i * width + j;
-    var wbB = wbA + blockWidth;
-    var wbD = wbA + blockHeight * width;
-    var wbC = wbD + blockWidth;
-    var blockEdgesDensity = (integralImageSobel[wbA] - integralImageSobel[wbB] - integralImageSobel[wbD] + integralImageSobel[wbC]) / (blockWidth * blockHeight * 255);
-    if (blockEdgesDensity < edgesDensity) {
-      return true;
-    }
-    return false;
-  };
-
-  /**
-   * Evaluates if the block size on i,j position is a valid HAAR cascade
-   * stage.
-   * @param {number} data The HAAR cascade data.
-   * @param {number} i Vertical position of the pixel to be evaluated.
-   * @param {number} j Horizontal position of the pixel to be evaluated.
-   * @param {number} width The image width.
-   * @param {number} blockSize The block size.
-   * @param {number} scale The scale factor of the block size and its original
-   *     size.
-   * @param {number} inverseArea The inverse area of the block size.
-   * @return {boolean} Whether the region passes all the stage tests.
-   * @private
-   * @static
-   */
-  tracking.ViolaJones.evalStages_ = function(data, integralImage, integralImageSquare, tiltedIntegralImage, i, j, width, blockWidth, blockHeight, scale) {
-    var inverseArea = 1.0 / (blockWidth * blockHeight);
-    var wbA = i * width + j;
-    var wbB = wbA + blockWidth;
-    var wbD = wbA + blockHeight * width;
-    var wbC = wbD + blockWidth;
-    var mean = (integralImage[wbA] - integralImage[wbB] - integralImage[wbD] + integralImage[wbC]) * inverseArea;
-    var variance = (integralImageSquare[wbA] - integralImageSquare[wbB] - integralImageSquare[wbD] + integralImageSquare[wbC]) * inverseArea - mean * mean;
-
-    var standardDeviation = 1;
-    if (variance > 0) {
-      standardDeviation = Math.sqrt(variance);
-    }
-
-    var length = data.length;
-
-    for (var w = 2; w < length; ) {
-      var stageSum = 0;
-      var stageThreshold = data[w++];
-      var nodeLength = data[w++];
-
-      while (nodeLength--) {
-        var rectsSum = 0;
-        var tilted = data[w++];
-        var rectsLength = data[w++];
-
-        for (var r = 0; r < rectsLength; r++) {
-          var rectLeft = (j + data[w++] * scale + 0.5) | 0;
-          var rectTop = (i + data[w++] * scale + 0.5) | 0;
-          var rectWidth = (data[w++] * scale + 0.5) | 0;
-          var rectHeight = (data[w++] * scale + 0.5) | 0;
-          var rectWeight = data[w++];
-
-          var w1;
-          var w2;
-          var w3;
-          var w4;
-          if (tilted) {
-            // RectSum(r) = RSAT(x-h+w, y+w+h-1) + RSAT(x, y-1) - RSAT(x-h, y+h-1) - RSAT(x+w, y+w-1)
-            w1 = (rectLeft - rectHeight + rectWidth) + (rectTop + rectWidth + rectHeight - 1) * width;
-            w2 = rectLeft + (rectTop - 1) * width;
-            w3 = (rectLeft - rectHeight) + (rectTop + rectHeight - 1) * width;
-            w4 = (rectLeft + rectWidth) + (rectTop + rectWidth - 1) * width;
-            rectsSum += (tiltedIntegralImage[w1] + tiltedIntegralImage[w2] - tiltedIntegralImage[w3] - tiltedIntegralImage[w4]) * rectWeight;
-          } else {
-            // RectSum(r) = SAT(x-1, y-1) + SAT(x+w-1, y+h-1) - SAT(x-1, y+h-1) - SAT(x+w-1, y-1)
-            w1 = rectTop * width + rectLeft;
-            w2 = w1 + rectWidth;
-            w3 = w1 + rectHeight * width;
-            w4 = w3 + rectWidth;
-            rectsSum += (integralImage[w1] - integralImage[w2] - integralImage[w3] + integralImage[w4]) * rectWeight;
-            // TODO: Review the code below to analyze performance when using it instead.
-            // w1 = (rectLeft - 1) + (rectTop - 1) * width;
-            // w2 = (rectLeft + rectWidth - 1) + (rectTop + rectHeight - 1) * width;
-            // w3 = (rectLeft - 1) + (rectTop + rectHeight - 1) * width;
-            // w4 = (rectLeft + rectWidth - 1) + (rectTop - 1) * width;
-            // rectsSum += (integralImage[w1] + integralImage[w2] - integralImage[w3] - integralImage[w4]) * rectWeight;
-          }
-        }
-
-        var nodeThreshold = data[w++];
-        var nodeLeft = data[w++];
-        var nodeRight = data[w++];
-
-        if (rectsSum * inverseArea < nodeThreshold * standardDeviation) {
-          stageSum += nodeLeft;
-        } else {
-          stageSum += nodeRight;
-        }
-      }
-
-      if (stageSum < stageThreshold) {
-        return false;
-      }
-    }
-    return true;
-  };
-
-  /**
-   * Postprocess the detected sub-windows in order to combine overlapping
-   * detections into a single detection.
-   * @param {array} rects
-   * @return {array}
-   * @private
-   * @static
-   */
-  tracking.ViolaJones.mergeRectangles_ = function(rects) {
-    var disjointSet = new tracking.DisjointSet(rects.length);
-
-    for (var i = 0; i < rects.length; i++) {
-      var r1 = rects[i];
-      for (var j = 0; j < rects.length; j++) {
-        var r2 = rects[j];
-        if (tracking.Math.intersectRect(r1.x, r1.y, r1.x + r1.width, r1.y + r1.height, r2.x, r2.y, r2.x + r2.width, r2.y + r2.height)) {
-          var x1 = Math.max(r1.x, r2.x);
-          var y1 = Math.max(r1.y, r2.y);
-          var x2 = Math.min(r1.x + r1.width, r2.x + r2.width);
-          var y2 = Math.min(r1.y + r1.height, r2.y + r2.height);
-          var overlap = (x1 - x2) * (y1 - y2);
-          var area1 = (r1.width * r1.height);
-          var area2 = (r2.width * r2.height);
-
-          if ((overlap / (area1 * (area1 / area2)) >= this.REGIONS_OVERLAP) &&
-            (overlap / (area2 * (area1 / area2)) >= this.REGIONS_OVERLAP)) {
-            disjointSet.union(i, j);
-          }
-        }
-      }
-    }
-
-    var map = {};
-    for (var k = 0; k < disjointSet.length; k++) {
-      var rep = disjointSet.find(k);
-      if (!map[rep]) {
-        map[rep] = {
-          total: 1,
-          width: rects[k].width,
-          height: rects[k].height,
-          x: rects[k].x,
-          y: rects[k].y
-        };
-        continue;
-      }
-      map[rep].total++;
-      map[rep].width += rects[k].width;
-      map[rep].height += rects[k].height;
-      map[rep].x += rects[k].x;
-      map[rep].y += rects[k].y;
-    }
-
-    var result = [];
-    Object.keys(map).forEach(function(key) {
-      var rect = map[key];
-      result.push({
-        total: rect.total,
-        width: (rect.width / rect.total + 0.5) | 0,
-        height: (rect.height / rect.total + 0.5) | 0,
-        x: (rect.x / rect.total + 0.5) | 0,
-        y: (rect.y / rect.total + 0.5) | 0
-      });
-    });
-
-    return result;
-  };
-
-}());
-
-(function() {
-  /**
-   * Brief intends for "Binary Robust Independent Elementary Features".This
-   * method generates a binary string for each keypoint found by an extractor
-   * method.
-   * @static
-   * @constructor
-   */
-  tracking.Brief = {};
-
-  /**
-   * The set of binary tests is defined by the nd (x,y)-location pairs
-   * uniquely chosen during the initialization. Values could vary between N =
-   * 128,256,512. N=128 yield good compromises between speed, storage
-   * efficiency, and recognition rate.
-   * @type {number}
-   */
-  tracking.Brief.N = 512;
-
-  /**
-   * Caches coordinates values of (x,y)-location pairs uniquely chosen during
-   * the initialization.
-   * @type {Object.<number, Int32Array>}
-   * @private
-   * @static
-   */
-  tracking.Brief.randomImageOffsets_ = {};
-
-  /**
-   * Caches delta values of (x,y)-location pairs uniquely chosen during
-   * the initialization.
-   * @type {Int32Array}
-   * @private
-   * @static
-   */
-  tracking.Brief.randomWindowOffsets_ = null;
-
-  /**
-   * Generates a binary string for each found keypoints extracted using an
-   * extractor method.
-   * @param {array} The grayscale pixels in a linear [p1,p2,...] array.
-   * @param {number} width The image width.
-   * @param {array} keypoints
-   * @return {Int32Array} Returns an array where for each four sequence int
-   *     values represent the descriptor binary string (128 bits) necessary
-   *     to describe the corner, e.g. [0,0,0,0, 0,0,0,0, ...].
-   * @static
-   */
-  tracking.Brief.getDescriptors = function(pixels, width, keypoints) {
-    // Optimizing divide by 32 operation using binary shift
-    // (this.N >> 5) === this.N/32.
-    var descriptors = new Int32Array((keypoints.length >> 1) * (this.N >> 5));
-    var descriptorWord = 0;
-    var offsets = this.getRandomOffsets_(width);
-    var position = 0;
-
-    for (var i = 0; i < keypoints.length; i += 2) {
-      var w = width * keypoints[i + 1] + keypoints[i];
-
-      var offsetsPosition = 0;
-      for (var j = 0, n = this.N; j < n; j++) {
-        if (pixels[offsets[offsetsPosition++] + w] < pixels[offsets[offsetsPosition++] + w]) {
-          // The bit in the position `j % 32` of descriptorWord should be set to 1. We do
-          // this by making an OR operation with a binary number that only has the bit
-          // in that position set to 1. That binary number is obtained by shifting 1 left by
-          // `j % 32` (which is the same as `j & 31` left) positions.
-          descriptorWord |= 1 << (j & 31);
-        }
-
-        // If the next j is a multiple of 32, we will need to use a new descriptor word to hold
-        // the next results.
-        if (!((j + 1) & 31)) {
-          descriptors[position++] = descriptorWord;
-          descriptorWord = 0;
-        }
-      }
-    }
-
-    return descriptors;
-  };
-
-  /**
-   * Matches sets of features {mi} and {m′j} extracted from two images taken
-   * from similar, and often successive, viewpoints. A classical procedure
-   * runs as follows. For each point {mi} in the first image, search in a
-   * region of the second image around location {mi} for point {m′j}. The
-   * search is based on the similarity of the local image windows, also known
-   * as kernel windows, centered on the points, which strongly characterizes
-   * the points when the images are sufficiently close. Once each keypoint is
-   * described with its binary string, they need to be compared with the
-   * closest matching point. Distance metric is critical to the performance of
-   * in- trusion detection systems. Thus using binary strings reduces the size
-   * of the descriptor and provides an interesting data structure that is fast
-   * to operate whose similarity can be measured by the Hamming distance.
-   * @param {array} keypoints1
-   * @param {array} descriptors1
-   * @param {array} keypoints2
-   * @param {array} descriptors2
-   * @return {Int32Array} Returns an array where the index is the corner1
-   *     index coordinate, and the value is the corresponding match index of
-   *     corner2, e.g. keypoints1=[x0,y0,x1,y1,...] and
-   *     keypoints2=[x'0,y'0,x'1,y'1,...], if x0 matches x'1 and x1 matches x'0,
-   *     the return array would be [3,0].
-   * @static
-   */
-  tracking.Brief.match = function(keypoints1, descriptors1, keypoints2, descriptors2) {
-    var len1 = keypoints1.length >> 1;
-    var len2 = keypoints2.length >> 1;
-    var matches = new Array(len1);
-
-    for (var i = 0; i < len1; i++) {
-      var min = Infinity;
-      var minj = 0;
-      for (var j = 0; j < len2; j++) {
-        var dist = 0;
-        // Optimizing divide by 32 operation using binary shift
-        // (this.N >> 5) === this.N/32.
-        for (var k = 0, n = this.N >> 5; k < n; k++) {
-          dist += tracking.Math.hammingWeight(descriptors1[i * n + k] ^ descriptors2[j * n + k]);
-        }
-        if (dist < min) {
-          min = dist;
-          minj = j;
-        }
-      }
-      matches[i] = {
-        index1: i,
-        index2: minj,
-        keypoint1: [keypoints1[2 * i], keypoints1[2 * i + 1]],
-        keypoint2: [keypoints2[2 * minj], keypoints2[2 * minj + 1]],
-        confidence: 1 - min / this.N
-      };
-    }
-
-    return matches;
-  };
-
-  /**
-   * Removes matches outliers by testing matches on both directions.
-   * @param {array} keypoints1
-   * @param {array} descriptors1
-   * @param {array} keypoints2
-   * @param {array} descriptors2
-   * @return {Int32Array} Returns an array where the index is the corner1
-   *     index coordinate, and the value is the corresponding match index of
-   *     corner2, e.g. keypoints1=[x0,y0,x1,y1,...] and
-   *     keypoints2=[x'0,y'0,x'1,y'1,...], if x0 matches x'1 and x1 matches x'0,
-   *     the return array would be [3,0].
-   * @static
-   */
-  tracking.Brief.reciprocalMatch = function(keypoints1, descriptors1, keypoints2, descriptors2) {
-    var matches = [];
-    if (keypoints1.length === 0 || keypoints2.length === 0) {
-      return matches;
-    }
-
-    var matches1 = tracking.Brief.match(keypoints1, descriptors1, keypoints2, descriptors2);
-    var matches2 = tracking.Brief.match(keypoints2, descriptors2, keypoints1, descriptors1);
-    for (var i = 0; i < matches1.length; i++) {
-      if (matches2[matches1[i].index2].index2 === i) {
-        matches.push(matches1[i]);
-      }
-    }
-    return matches;
-  };
-
-  /**
-   * Gets the coordinates values of (x,y)-location pairs uniquely chosen
-   * during the initialization.
-   * @return {array} Array with the random offset values.
-   * @private
-   */
-  tracking.Brief.getRandomOffsets_ = function(width) {
-    if (!this.randomWindowOffsets_) {
-      var windowPosition = 0;
-      var windowOffsets = new Int32Array(4 * this.N);
-      for (var i = 0; i < this.N; i++) {
-        windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16));
-        windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16));
-        windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16));
-        windowOffsets[windowPosition++] = Math.round(tracking.Math.uniformRandom(-15, 16));
-      }
-      this.randomWindowOffsets_ = windowOffsets;
-    }
-
-    if (!this.randomImageOffsets_[width]) {
-      var imagePosition = 0;
-      var imageOffsets = new Int32Array(2 * this.N);
-      for (var j = 0; j < this.N; j++) {
-        imageOffsets[imagePosition++] = this.randomWindowOffsets_[4 * j] * width + this.randomWindowOffsets_[4 * j + 1];
-        imageOffsets[imagePosition++] = this.randomWindowOffsets_[4 * j + 2] * width + this.randomWindowOffsets_[4 * j + 3];
-      }
-      this.randomImageOffsets_[width] = imageOffsets;
-    }
-
-    return this.randomImageOffsets_[width];
-  };
-}());
-
-(function() {
-  /**
-   * FAST intends for "Features from Accelerated Segment Test". This method
-   * performs a point segment test corner detection. The segment test
-   * criterion operates by considering a circle of sixteen pixels around the
-   * corner candidate p. The detector classifies p as a corner if there exists
-   * a set of n contiguous pixelsin the circle which are all brighter than the
-   * intensity of the candidate pixel Ip plus a threshold t, or all darker
-   * than Ip − t.
-   *
-   *       15 00 01
-   *    14          02
-   * 13                03
-   * 12       []       04
-   * 11                05
-   *    10          06
-   *       09 08 07
-   *
-   * For more reference:
-   * http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.60.3991&rep=rep1&type=pdf
-   * @static
-   * @constructor
-   */
-  tracking.Fast = {};
-
-  /**
-   * Holds the threshold to determine whether the tested pixel is brighter or
-   * darker than the corner candidate p.
-   * @type {number}
-   * @default 40
-   * @static
-   */
-  tracking.Fast.THRESHOLD = 40;
-
-  /**
-   * Caches coordinates values of the circle surrounding the pixel candidate p.
-   * @type {Object.<number, Int32Array>}
-   * @private
-   * @static
-   */
-  tracking.Fast.circles_ = {};
-
-  /**
-   * Finds corners coordinates on the graysacaled image.
-   * @param {array} The grayscale pixels in a linear [p1,p2,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {number} threshold to determine whether the tested pixel is brighter or
-   *     darker than the corner candidate p. Default value is 40.
-   * @return {array} Array containing the coordinates of all found corners,
-   *     e.g. [x0,y0,x1,y1,...], where P(x0,y0) represents a corner coordinate.
-   * @static
-   */
-  tracking.Fast.findCorners = function(pixels, width, height, opt_threshold) {
-    var circleOffsets = this.getCircleOffsets_(width);
-    var circlePixels = new Int32Array(16);
-    var corners = [];
-
-    if (opt_threshold === undefined) {
-      opt_threshold = this.THRESHOLD;
-    }
-
-    // When looping through the image pixels, skips the first three lines from
-    // the image boundaries to constrain the surrounding circle inside the image
-    // area.
-    for (var i = 3; i < height - 3; i++) {
-      for (var j = 3; j < width - 3; j++) {
-        var w = i * width + j;
-        var p = pixels[w];
-
-        // Loops the circle offsets to read the pixel value for the sixteen
-        // surrounding pixels.
-        for (var k = 0; k < 16; k++) {
-          circlePixels[k] = pixels[w + circleOffsets[k]];
-        }
-
-        if (this.isCorner(p, circlePixels, opt_threshold)) {
-          // The pixel p is classified as a corner, as optimization increment j
-          // by the circle radius 3 to skip the neighbor pixels inside the
-          // surrounding circle. This can be removed without compromising the
-          // result.
-          corners.push(j, i);
-          j += 3;
-        }
-      }
-    }
-
-    return corners;
-  };
-
-  /**
-   * Checks if the circle pixel is brighter than the candidate pixel p by
-   * a threshold.
-   * @param {number} circlePixel The circle pixel value.
-   * @param {number} p The value of the candidate pixel p.
-   * @param {number} threshold
-   * @return {Boolean}
-   * @static
-   */
-  tracking.Fast.isBrighter = function(circlePixel, p, threshold) {
-    return circlePixel - p > threshold;
-  };
-
-  /**
-   * Checks if the circle pixel is within the corner of the candidate pixel p
-   * by a threshold.
-   * @param {number} p The value of the candidate pixel p.
-   * @param {number} circlePixel The circle pixel value.
-   * @param {number} threshold
-   * @return {Boolean}
-   * @static
-   */
-  tracking.Fast.isCorner = function(p, circlePixels, threshold) {
-    if (this.isTriviallyExcluded(circlePixels, p, threshold)) {
-      return false;
-    }
-
-    for (var x = 0; x < 16; x++) {
-      var darker = true;
-      var brighter = true;
-
-      for (var y = 0; y < 9; y++) {
-        var circlePixel = circlePixels[(x + y) & 15];
-
-        if (!this.isBrighter(p, circlePixel, threshold)) {
-          brighter = false;
-          if (darker === false) {
-            break;
-          }
-        }
-
-        if (!this.isDarker(p, circlePixel, threshold)) {
-          darker = false;
-          if (brighter === false) {
-            break;
-          }
-        }
-      }
-
-      if (brighter || darker) {
-        return true;
-      }
-    }
-
-    return false;
-  };
-
-  /**
-   * Checks if the circle pixel is darker than the candidate pixel p by
-   * a threshold.
-   * @param {number} circlePixel The circle pixel value.
-   * @param {number} p The value of the candidate pixel p.
-   * @param {number} threshold
-   * @return {Boolean}
-   * @static
-   */
-  tracking.Fast.isDarker = function(circlePixel, p, threshold) {
-    return p - circlePixel > threshold;
-  };
-
-  /**
-   * Fast check to test if the candidate pixel is a trivially excluded value.
-   * In order to be a corner, the candidate pixel value should be darker or
-   * brighter than 9-12 surrounding pixels, when at least three of the top,
-   * bottom, left and right pixels are brighter or darker it can be
-   * automatically excluded improving the performance.
-   * @param {number} circlePixel The circle pixel value.
-   * @param {number} p The value of the candidate pixel p.
-   * @param {number} threshold
-   * @return {Boolean}
-   * @static
-   * @protected
-   */
-  tracking.Fast.isTriviallyExcluded = function(circlePixels, p, threshold) {
-    var count = 0;
-    var circleBottom = circlePixels[8];
-    var circleLeft = circlePixels[12];
-    var circleRight = circlePixels[4];
-    var circleTop = circlePixels[0];
-
-    if (this.isBrighter(circleTop, p, threshold)) {
-      count++;
-    }
-    if (this.isBrighter(circleRight, p, threshold)) {
-      count++;
-    }
-    if (this.isBrighter(circleBottom, p, threshold)) {
-      count++;
-    }
-    if (this.isBrighter(circleLeft, p, threshold)) {
-      count++;
-    }
-
-    if (count < 3) {
-      count = 0;
-      if (this.isDarker(circleTop, p, threshold)) {
-        count++;
-      }
-      if (this.isDarker(circleRight, p, threshold)) {
-        count++;
-      }
-      if (this.isDarker(circleBottom, p, threshold)) {
-        count++;
-      }
-      if (this.isDarker(circleLeft, p, threshold)) {
-        count++;
-      }
-      if (count < 3) {
-        return true;
-      }
-    }
-
-    return false;
-  };
-
-  /**
-   * Gets the sixteen offset values of the circle surrounding pixel.
-   * @param {number} width The image width.
-   * @return {array} Array with the sixteen offset values of the circle
-   *     surrounding pixel.
-   * @private
-   */
-  tracking.Fast.getCircleOffsets_ = function(width) {
-    if (this.circles_[width]) {
-      return this.circles_[width];
-    }
-
-    var circle = new Int32Array(16);
-
-    circle[0] = -width - width - width;
-    circle[1] = circle[0] + 1;
-    circle[2] = circle[1] + width + 1;
-    circle[3] = circle[2] + width + 1;
-    circle[4] = circle[3] + width;
-    circle[5] = circle[4] + width;
-    circle[6] = circle[5] + width - 1;
-    circle[7] = circle[6] + width - 1;
-    circle[8] = circle[7] - 1;
-    circle[9] = circle[8] - 1;
-    circle[10] = circle[9] - width - 1;
-    circle[11] = circle[10] - width - 1;
-    circle[12] = circle[11] - width;
-    circle[13] = circle[12] - width;
-    circle[14] = circle[13] - width + 1;
-    circle[15] = circle[14] - width + 1;
-
-    this.circles_[width] = circle;
-    return circle;
-  };
-}());
-
-(function() {
-  /**
-   * Math utility.
-   * @static
-   * @constructor
-   */
-  tracking.Math = {};
-
-  /**
-   * Euclidean distance between two points P(x0, y0) and P(x1, y1).
-   * @param {number} x0 Horizontal coordinate of P0.
-   * @param {number} y0 Vertical coordinate of P0.
-   * @param {number} x1 Horizontal coordinate of P1.
-   * @param {number} y1 Vertical coordinate of P1.
-   * @return {number} The euclidean distance.
-   */
-  tracking.Math.distance = function(x0, y0, x1, y1) {
-    var dx = x1 - x0;
-    var dy = y1 - y0;
-
-    return Math.sqrt(dx * dx + dy * dy);
-  };
-
-  /**
-   * Calculates the Hamming weight of a string, which is the number of symbols that are
-   * different from the zero-symbol of the alphabet used. It is thus
-   * equivalent to the Hamming distance from the all-zero string of the same
-   * length. For the most typical case, a string of bits, this is the number
-   * of 1's in the string.
-   *
-   * Example:
-   *
-   * <pre>
-   *  Binary string     Hamming weight
-   *   11101                 4
-   *   11101010              5
-   * </pre>
-   *
-   * @param {number} i Number that holds the binary string to extract the hamming weight.
-   * @return {number} The hamming weight.
-   */
-  tracking.Math.hammingWeight = function(i) {
-    i = i - ((i >> 1) & 0x55555555);
-    i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
-
-    return ((i + (i >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
-  };
-
-  /**
-   * Generates a random number between [a, b] interval.
-   * @param {number} a
-   * @param {number} b
-   * @return {number}
-   */
-  tracking.Math.uniformRandom = function(a, b) {
-    return a + Math.random() * (b - a);
-  };
-
-  /**
-   * Tests if a rectangle intersects with another.
-   *
-   *  <pre>
-   *  x0y0 --------       x2y2 --------
-   *      |       |           |       |
-   *      -------- x1y1       -------- x3y3
-   * </pre>
-   *
-   * @param {number} x0 Horizontal coordinate of P0.
-   * @param {number} y0 Vertical coordinate of P0.
-   * @param {number} x1 Horizontal coordinate of P1.
-   * @param {number} y1 Vertical coordinate of P1.
-   * @param {number} x2 Horizontal coordinate of P2.
-   * @param {number} y2 Vertical coordinate of P2.
-   * @param {number} x3 Horizontal coordinate of P3.
-   * @param {number} y3 Vertical coordinate of P3.
-   * @return {boolean}
-   */
-  tracking.Math.intersectRect = function(x0, y0, x1, y1, x2, y2, x3, y3) {
-    return !(x2 > x1 || x3 < x0 || y2 > y1 || y3 < y0);
-  };
-
-}());
-
-(function() {
-  /**
-   * Matrix utility.
-   * @static
-   * @constructor
-   */
-  tracking.Matrix = {};
-
-  /**
-   * Loops the array organized as major-row order and executes `fn` callback
-   * for each iteration. The `fn` callback receives the following parameters:
-   * `(r,g,b,a,index,i,j)`, where `r,g,b,a` represents the pixel color with
-   * alpha channel, `index` represents the position in the major-row order
-   * array and `i,j` the respective indexes positions in two dimensions.
-   * @param {array} pixels The pixels in a linear [r,g,b,a,...] array to loop
-   *     through.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {function} fn The callback function for each pixel.
-   * @param {number} opt_jump Optional jump for the iteration, by default it
-   *     is 1, hence loops all the pixels of the array.
-   * @static
-   */
-  tracking.Matrix.forEach = function(pixels, width, height, fn, opt_jump) {
-    opt_jump = opt_jump || 1;
-    for (var i = 0; i < height; i += opt_jump) {
-      for (var j = 0; j < width; j += opt_jump) {
-        var w = i * width * 4 + j * 4;
-        fn.call(this, pixels[w], pixels[w + 1], pixels[w + 2], pixels[w + 3], w, i, j);
-      }
-    }
-  };
-
-  /**
-   * Calculates the per-element subtraction of two NxM matrices and returns a 
-   * new NxM matrix as the result.
-   * @param {matrix} a The first matrix.
-   * @param {matrix} a The second matrix.
-   * @static
-   */
-  tracking.Matrix.sub = function(a, b){
-    var res = tracking.Matrix.clone(a);
-    for(var i=0; i < res.length; i++){
-      for(var j=0; j < res[i].length; j++){
-        res[i][j] -= b[i][j]; 
-      }
-    }
-    return res;
-  }
-
-  /**
-   * Calculates the per-element sum of two NxM matrices and returns a new NxM
-   * NxM matrix as the result.
-   * @param {matrix} a The first matrix.
-   * @param {matrix} a The second matrix.
-   * @static
-   */
-  tracking.Matrix.add = function(a, b){
-    var res = tracking.Matrix.clone(a);
-    for(var i=0; i < res.length; i++){
-      for(var j=0; j < res[i].length; j++){
-        res[i][j] += b[i][j]; 
-      }
-    }
-    return res;
-  }
-
-  /**
-   * Clones a matrix (or part of it) and returns a new matrix as the result.
-   * @param {matrix} src The matrix to be cloned.
-   * @param {number} width The second matrix.
-   * @static
-   */
-  tracking.Matrix.clone = function(src, width, height){
-    width = width || src[0].length;
-    height = height || src.length;
-    var temp = new Array(height);
-    var i = height;
-    while(i--){
-      temp[i] = new Array(width);
-      var j = width;
-      while(j--) temp[i][j] = src[i][j];
-    } 
-    return temp;
-  }
-
-  /**
-   * Multiply a matrix by a scalar and returns a new matrix as the result.
-   * @param {number} scalar The scalar to multiply the matrix by.
-   * @param {matrix} src The matrix to be multiplied.
-   * @static
-   */
-  tracking.Matrix.mulScalar = function(scalar, src){
-    var res = tracking.Matrix.clone(src);
-    for(var i=0; i < src.length; i++){
-      for(var j=0; j < src[i].length; j++){
-        res[i][j] *= scalar;
-      }
-    }
-    return res;
-  }
-
-  /**
-   * Transpose a matrix and returns a new matrix as the result.
-   * @param {matrix} src The matrix to be transposed.
-   * @static
-   */
-  tracking.Matrix.transpose = function(src){
-    var transpose = new Array(src[0].length);
-    for(var i=0; i < src[0].length; i++){
-      transpose[i] = new Array(src.length);
-      for(var j=0; j < src.length; j++){
-        transpose[i][j] = src[j][i];
-      }
-    }
-    return transpose;
-  }
-
-  /**
-   * Multiply an MxN matrix with an NxP matrix and returns a new MxP matrix
-   * as the result.
-   * @param {matrix} a The first matrix.
-   * @param {matrix} b The second matrix.
-   * @static
-   */
-  tracking.Matrix.mul = function(a, b) {
-    var res = new Array(a.length);
-    for (var i = 0; i < a.length; i++) {
-      res[i] = new Array(b[0].length);
-      for (var j = 0; j < b[0].length; j++) {
-        res[i][j] = 0;            
-        for (var k = 0; k < a[0].length; k++) {
-          res[i][j] += a[i][k] * b[k][j];
-        }
-      }
-    }
-    return res;
-  }
-
-  /**
-   * Calculates the absolute norm of a matrix.
-   * @param {matrix} src The matrix which norm will be calculated.
-   * @static
-   */
-  tracking.Matrix.norm = function(src){
-    var res = 0;
-    for(var i=0; i < src.length; i++){
-      for(var j=0; j < src[i].length; j++){
-        res += src[i][j]*src[i][j];
-      }
-    }
-    return Math.sqrt(res);
-  }
-
-  /**
-   * Calculates and returns the covariance matrix of a set of vectors as well
-   * as the mean of the matrix.
-   * @param {matrix} src The matrix which covariance matrix will be calculated.
-   * @static
-   */
-  tracking.Matrix.calcCovarMatrix = function(src){
-
-    var mean = new Array(src.length);
-    for(var i=0; i < src.length; i++){
-      mean[i] = [0.0];
-      for(var j=0; j < src[i].length; j++){
-        mean[i][0] += src[i][j]/src[i].length;
-      }
-    }
-
-    var deltaFull = tracking.Matrix.clone(mean);
-    for(var i=0; i < deltaFull.length; i++){
-      for(var j=0; j < src[0].length - 1; j++){
-        deltaFull[i].push(deltaFull[i][0]);
-      }
-    }
-
-    var a = tracking.Matrix.sub(src, deltaFull);
-    var b = tracking.Matrix.transpose(a);
-    var covar = tracking.Matrix.mul(b,a); 
-    return [covar, mean];
-
-  }
-
-}());
-(function() {
-  /**
-   * EPnp utility.
-   * @static
-   * @constructor
-   */
-  tracking.EPnP = {};
-
-  tracking.EPnP.solve = function(objectPoints, imagePoints, cameraMatrix) {};
-}());
-
-(function() {
-  /**
-   * Tracker utility.
-   * @constructor
-   * @extends {tracking.EventEmitter}
-   */
-  tracking.Tracker = function() {
-    tracking.Tracker.base(this, 'constructor');
-  };
-
-  tracking.inherits(tracking.Tracker, tracking.EventEmitter);
-
-  /**
-   * Tracks the pixels on the array. This method is called for each video
-   * frame in order to emit `track` event.
-   * @param {Uint8ClampedArray} pixels The pixels data to track.
-   * @param {number} width The pixels canvas width.
-   * @param {number} height The pixels canvas height.
-   */
-  tracking.Tracker.prototype.track = function() {};
-}());
-
-(function() {
-  /**
-   * TrackerTask utility.
-   * @constructor
-   * @extends {tracking.EventEmitter}
-   */
-  tracking.TrackerTask = function(tracker) {
-    tracking.TrackerTask.base(this, 'constructor');
-
-    if (!tracker) {
-      throw new Error('Tracker instance not specified.');
-    }
-
-    this.setTracker(tracker);
-  };
-
-  tracking.inherits(tracking.TrackerTask, tracking.EventEmitter);
-
-  /**
-   * Holds the tracker instance managed by this task.
-   * @type {tracking.Tracker}
-   * @private
-   */
-  tracking.TrackerTask.prototype.tracker_ = null;
-
-  /**
-   * Holds if the tracker task is in running.
-   * @type {boolean}
-   * @private
-   */
-  tracking.TrackerTask.prototype.running_ = false;
-
-  /**
-   * Gets the tracker instance managed by this task.
-   * @return {tracking.Tracker}
-   */
-  tracking.TrackerTask.prototype.getTracker = function() {
-    return this.tracker_;
-  };
-
-  /**
-   * Returns true if the tracker task is in running, false otherwise.
-   * @return {boolean}
-   * @private
-   */
-  tracking.TrackerTask.prototype.inRunning = function() {
-    return this.running_;
-  };
-
-  /**
-   * Sets if the tracker task is in running.
-   * @param {boolean} running
-   * @private
-   */
-  tracking.TrackerTask.prototype.setRunning = function(running) {
-    this.running_ = running;
-  };
-
-  /**
-   * Sets the tracker instance managed by this task.
-   * @return {tracking.Tracker}
-   */
-  tracking.TrackerTask.prototype.setTracker = function(tracker) {
-    this.tracker_ = tracker;
-  };
-
-  /**
-   * Emits a `run` event on the tracker task for the implementers to run any
-   * child action, e.g. `requestAnimationFrame`.
-   * @return {object} Returns itself, so calls can be chained.
-   */
-  tracking.TrackerTask.prototype.run = function() {
-    var self = this;
-
-    if (this.inRunning()) {
-      return;
-    }
-
-    this.setRunning(true);
-    this.reemitTrackEvent_ = function(event) {
-      self.emit('track', event);
-    };
-    this.tracker_.on('track', this.reemitTrackEvent_);
-    this.emit('run');
-    return this;
-  };
-
-  /**
-   * Emits a `stop` event on the tracker task for the implementers to stop any
-   * child action being done, e.g. `requestAnimationFrame`.
-   * @return {object} Returns itself, so calls can be chained.
-   */
-  tracking.TrackerTask.prototype.stop = function() {
-    if (!this.inRunning()) {
-      return;
-    }
-
-    this.setRunning(false);
-    this.emit('stop');
-    this.tracker_.removeListener('track', this.reemitTrackEvent_);
-    return this;
-  };
-}());
-
-(function() {
-  /**
-   * ColorTracker utility to track colored blobs in a frame using color
-   * difference evaluation.
-   * @constructor
-   * @param {string|Array.<string>} opt_colors Optional colors to track.
-   * @extends {tracking.Tracker}
-   */
-  tracking.ColorTracker = function(opt_colors) {
-    tracking.ColorTracker.base(this, 'constructor');
-
-    if (typeof opt_colors === 'string') {
-      opt_colors = [opt_colors];
-    }
-
-    if (opt_colors) {
-      opt_colors.forEach(function(color) {
-        if (!tracking.ColorTracker.getColor(color)) {
-          throw new Error('Color not valid, try `new tracking.ColorTracker("magenta")`.');
-        }
-      });
-      this.setColors(opt_colors);
-    }
-  };
-
-  tracking.inherits(tracking.ColorTracker, tracking.Tracker);
-
-  /**
-   * Holds the known colors.
-   * @type {Object.<string, function>}
-   * @private
-   * @static
-   */
-  tracking.ColorTracker.knownColors_ = {};
-
-  /**
-   * Caches coordinates values of the neighbours surrounding a pixel.
-   * @type {Object.<number, Int32Array>}
-   * @private
-   * @static
-   */
-  tracking.ColorTracker.neighbours_ = {};
-
-  /**
-   * Registers a color as known color.
-   * @param {string} name The color name.
-   * @param {function} fn The color function to test if the passed (r,g,b) is
-   *     the desired color.
-   * @static
-   */
-  tracking.ColorTracker.registerColor = function(name, fn) {
-    tracking.ColorTracker.knownColors_[name] = fn;
-  };
-
-  /**
-   * Gets the known color function that is able to test whether an (r,g,b) is
-   * the desired color.
-   * @param {string} name The color name.
-   * @return {function} The known color test function.
-   * @static
-   */
-  tracking.ColorTracker.getColor = function(name) {
-    return tracking.ColorTracker.knownColors_[name];
-  };
-
-  /**
-   * Holds the colors to be tracked by the `ColorTracker` instance.
-   * @default ['magenta']
-   * @type {Array.<string>}
-   */
-  tracking.ColorTracker.prototype.colors = ['magenta'];
-
-  /**
-   * Holds the minimum dimension to classify a rectangle.
-   * @default 20
-   * @type {number}
-   */
-  tracking.ColorTracker.prototype.minDimension = 20;
-
-  /**
-   * Holds the maximum dimension to classify a rectangle.
-   * @default Infinity
-   * @type {number}
-   */
-  tracking.ColorTracker.prototype.maxDimension = Infinity;
-
-
-  /**
-   * Holds the minimum group size to be classified as a rectangle.
-   * @default 30
-   * @type {number}
-   */
-  tracking.ColorTracker.prototype.minGroupSize = 30;
-
-  /**
-   * Calculates the central coordinate from the cloud points. The cloud points
-   * are all points that matches the desired color.
-   * @param {Array.<number>} cloud Major row order array containing all the
-   *     points from the desired color, e.g. [x1, y1, c2, y2, ...].
-   * @param {number} total Total numbers of pixels of the desired color.
-   * @return {object} Object containing the x, y and estimated z coordinate of
-   *     the blog extracted from the cloud points.
-   * @private
-   */
-  tracking.ColorTracker.prototype.calculateDimensions_ = function(cloud, total) {
-    var maxx = -1;
-    var maxy = -1;
-    var minx = Infinity;
-    var miny = Infinity;
-
-    for (var c = 0; c < total; c += 2) {
-      var x = cloud[c];
-      var y = cloud[c + 1];
-
-      if (x < minx) {
-        minx = x;
-      }
-      if (x > maxx) {
-        maxx = x;
-      }
-      if (y < miny) {
-        miny = y;
-      }
-      if (y > maxy) {
-        maxy = y;
-      }
-    }
-
-    return {
-      width: maxx - minx,
-      height: maxy - miny,
-      x: minx,
-      y: miny
-    };
-  };
-
-  /**
-   * Gets the colors being tracked by the `ColorTracker` instance.
-   * @return {Array.<string>}
-   */
-  tracking.ColorTracker.prototype.getColors = function() {
-    return this.colors;
-  };
-
-  /**
-   * Gets the minimum dimension to classify a rectangle.
-   * @return {number}
-   */
-  tracking.ColorTracker.prototype.getMinDimension = function() {
-    return this.minDimension;
-  };
-
-  /**
-   * Gets the maximum dimension to classify a rectangle.
-   * @return {number}
-   */
-  tracking.ColorTracker.prototype.getMaxDimension = function() {
-    return this.maxDimension;
-  };
-
-  /**
-   * Gets the minimum group size to be classified as a rectangle.
-   * @return {number}
-   */
-  tracking.ColorTracker.prototype.getMinGroupSize = function() {
-    return this.minGroupSize;
-  };
-
-  /**
-   * Gets the eight offset values of the neighbours surrounding a pixel.
-   * @param {number} width The image width.
-   * @return {array} Array with the eight offset values of the neighbours
-   *     surrounding a pixel.
-   * @private
-   */
-  tracking.ColorTracker.prototype.getNeighboursForWidth_ = function(width) {
-    if (tracking.ColorTracker.neighbours_[width]) {
-      return tracking.ColorTracker.neighbours_[width];
-    }
-
-    var neighbours = new Int32Array(8);
-
-    neighbours[0] = -width * 4;
-    neighbours[1] = -width * 4 + 4;
-    neighbours[2] = 4;
-    neighbours[3] = width * 4 + 4;
-    neighbours[4] = width * 4;
-    neighbours[5] = width * 4 - 4;
-    neighbours[6] = -4;
-    neighbours[7] = -width * 4 - 4;
-
-    tracking.ColorTracker.neighbours_[width] = neighbours;
-
-    return neighbours;
-  };
-
-  /**
-   * Unites groups whose bounding box intersect with each other.
-   * @param {Array.<Object>} rects
-   * @private
-   */
-  tracking.ColorTracker.prototype.mergeRectangles_ = function(rects) {
-    var intersects;
-    var results = [];
-    var minDimension = this.getMinDimension();
-    var maxDimension = this.getMaxDimension();
-
-    for (var r = 0; r < rects.length; r++) {
-      var r1 = rects[r];
-      intersects = true;
-      for (var s = r + 1; s < rects.length; s++) {
-        var r2 = rects[s];
-        if (tracking.Math.intersectRect(r1.x, r1.y, r1.x + r1.width, r1.y + r1.height, r2.x, r2.y, r2.x + r2.width, r2.y + r2.height)) {
-          intersects = false;
-          var x1 = Math.min(r1.x, r2.x);
-          var y1 = Math.min(r1.y, r2.y);
-          var x2 = Math.max(r1.x + r1.width, r2.x + r2.width);
-          var y2 = Math.max(r1.y + r1.height, r2.y + r2.height);
-          r2.height = y2 - y1;
-          r2.width = x2 - x1;
-          r2.x = x1;
-          r2.y = y1;
-          break;
-        }
-      }
-
-      if (intersects) {
-        if (r1.width >= minDimension && r1.height >= minDimension) {
-          if (r1.width <= maxDimension && r1.height <= maxDimension) {
-            results.push(r1);
-          }
-        }
-      }
-    }
-
-    return results;
-  };
-
-  /**
-   * Sets the colors to be tracked by the `ColorTracker` instance.
-   * @param {Array.<string>} colors
-   */
-  tracking.ColorTracker.prototype.setColors = function(colors) {
-    this.colors = colors;
-  };
-
-  /**
-   * Sets the minimum dimension to classify a rectangle.
-   * @param {number} minDimension
-   */
-  tracking.ColorTracker.prototype.setMinDimension = function(minDimension) {
-    this.minDimension = minDimension;
-  };
-
-  /**
-   * Sets the maximum dimension to classify a rectangle.
-   * @param {number} maxDimension
-   */
-  tracking.ColorTracker.prototype.setMaxDimension = function(maxDimension) {
-    this.maxDimension = maxDimension;
-  };
-
-  /**
-   * Sets the minimum group size to be classified as a rectangle.
-   * @param {number} minGroupSize
-   */
-  tracking.ColorTracker.prototype.setMinGroupSize = function(minGroupSize) {
-    this.minGroupSize = minGroupSize;
-  };
-
-  /**
-   * Tracks the `Video` frames. This method is called for each video frame in
-   * order to emit `track` event.
-   * @param {Uint8ClampedArray} pixels The pixels data to track.
-   * @param {number} width The pixels canvas width.
-   * @param {number} height The pixels canvas height.
-   */
-  tracking.ColorTracker.prototype.track = function(pixels, width, height) {
-    var self = this;
-    var colors = this.getColors();
-
-    if (!colors) {
-      throw new Error('Colors not specified, try `new tracking.ColorTracker("magenta")`.');
-    }
-
-    var results = [];
-
-    colors.forEach(function(color) {
-      results = results.concat(self.trackColor_(pixels, width, height, color));
-    });
-
-    this.emit('track', {
-      data: results
-    });
-  };
-
-  /**
-   * Find the given color in the given matrix of pixels using Flood fill
-   * algorithm to determines the area connected to a given node in a
-   * multi-dimensional array.
-   * @param {Uint8ClampedArray} pixels The pixels data to track.
-   * @param {number} width The pixels canvas width.
-   * @param {number} height The pixels canvas height.
-   * @param {string} color The color to be found
-   * @private
-   */
-  tracking.ColorTracker.prototype.trackColor_ = function(pixels, width, height, color) {
-    var colorFn = tracking.ColorTracker.knownColors_[color];
-    var currGroup = new Int32Array(pixels.length >> 2);
-    var currGroupSize;
-    var currI;
-    var currJ;
-    var currW;
-    var marked = new Int8Array(pixels.length);
-    var minGroupSize = this.getMinGroupSize();
-    var neighboursW = this.getNeighboursForWidth_(width);
-    var queue = new Int32Array(pixels.length);
-    var queuePosition;
-    var results = [];
-    var w = -4;
-
-    if (!colorFn) {
-      return results;
-    }
-
-    for (var i = 0; i < height; i++) {
-      for (var j = 0; j < width; j++) {
-        w += 4;
-
-        if (marked[w]) {
-          continue;
-        }
-
-        currGroupSize = 0;
-
-        queuePosition = -1;
-        queue[++queuePosition] = w;
-        queue[++queuePosition] = i;
-        queue[++queuePosition] = j;
-
-        marked[w] = 1;
-
-        while (queuePosition >= 0) {
-          currJ = queue[queuePosition--];
-          currI = queue[queuePosition--];
-          currW = queue[queuePosition--];
-
-          if (colorFn(pixels[currW], pixels[currW + 1], pixels[currW + 2], pixels[currW + 3], currW, currI, currJ)) {
-            currGroup[currGroupSize++] = currJ;
-            currGroup[currGroupSize++] = currI;
-
-            for (var k = 0; k < neighboursW.length; k++) {
-              var otherW = currW + neighboursW[k];
-              var otherI = currI + neighboursI[k];
-              var otherJ = currJ + neighboursJ[k];
-              if (!marked[otherW] && otherI >= 0 && otherI < height && otherJ >= 0 && otherJ < width) {
-                queue[++queuePosition] = otherW;
-                queue[++queuePosition] = otherI;
-                queue[++queuePosition] = otherJ;
-
-                marked[otherW] = 1;
-              }
-            }
-          }
-        }
-
-        if (currGroupSize >= minGroupSize) {
-          var data = this.calculateDimensions_(currGroup, currGroupSize);
-          if (data) {
-            data.color = color;
-            results.push(data);
-          }
-        }
-      }
-    }
-
-    return this.mergeRectangles_(results);
-  };
-
-  // Default colors
-  //===================
-
-  tracking.ColorTracker.registerColor('cyan', function(r, g, b) {
-    var thresholdGreen = 50,
-      thresholdBlue = 70,
-      dx = r - 0,
-      dy = g - 255,
-      dz = b - 255;
-
-    if ((g - r) >= thresholdGreen && (b - r) >= thresholdBlue) {
-      return true;
-    }
-    return dx * dx + dy * dy + dz * dz < 6400;
-  });
-
-  tracking.ColorTracker.registerColor('magenta', function(r, g, b) {
-    var threshold = 50,
-      dx = r - 255,
-      dy = g - 0,
-      dz = b - 255;
-
-    if ((r - g) >= threshold && (b - g) >= threshold) {
-      return true;
-    }
-    return dx * dx + dy * dy + dz * dz < 19600;
-  });
-
-  tracking.ColorTracker.registerColor('yellow', function(r, g, b) {
-    var threshold = 50,
-      dx = r - 255,
-      dy = g - 255,
-      dz = b - 0;
-
-    if ((r - b) >= threshold && (g - b) >= threshold) {
-      return true;
-    }
-    return dx * dx + dy * dy + dz * dz < 10000;
-  });
-
-
-  // Caching neighbour i/j offset values.
-  //=====================================
-  var neighboursI = new Int32Array([-1, -1, 0, 1, 1, 1, 0, -1]);
-  var neighboursJ = new Int32Array([0, 1, 1, 1, 0, -1, -1, -1]);
-}());
-
-(function() {
-  /**
-   * ObjectTracker utility.
-   * @constructor
-   * @param {string|Array.<string|Array.<number>>} opt_classifiers Optional
-   *     object classifiers to track.
-   * @extends {tracking.Tracker}
-   */
-  tracking.ObjectTracker = function(opt_classifiers) {
-    tracking.ObjectTracker.base(this, 'constructor');
-
-    if (opt_classifiers) {
-      if (!Array.isArray(opt_classifiers)) {
-        opt_classifiers = [opt_classifiers];
-      }
-
-      if (Array.isArray(opt_classifiers)) {
-        opt_classifiers.forEach(function(classifier, i) {
-          if (typeof classifier === 'string') {
-            opt_classifiers[i] = tracking.ViolaJones.classifiers[classifier];
-          }
-          if (!opt_classifiers[i]) {
-            throw new Error('Object classifier not valid, try `new tracking.ObjectTracker("face")`.');
-          }
-        });
-      }
-    }
-
-    this.setClassifiers(opt_classifiers);
-  };
-
-  tracking.inherits(tracking.ObjectTracker, tracking.Tracker);
-
-  /**
-   * Specifies the edges density of a block in order to decide whether to skip
-   * it or not.
-   * @default 0.2
-   * @type {number}
-   */
-  tracking.ObjectTracker.prototype.edgesDensity = 0.2;
-
-  /**
-   * Specifies the initial scale to start the feature block scaling.
-   * @default 1.0
-   * @type {number}
-   */
-  tracking.ObjectTracker.prototype.initialScale = 1.0;
-
-  /**
-   * Specifies the scale factor to scale the feature block.
-   * @default 1.25
-   * @type {number}
-   */
-  tracking.ObjectTracker.prototype.scaleFactor = 1.25;
-
-  /**
-   * Specifies the block step size.
-   * @default 1.5
-   * @type {number}
-   */
-  tracking.ObjectTracker.prototype.stepSize = 1.5;
-
-  /**
-   * Gets the tracker HAAR classifiers.
-   * @return {TypedArray.<number>}
-   */
-  tracking.ObjectTracker.prototype.getClassifiers = function() {
-    return this.classifiers;
-  };
-
-  /**
-   * Gets the edges density value.
-   * @return {number}
-   */
-  tracking.ObjectTracker.prototype.getEdgesDensity = function() {
-    return this.edgesDensity;
-  };
-
-  /**
-   * Gets the initial scale to start the feature block scaling.
-   * @return {number}
-   */
-  tracking.ObjectTracker.prototype.getInitialScale = function() {
-    return this.initialScale;
-  };
-
-  /**
-   * Gets the scale factor to scale the feature block.
-   * @return {number}
-   */
-  tracking.ObjectTracker.prototype.getScaleFactor = function() {
-    return this.scaleFactor;
-  };
-
-  /**
-   * Gets the block step size.
-   * @return {number}
-   */
-  tracking.ObjectTracker.prototype.getStepSize = function() {
-    return this.stepSize;
-  };
-
-  /**
-   * Tracks the `Video` frames. This method is called for each video frame in
-   * order to emit `track` event.
-   * @param {Uint8ClampedArray} pixels The pixels data to track.
-   * @param {number} width The pixels canvas width.
-   * @param {number} height The pixels canvas height.
-   */
-  tracking.ObjectTracker.prototype.track = function(pixels, width, height) {
-    var self = this;
-    var classifiers = this.getClassifiers();
-
-    if (!classifiers) {
-      throw new Error('Object classifier not specified, try `new tracking.ObjectTracker("face")`.');
-    }
-
-    var results = [];
-
-    classifiers.forEach(function(classifier) {
-      results = results.concat(tracking.ViolaJones.detect(pixels, width, height, self.getInitialScale(), self.getScaleFactor(), self.getStepSize(), self.getEdgesDensity(), classifier));
-    });
-
-    this.emit('track', {
-      data: results
-    });
-  };
-
-  /**
-   * Sets the tracker HAAR classifiers.
-   * @param {TypedArray.<number>} classifiers
-   */
-  tracking.ObjectTracker.prototype.setClassifiers = function(classifiers) {
-    this.classifiers = classifiers;
-  };
-
-  /**
-   * Sets the edges density.
-   * @param {number} edgesDensity
-   */
-  tracking.ObjectTracker.prototype.setEdgesDensity = function(edgesDensity) {
-    this.edgesDensity = edgesDensity;
-  };
-
-  /**
-   * Sets the initial scale to start the block scaling.
-   * @param {number} initialScale
-   */
-  tracking.ObjectTracker.prototype.setInitialScale = function(initialScale) {
-    this.initialScale = initialScale;
-  };
-
-  /**
-   * Sets the scale factor to scale the feature block.
-   * @param {number} scaleFactor
-   */
-  tracking.ObjectTracker.prototype.setScaleFactor = function(scaleFactor) {
-    this.scaleFactor = scaleFactor;
-  };
-
-  /**
-   * Sets the block step size.
-   * @param {number} stepSize
-   */
-  tracking.ObjectTracker.prototype.setStepSize = function(stepSize) {
-    this.stepSize = stepSize;
-  };
-
-}());
-
-(function() {
-
-
-  tracking.LandmarksTracker = function() {
-    tracking.LandmarksTracker.base(this, 'constructor');
-  }
-
-  tracking.inherits(tracking.LandmarksTracker, tracking.ObjectTracker);
-
-  tracking.LandmarksTracker.prototype.track = function(pixels, width, height) {
-	 
-    var image = {
-      'data': pixels,
-      'width': width,
-      'height': height
-    };
-
-    var classifier = tracking.ViolaJones.classifiers['face'];
-
-    var faces = tracking.ViolaJones.detect(pixels, width, height, 
-      this.getInitialScale(), this.getScaleFactor(), this.getStepSize(), 
-      this.getEdgesDensity(), classifier);
-
-    var landmarks = tracking.LBF.align(pixels, width, height, faces);
-
-    this.emit('track', {
-      'data': {
-        'faces' : faces,
-        'landmarks' : landmarks
-      }
-    });
-
-  }
-
-}());
-
-(function() {
-
-  tracking.LBF = {};
-
-  /**
-   * LBF Regressor utility.
-   * @constructor
-   */
-  tracking.LBF.Regressor = function(maxNumStages){
-    this.maxNumStages = maxNumStages;
-
-    this.rfs = new Array(maxNumStages);
-    this.models = new Array(maxNumStages);
-    for(var i=0; i < maxNumStages; i++){
-      this.rfs[i] = new tracking.LBF.RandomForest(i);
-      this.models[i] = tracking.LBF.RegressorData[i].models;
-    }
-
-    this.meanShape = tracking.LBF.LandmarksData;
-  }
-
-  /**
-   * Predicts the position of the landmarks based on the bounding box of the face.
-   * @param {pixels} pixels The grayscale pixels in a linear array.
-   * @param {number} width Width of the image.
-   * @param {number} height Height of the image.
-   * @param {object} boudingBox Bounding box of the face to be aligned.
-   * @return {matrix} A matrix with each landmark position in a row [x,y].
-   */
-  tracking.LBF.Regressor.prototype.predict = function(pixels, width, height, boundingBox) {
-
-    var images = [];
-    var currentShapes = [];
-    var boundingBoxes = [];
-
-    var meanShapeClone = tracking.Matrix.clone(this.meanShape);
-
-    images.push({
-      'data': pixels,
-      'width': width,
-      'height': height
-    });
-    boundingBoxes.push(boundingBox);
-
-    currentShapes.push(tracking.LBF.projectShapeToBoundingBox_(meanShapeClone, boundingBox));
-
-    for(var stage = 0; stage < this.maxNumStages; stage++){
-      var binaryFeatures = tracking.LBF.Regressor.deriveBinaryFeat(this.rfs[stage], images, currentShapes, boundingBoxes, meanShapeClone);
-      this.applyGlobalPrediction(binaryFeatures, this.models[stage], currentShapes, boundingBoxes);
-    }
-
-    return currentShapes[0];
-  };
-
-  /**
-   * Multiplies the binary features of the landmarks with the regression matrix
-   * to obtain the displacement for each landmark. Then applies this displacement
-   * into the landmarks shape.
-   * @param {object} binaryFeatures The binary features for the landmarks.
-   * @param {object} models The regressor models.
-   * @param {matrix} currentShapes The landmarks shapes.
-   * @param {array} boudingBoxes The bounding boxes of the faces.
-   */
-  tracking.LBF.Regressor.prototype.applyGlobalPrediction = function(binaryFeatures, models, currentShapes, 
-    boundingBoxes){
-
-    var residual = currentShapes[0].length * 2;
-
-    var rotation = [];
-    var deltashape = new Array(residual/2);
-    for(var i=0; i < residual/2; i++){
-      deltashape[i] = [0.0, 0.0];
-    }
-
-    for(var i=0; i < currentShapes.length; i++){
-      for(var j=0; j < residual; j++){
-        var tmp = 0;
-        for(var lx=0, idx=0; (idx = binaryFeatures[i][lx].index) != -1; lx++){
-          if(idx <= models[j].nr_feature){
-            tmp += models[j].data[(idx - 1)] * binaryFeatures[i][lx].value;
-          }
-        }
-        if(j < residual/2){
-          deltashape[j][0] = tmp;
-        }else{
-          deltashape[j - residual/2][1] = tmp;
-        }
-      }
-
-      var res = tracking.LBF.similarityTransform_(tracking.LBF.unprojectShapeToBoundingBox_(currentShapes[i], boundingBoxes[i]), this.meanShape);
-      var rotation = tracking.Matrix.transpose(res[0]);
-
-      var s = tracking.LBF.unprojectShapeToBoundingBox_(currentShapes[i], boundingBoxes[i]);
-      s = tracking.Matrix.add(s, deltashape);
-
-      currentShapes[i] = tracking.LBF.projectShapeToBoundingBox_(s, boundingBoxes[i]);
-
-    }
-  };
-
-  /**
-   * Derives the binary features from the image for each landmark. 
-   * @param {object} forest The random forest to search for the best binary feature match.
-   * @param {array} images The images with pixels in a grayscale linear array.
-   * @param {array} currentShapes The current landmarks shape.
-   * @param {array} boudingBoxes The bounding boxes of the faces.
-   * @param {matrix} meanShape The mean shape of the current landmarks set.
-   * @return {array} The binary features extracted from the image and matched with the
-   *     training data.
-   * @static
-   */
-  tracking.LBF.Regressor.deriveBinaryFeat = function(forest, images, currentShapes, boundingBoxes, meanShape){
-
-    var binaryFeatures = new Array(images.length);
-    for(var i=0; i < images.length; i++){
-      var t = forest.maxNumTrees * forest.landmarkNum + 1;
-      binaryFeatures[i] = new Array(t);
-      for(var j=0; j < t; j++){
-        binaryFeatures[i][j] = {};
-      }
-    }
-
-    var leafnodesPerTree = 1 << (forest.maxDepth - 1);
-
-    for(var i=0; i < images.length; i++){
-
-      var projectedShape = tracking.LBF.unprojectShapeToBoundingBox_(currentShapes[i], boundingBoxes[i]);
-      var transform = tracking.LBF.similarityTransform_(projectedShape, meanShape);
-      
-      for(var j=0; j < forest.landmarkNum; j++){
-        for(var k=0; k < forest.maxNumTrees; k++){
-
-          var binaryCode = tracking.LBF.Regressor.getCodeFromTree(forest.rfs[j][k], images[i], 
-                              currentShapes[i], boundingBoxes[i], transform[0], transform[1]);
-
-          var index = j*forest.maxNumTrees + k;
-          binaryFeatures[i][index].index = leafnodesPerTree * index + binaryCode;
-          binaryFeatures[i][index].value = 1;
-
-        }
-      }
-      binaryFeatures[i][forest.landmarkNum * forest.maxNumTrees].index = -1;
-      binaryFeatures[i][forest.landmarkNum * forest.maxNumTrees].value = -1;
-    }
-    return binaryFeatures;
-
-  }
-
-  /**
-   * Gets the binary code for a specific tree in a random forest. For each landmark,
-   * the position from two pre-defined points are recovered from the training data
-   * and then the intensity of the pixels corresponding to these points is extracted 
-   * from the image and used to traverse the trees in the random forest. At the end,
-   * the ending nodes will be represented by 1, and the remaining nodes by 0.
-   * 
-   * +--------------------------- Random Forest -----------------------------+ 
-   * | Ø = Ending leaf                                                       |
-   * |                                                                       |
-   * |       O             O             O             O             O       |
-   * |     /   \         /   \         /   \         /   \         /   \     |
-   * |    O     O       O     O       O     O       O     O       O     O    |
-   * |   / \   / \     / \   / \     / \   / \     / \   / \     / \   / \   |
-   * |  Ø   O O   O   O   O Ø   O   O   Ø O   O   O   O Ø   O   O   O O   Ø  |
-   * |  1   0 0   0   0   0 1   0   0   1 0   0   0   0 1   0   0   0 0   1  |
-   * +-----------------------------------------------------------------------+
-   * Final binary code for this landmark: 10000010010000100001
-   *
-   * @param {object} forest The tree to be analyzed.
-   * @param {array} image The image with pixels in a grayscale linear array.
-   * @param {matrix} shape The current landmarks shape.
-   * @param {object} boudingBoxes The bounding box of the face.
-   * @param {matrix} rotation The rotation matrix used to transform the projected landmarks
-   *     into the mean shape.
-   * @param {number} scale The scale factor used to transform the projected landmarks
-   *     into the mean shape.
-   * @return {number} The binary code extracted from the tree.
-   * @static
-   */
-  tracking.LBF.Regressor.getCodeFromTree = function(tree, image, shape, boundingBox, rotation, scale){
-    var current = 0;
-    var bincode = 0;
-
-    while(true){
-      
-      var x1 = Math.cos(tree.nodes[current].feats[0]) * tree.nodes[current].feats[2] * tree.maxRadioRadius * boundingBox.width;
-      var y1 = Math.sin(tree.nodes[current].feats[0]) * tree.nodes[current].feats[2] * tree.maxRadioRadius * boundingBox.height;
-      var x2 = Math.cos(tree.nodes[current].feats[1]) * tree.nodes[current].feats[3] * tree.maxRadioRadius * boundingBox.width;
-      var y2 = Math.sin(tree.nodes[current].feats[1]) * tree.nodes[current].feats[3] * tree.maxRadioRadius * boundingBox.height;
-
-      var project_x1 = rotation[0][0] * x1 + rotation[0][1] * y1;
-      var project_y1 = rotation[1][0] * x1 + rotation[1][1] * y1;
-
-      var real_x1 = Math.floor(project_x1 + shape[tree.landmarkID][0]);
-      var real_y1 = Math.floor(project_y1 + shape[tree.landmarkID][1]);
-      real_x1 = Math.max(0.0, Math.min(real_x1, image.height - 1.0));
-      real_y1 = Math.max(0.0, Math.min(real_y1, image.width - 1.0));
-
-      var project_x2 = rotation[0][0] * x2 + rotation[0][1] * y2;
-      var project_y2 = rotation[1][0] * x2 + rotation[1][1] * y2;
-
-      var real_x2 = Math.floor(project_x2 + shape[tree.landmarkID][0]);
-      var real_y2 = Math.floor(project_y2 + shape[tree.landmarkID][1]);
-      real_x2 = Math.max(0.0, Math.min(real_x2, image.height - 1.0));
-      real_y2 = Math.max(0.0, Math.min(real_y2, image.width - 1.0));
-      var pdf = Math.floor(image.data[real_y1*image.width + real_x1]) - 
-          Math.floor(image.data[real_y2 * image.width +real_x2]);
-
-      if(pdf < tree.nodes[current].thresh){
-        current = tree.nodes[current].cnodes[0];
-      }else{
-        current = tree.nodes[current].cnodes[1];
-      }
-
-      if (tree.nodes[current].is_leafnode == 1) {
-        bincode = 1;
-        for (var i=0; i < tree.leafnodes.length; i++) {
-          if (tree.leafnodes[i] == current) {
-            return bincode;
-          }
-          bincode++;
-        }
-        return bincode;
-      }
-
-    }
-
-    return bincode;
-  }
-
-}());
-(function() {
-  /**
-   * Face Alignment via Regressing Local Binary Features (LBF)
-   * This approach has two components: a set of local binary features and
-   * a locality principle for learning those features.
-   * The locality principle is used to guide the learning of a set of highly
-   * discriminative local binary features for each landmark independently.
-   * The obtained local binary features are used to learn a linear regression
-   * that later will be used to guide the landmarks in the alignment phase.
-   * 
-   * @authors: VoxarLabs Team (http://cin.ufpe.br/~voxarlabs)
-   *           Lucas Figueiredo <lsf@cin.ufpe.br>, Thiago Menezes <tmc2@cin.ufpe.br>,
-   *           Thiago Domingues <tald@cin.ufpe.br>, Rafael Roberto <rar3@cin.ufpe.br>,
-   *           Thulio Araujo <tlsa@cin.ufpe.br>, Joao Victor <jvfl@cin.ufpe.br>,
-   *           Tomer Simis <tls@cin.ufpe.br>)
-   */
-  
-  /**
-   * Holds the maximum number of stages that will be used in the alignment algorithm.
-   * Each stage contains a different set of random forests and retrieves the binary
-   * code from a more "specialized" (i.e. smaller) region around the landmarks.
-   * @type {number}
-   * @static
-   */
-  tracking.LBF.maxNumStages = 4;
-
-  /**
-   * Holds the regressor that will be responsible for extracting the local features from 
-   * the image and guide the landmarks using the training data.
-   * @type {object}
-   * @protected
-   * @static
-   */
-  tracking.LBF.regressor_ = null; 
-  
-  /**
-   * Generates a set of landmarks for a set of faces
-   * @param {pixels} pixels The pixels in a linear [r,g,b,a,...] array.
-   * @param {number} width The image width.
-   * @param {number} height The image height.
-   * @param {array} faces The list of faces detected in the image
-   * @return {array} The aligned landmarks, each set of landmarks corresponding
-   *     to a specific face.
-   * @static
-   */
-  tracking.LBF.align = function(pixels, width, height, faces){
-
-    if(tracking.LBF.regressor_ == null){
-      tracking.LBF.regressor_ = new tracking.LBF.Regressor(
-        tracking.LBF.maxNumStages
-      );
-    }
-
-    pixels = tracking.Image.grayscale(pixels, width, height, false);
-
-    pixels = tracking.Image.equalizeHist(pixels, width, height);
-
-    var shapes = new Array(faces.length);
-
-    for(var i in faces){
-
-      faces[i].height = faces[i].width;
-
-      var boundingBox = {};
-      boundingBox.startX = faces[i].x;
-      boundingBox.startY = faces[i].y;
-      boundingBox.width = faces[i].width;
-      boundingBox.height = faces[i].height;
-
-      shapes[i] = tracking.LBF.regressor_.predict(pixels, width, height, boundingBox);
-    }
-
-    return shapes;
-  }
-
-  /**
-   * Unprojects the landmarks shape from the bounding box.
-   * @param {matrix} shape The landmarks shape.
-   * @param {matrix} boudingBox The bounding box.
-   * @return {matrix} The landmarks shape projected into the bounding box.
-   * @static
-   * @protected
-   */
-  tracking.LBF.unprojectShapeToBoundingBox_ = function(shape, boundingBox){
-    var temp = new Array(shape.length);
-    for(var i=0; i < shape.length; i++){
-      temp[i] = [
-        (shape[i][0] - boundingBox.startX) / boundingBox.width,
-        (shape[i][1] - boundingBox.startY) / boundingBox.height
-      ];
-    }
-    return temp;
-  }
-
-  /**
-   * Projects the landmarks shape into the bounding box. The landmarks shape has
-   * normalized coordinates, so it is necessary to map these coordinates into
-   * the bounding box coordinates.
-   * @param {matrix} shape The landmarks shape.
-   * @param {matrix} boudingBox The bounding box.
-   * @return {matrix} The landmarks shape.
-   * @static
-   * @protected
-   */
-  tracking.LBF.projectShapeToBoundingBox_ = function(shape, boundingBox){
-    var temp = new Array(shape.length);
-    for(var i=0; i < shape.length; i++){
-      temp[i] = [
-        shape[i][0] * boundingBox.width + boundingBox.startX,
-        shape[i][1] * boundingBox.height + boundingBox.startY
-      ];
-    }
-    return temp;
-  }
-
-  /**
-   * Calculates the rotation and scale necessary to transform shape1 into shape2.
-   * @param {matrix} shape1 The shape to be transformed.
-   * @param {matrix} shape2 The shape to be transformed in.
-   * @return {[matrix, scalar]} The rotation matrix and scale that applied to shape1
-   *     results in shape2.
-   * @static
-   * @protected
-   */
-  tracking.LBF.similarityTransform_ = function(shape1, shape2){
-
-    var center1 = [0,0];
-    var center2 = [0,0];
-    for (var i = 0; i < shape1.length; i++) {
-      center1[0] += shape1[i][0];
-      center1[1] += shape1[i][1];
-      center2[0] += shape2[i][0];
-      center2[1] += shape2[i][1];
-    }
-    center1[0] /= shape1.length;
-    center1[1] /= shape1.length;
-    center2[0] /= shape2.length;
-    center2[1] /= shape2.length;
-
-    var temp1 = tracking.Matrix.clone(shape1);
-    var temp2 = tracking.Matrix.clone(shape2);
-    for(var i=0; i < shape1.length; i++){
-      temp1[i][0] -= center1[0];
-      temp1[i][1] -= center1[1];
-      temp2[i][0] -= center2[0];
-      temp2[i][1] -= center2[1];
-    }
-
-    var covariance1, covariance2;
-    var mean1, mean2;
-
-    var t = tracking.Matrix.calcCovarMatrix(temp1);
-    covariance1 = t[0];
-    mean1 = t[1];
-
-    t = tracking.Matrix.calcCovarMatrix(temp2);
-    covariance2 = t[0];
-    mean2 = t[1];
-
-    var s1 = Math.sqrt(tracking.Matrix.norm(covariance1));
-    var s2 = Math.sqrt(tracking.Matrix.norm(covariance2));
-
-    var scale = s1/s2;
-    temp1 = tracking.Matrix.mulScalar(1.0/s1, temp1);
-    temp2 = tracking.Matrix.mulScalar(1.0/s2, temp2);
-
-    var num = 0, den = 0;
-    for (var i = 0; i < shape1.length; i++) {
-      num = num + temp1[i][1] * temp2[i][0] - temp1[i][0] * temp2[i][1];
-      den = den + temp1[i][0] * temp2[i][0] + temp1[i][1] * temp2[i][1];
-    }
-
-    var norm = Math.sqrt(num*num + den*den);
-    var sin_theta = num/norm;
-    var cos_theta = den/norm;
-    var rotation = [
-      [cos_theta, -sin_theta],
-      [sin_theta, cos_theta]
-    ];
-
-    return [rotation, scale];
-  }
-
-  /**
-   * LBF Random Forest data structure.
-   * @static
-   * @constructor
-   */
-  tracking.LBF.RandomForest = function(forestIndex){
-    this.maxNumTrees = tracking.LBF.RegressorData[forestIndex].max_numtrees;
-    this.landmarkNum = tracking.LBF.RegressorData[forestIndex].num_landmark;
-    this.maxDepth = tracking.LBF.RegressorData[forestIndex].max_depth;
-    this.stages = tracking.LBF.RegressorData[forestIndex].stages; 
-
-    this.rfs = new Array(this.landmarkNum);
-    for(var i=0; i < this.landmarkNum; i++){
-      this.rfs[i] = new Array(this.maxNumTrees);
-      for(var j=0; j < this.maxNumTrees; j++){
-        this.rfs[i][j] = new tracking.LBF.Tree(forestIndex, i, j);
-      }
-    }
-  }
-
-  /**
-   * LBF Tree data structure.
-   * @static
-   * @constructor
-   */
-  tracking.LBF.Tree = function(forestIndex, landmarkIndex, treeIndex){
-    var data = tracking.LBF.RegressorData[forestIndex].landmarks[landmarkIndex][treeIndex];
-    this.maxDepth = data.max_depth;
-    this.maxNumNodes = data.max_numnodes;
-    this.nodes = data.nodes;
-    this.landmarkID = data.landmark_id;
-    this.numLeafnodes = data.num_leafnodes;
-    this.numNodes = data.num_nodes;
-    this.maxNumFeats = data.max_numfeats;
-    this.maxRadioRadius = data.max_radio_radius;
-    this.leafnodes = data.id_leafnodes;
-  }
-
-}());

+ 17 - 2
src/store/modules/common.js

@@ -36,7 +36,7 @@ const commonStore = defineStore("common", {
 
     doorArray: {
       systemSeeting: {
-        relayControl: false,//继电器开关状态
+        relayControl: true,//继电器开关状态
         navBarNew: false,//导航栏显示隐藏
         statusBar: false,//状态显示隐藏
         statusBarDrop: false,//状态面板下拉
@@ -44,7 +44,22 @@ const commonStore = defineStore("common", {
     },//门禁系统配置数据存储
 
   }),
-  unistorage: true,
+  // 只持久化门禁配置;兼容 App 端 getStorageSync 可能已是对象
+  unistorage: {
+    paths: ["doorArray"],
+    serializer: {
+      serialize: (v) => JSON.stringify(v),
+      deserialize: (v) => {
+        if (v == null || v === "") return {};
+        if (typeof v === "object") return v;
+        try {
+          return JSON.parse(v);
+        } catch (e) {
+          return {};
+        }
+      },
+    },
+  },
   actions: {
   },
 });

+ 5 - 45
src/store/modules/control.js

@@ -346,35 +346,6 @@ const controlStore = defineStore("control", {
                 .sort((a, b) => new Date(`${dayjs().format("YYYY-MM-DD")}T${a.startTime}`) - new Date(`${dayjs().format("YYYY-MM-DD")}T${b.startTime}`));
             return newTimesXleList;
         },
-        /**
-         * @人脸验证
-         */
-        faceVerify(imageBase) {
-            var that = this
-            console.log(1)
-
-            faceApi()
-                .facePerson({
-                    faceBase: imageBase,
-                })
-                .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);
-                        }
-                    }
-
-                    that.handleChildren({ funcName: "人脸冷却", data: {} });
-                }).catch((err) => {
-                    that.handleChildren({ funcName: "人脸冷却", data: {} });
-                });
-        },
         /**
          * @会议验证
          */
@@ -459,8 +430,9 @@ const controlStore = defineStore("control", {
             doorApi()
                 .control({
                     skipCheck: true,
-                    userId: !getToken() ? event.userId : undefined,
-                    userName: !getToken() ? event.userName : undefined,
+                    userId: event.userId ? event.userId : undefined,
+                    userName: event.userName ? event.userName : undefined,
+                    passType: "1", // 1: 人脸 2: 刷卡 3: 手机
                     productCode: "502_USKY",
                     deviceUuid: this.form.door.deviceUuid,
                     commandCode: "door_onoff",
@@ -491,13 +463,11 @@ const controlStore = defineStore("control", {
             if ("funcName" in event) {
                 if (event.funcName == "打开门禁配置") {
                     this.handleChildren({ funcName: "关闭摄像头", data: {} });
-                    tab.navigateTo("/pages/door/setting/index")
+                    tab.navigateTo("/pages/setting/index")
                 } else if (event.funcName == "打开会议配置") {
                     this.handleChildren({ funcName: "关闭摄像头", data: {} });
                     // 跳转到密码验证页面
-                    tab.navigateTo("/pages/meeting/setting/password");
-                } else if (event.funcName == "人脸识别") {
-                    this.faceVerify(event.data.imageBase);
+                    tab.navigateTo("/pages/setting/password");
                 } else if (event.funcName == "点击开门") {
                     this.openDoor({
                         userId: "",
@@ -592,16 +562,6 @@ const controlStore = defineStore("control", {
                 }
             }
         },
-        /**
-         * @门禁开关
-         */
-        doorControl(value) {
-            if (value) {
-                sysPlugins.openDoor(2, 0);
-            } else {
-                sysPlugins.openDoor(3, 0);
-            }
-        }
     },
 });
 

+ 0 - 17
src/utils/dict.js

@@ -1,17 +0,0 @@
-import { getDicts } from '@/api/system/dict.js'
-import { ref, toRefs } from "vue";
-/**
- * 获取字典数据
- */
-export function useDict(...args) {
-    const res = ref({});
-    return (() => {
-        args.forEach((d, index) => {
-            res.value[d] = [];
-            getDicts(d).then(resp => {
-                res.value[d] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
-            })
-        })
-        return toRefs(res.value);
-    })()
-}

+ 0 - 45
src/utils/hideHead.js

@@ -1,45 +0,0 @@
-// 页面白名单
-const whiteList = [];
-
-export default {
-	mounted() {
-		if (this.isWeiXinBrowser() || this.isQQBrowser()) {
-			this.navTitle()
-		}
-	},
-	methods: {
-		isWeiXinBrowser() { //判断是否为微信
-			let ua = navigator?.userAgent.toLowerCase()
-			if (ua) {
-				return ua.indexOf('micromessenger') != -1
-			}
-		},
-		isQQBrowser() { //判断是否为qq
-			var ua = navigator?.userAgent.toLowerCase()
-			if (ua?.match(/QQ/i) == "qq") {
-				return true
-			} else {
-				return false
-			}
-		},
-		navTitle() {
-			this.$nextTick(() => {
-				const pages = getCurrentPages(); // 获取当前页面栈
-				const currentPage = pages[pages.length - 1]; // 最后一个元素即为当前页面
-
-				if (whiteList.includes(currentPage?.route)) {
-					return
-				}
-
-				let navTitleDom = document.getElementsByTagName('uni-page-head')
-				if (navTitleDom.length) {
-					navTitleDom[0].style.display = 'none'
-				}
-				let navTitleDom1 = document.querySelector('.u-navbar')
-				if (navTitleDom1) {
-					navTitleDom1.style.display = 'none'
-				}
-			})
-		},
-	}
-}

+ 0 - 38
src/utils/jsencrypt.js

@@ -1,38 +0,0 @@
-import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
-
-// 密钥对生成 http://web.chacuo.net/netrsakeypair
-
-const publicKey = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxPWP0HTBE9vEeM34Qx03U8oVm
-C6xIqWPRuI5t8J0zEDQudAgXKPjy8E0Q3cX800UNBTx2gUfRRNrONqALKDnJ1SE6
-qCUDeXOez8sa95GQ9d4BX7pSjZLrPfnCBTBtb5LGkY5zmlmtpG2AV9eJr+kQqhs/
-r0c4njwaDjVG4kF3ZQIDAQAB`
-
-const privateKey = `MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALE9Y/QdMET28R4z
-fhDHTdTyhWYLrEipY9G4jm3wnTMQNC50CBco+PLwTRDdxfzTRQ0FPHaBR9FE2s42
-oAsoOcnVITqoJQN5c57Pyxr3kZD13gFfulKNkus9+cIFMG1vksaRjnOaWa2kbYBX
-14mv6RCqGz+vRziePBoONUbiQXdlAgMBAAECgYBjSDdAXEVYrFdeiouYjHwdyAhP
-pERKo5BFvzMRhJIaM353cwnBJ3NkapVQ2Fn6iMIKTB+VZk+7eu1yTAkUluDfLowd
-REZS4ipOBY5UuNnjbXmSOoUQw6vRnox0X4x6S1vd4FBHgpVe1VkiE7Nz5U7Clyd5
-yw2P1lHwMyB/guAH4QJBAN3dGkMASj0jm23maHOfehp/zlACB8HpMKuV4z/bEg45
-nC9Hw5NloUHrXdzEXP1+S46MCH2THflxDVYtnZTRLO0CQQDMgp3Jrn7kkKtNceZF
-R08hLbVmfNlatgONgFJ5JnR+GTQ6o2gwM6SLyoBkfAIiEDpr6c6nBXTU09GOYxBk
-+h1ZAkB32pXxVBrG5JF20V3j+GcyIZEGz9H5A0xzpUlambIrVRv2vsH8wo5W2hue
-w8Woe629mBCOJgevVU9rGsFiP44RAkEApbTYAQjAjJakFpZJjKzg8vNEXoye2R9N
-9aOaL8v27A2kAjdRPm050IL+UW0hlVQs4i+KYE7NgX03+PVP3WHD0QJBANLo4PRw
-7Y+dLPAzuazsD3/5SYaSh+KSD/+tVbc6CFvLyfFUKp/a4PzzvGaLo/Ky/ffOY5k0
-hmavbHCKcg+r+hg=`
-
-
-// 加密
-export function encrypt(txt) {
-    const encryptor = new JSEncrypt()
-    encryptor.setPublicKey(publicKey) // 设置公钥
-    return encryptor.encrypt(txt) // 对数据进行加密
-}
-
-// 解密
-export function decrypt(txt) {
-    const encryptor = new JSEncrypt()
-    encryptor.setPrivateKey(privateKey) // 设置私钥
-    return encryptor.decrypt(txt) // 对数据进行解密
-}

+ 0 - 57
src/utils/jssdk.js

@@ -1,57 +0,0 @@
-import config from "@/config";
-
-//#ifdef H5
-const jweixin = import('weixin-js-sdk')
-//#endif
-export function configWeiXin(callback) {
-	const url = ""
-
-	//#ifdef H5
-	if (window.location.host) {
-		url = window.location.host;
-	}
-
-	if (uni.getStorageSync("serveUrl")) {
-		url = uni.getStorageSync("serveUrl");
-	}
-	//#endif
-
-	uni.request({
-		url: "https://qhome.usky.cn/USKYZHAF/sign.php",
-		header: {
-			'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
-		},
-		data: {
-			"url": url
-		},
-		method: 'GET',
-		success: (res) => {
-			// console.log('请求的签名的参数')
-			// console.log(res)
-
-			let apiList = [ // 可能需要用到的能力 需要啥就写啥。多写也没有坏处
-				'openLocation',
-				'getLocation',
-				'scanQRCode'
-			];
-			let info = {
-				debug: false, // 调试,发布的时候改为false
-				appId: res.data.appid,
-				nonceStr: res.data.nonceStr,
-				timestamp: parseInt(res.data.timestamp),
-				signature: res.data.sha_str,
-				jsApiList: apiList
-			};
-
-			jweixin.config(info);
-			jweixin.error(err => {
-				alert('config fail:', err);
-				return
-			});
-
-			jweixin.ready(res => {
-				if (callback) callback(jweixin);
-			});
-		}
-	});
-}

+ 2 - 22
unpackage/config/setting.js

@@ -9,8 +9,6 @@ filesToModify.forEach((file) => {
         let state = JSON.parse(cleanedData);// 解析JSON数据
 
         if (file == './src/manifest.json') {
-            var maps = state["app-plus"].distribute.sdkConfigs.maps
-            var geolocation = state["app-plus"].distribute.sdkConfigs.geolocation
             var icons = state["app-plus"].distribute.icons
             var push = state["app-plus"].distribute.sdkConfigs.push
 
@@ -22,15 +20,6 @@ filesToModify.forEach((file) => {
                 state.versionCode = 5
                 state.h5.title = '智能会议'
 
-                maps.amap.name = "amapBOujshtbA"
-                maps.amap.appkey_ios = "fb35d03fbb17cbf7a8743a522da3c7fc"
-                maps.amap.appkey_android = "ffc71dfd4e576596027f8f45a1b8fb2f"
-
-                geolocation.amap.name = "amapBOujshtbA"
-                geolocation.amap.appkey_ios = "fb35d03fbb17cbf7a8743a522da3c7fc"
-                geolocation.amap.appkey_android = "ffc71dfd4e576596027f8f45a1b8fb2f"
-                geolocation.tencent.apikey_ios = "EGOBZ-74ZET-ST7XS-VYICT-RBLHZ-KLFEX"
-                geolocation.tencent.apikey_android = "EGOBZ-74ZET-ST7XS-VYICT-RBLHZ-KLFEX"
                 icons.android = {
                     "hdpi": "unpackage/res/icons/72x72.png",
                     "xhdpi": "unpackage/res/icons/96x96.png",
@@ -70,19 +59,10 @@ filesToModify.forEach((file) => {
                 state.name = '智能门禁'
                 state.appid = '__UNI__8D6E9FD'
                 state.description = '智能门禁APP,是一款集成了现代信息技术和智能化管理功能的移动应用程序。'
-                state.versionName = "1.0.7"
-                state.versionCode = 7
+                state.versionName = "1.1.6"
+                state.versionCode = 16
                 state.h5.title = '智能门禁'
 
-                maps.amap.name = "amapBOujshtbA"
-                maps.amap.appkey_ios = "fb35d03fbb17cbf7a8743a522da3c7fc"
-                maps.amap.appkey_android = "ffc71dfd4e576596027f8f45a1b8fb2f"
-
-                geolocation.amap.name = "amapBOujshtbA"
-                geolocation.amap.appkey_ios = "fb35d03fbb17cbf7a8743a522da3c7fc"
-                geolocation.amap.appkey_android = "ffc71dfd4e576596027f8f45a1b8fb2f"
-                geolocation.tencent.apikey_ios = "EGOBZ-74ZET-ST7XS-VYICT-RBLHZ-KLFEX"
-                geolocation.tencent.apikey_android = "EGOBZ-74ZET-ST7XS-VYICT-RBLHZ-KLFEX"
                 icons.android = {
                     "hdpi": "unpackage/res/icons/72x72.png",
                     "xhdpi": "unpackage/res/icons/96x96.png",

+ 12 - 6
vite.config.js

@@ -25,7 +25,18 @@ export default defineConfig(({ mode, command }) => {
         },
       },
     },
-    plugins: [uni()],
+    plugins: [
+      uni({
+        vueOptions: {
+          template: {
+            compilerOptions: {
+              // 原生插件组件,勿当 Vue 组件解析(消除 Failed to resolve component 警告)
+              isCustomElement: (tag) => tag === "door-camera-x" || tag === "my-face-camera",
+            },
+          },
+        },
+      }),
+    ],
     resolve: {
       alias: {
         "@/": path.resolve(__dirname, "src"),
@@ -33,10 +44,5 @@ export default defineConfig(({ mode, command }) => {
         "@/utils": path.resolve(__dirname, "src/utils"),
       },
     },
-    configureWebpack: {
-      externals: {
-        AMap: "AMap",
-      },
-    },
   };
 });

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott