From cad424a66fb99ef516534a562505b97c54ce2c8b Mon Sep 17 00:00:00 2001
From: zhangchen <1652267879@qq.com>
Date: 星期一, 01 九月 2025 15:53:45 +0800
Subject: [PATCH] Merge branch 'ID1766-添加推送登录功能' into test

---
 src/components/QrScanner/index.vue                                         |  222 +++++++++++++++++++++++++++++++++++++
 vite.config.ts                                                             |    4 
 src/views/mobile/bedsideAuxiliaryScreen/components/SettingDeviceDialog.vue |   22 +++
 package-lock.json                                                          |   33 ++++-
 src/assets/font/iconfont.woff2                                             |    0 
 package.json                                                               |    1 
 src/assets/css/iconfont.css                                                |   22 +++
 src/assets/font/iconfont.ttf                                               |    0 
 src/assets/font/iconfont.woff                                              |    0 
 src/main.ts                                                                |    6 
 10 files changed, 300 insertions(+), 10 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 0ad23e7..f103eff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@vant/icons": "^3.0.2",
+        "@zxing/browser": "^0.1.5",
         "@zxing/library": "^0.21.3",
         "axios": "^1.9.0",
         "dayjs": "^1.11.13",
@@ -103,9 +104,9 @@
       }
     },
     "node_modules/@element-plus/icons-vue": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
-      "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
       "license": "MIT",
       "peerDependencies": {
         "vue": "^3.2.0"
@@ -1213,6 +1214,18 @@
         "@vue/composition-api": {
           "optional": true
         }
+      }
+    },
+    "node_modules/@zxing/browser": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.5.tgz",
+      "integrity": "sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==",
+      "license": "MIT",
+      "optionalDependencies": {
+        "@zxing/text-encoding": "^0.9.0"
+      },
+      "peerDependencies": {
+        "@zxing/library": "^0.21.0"
       }
     },
     "node_modules/@zxing/library": {
@@ -5375,9 +5388,9 @@
       "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA=="
     },
     "@element-plus/icons-vue": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
-      "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
       "requires": {}
     },
     "@esbuild/aix-ppc64": {
@@ -6034,6 +6047,14 @@
         }
       }
     },
+    "@zxing/browser": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.5.tgz",
+      "integrity": "sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==",
+      "requires": {
+        "@zxing/text-encoding": "^0.9.0"
+      }
+    },
     "@zxing/library": {
       "version": "0.21.3",
       "resolved": "https://registry.npmmirror.com/@zxing/library/-/library-0.21.3.tgz",
diff --git a/package.json b/package.json
index 1c21545..7bf01ac 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
   },
   "dependencies": {
     "@vant/icons": "^3.0.2",
+    "@zxing/browser": "^0.1.5",
     "@zxing/library": "^0.21.3",
     "axios": "^1.9.0",
     "dayjs": "^1.11.13",
diff --git a/src/assets/css/iconfont.css b/src/assets/css/iconfont.css
new file mode 100644
index 0000000..ada5478
--- /dev/null
+++ b/src/assets/css/iconfont.css
@@ -0,0 +1,22 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 5011061 */
+  src: url('//at.alicdn.com/t/c/font_5011061_crebeujq91a.woff2?t=1756705233110') format('woff2'),
+       url('//at.alicdn.com/t/c/font_5011061_crebeujq91a.woff?t=1756705233110') format('woff'),
+       url('//at.alicdn.com/t/c/font_5011061_crebeujq91a.ttf?t=1756705233110') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-dituweizhixinxi_chahao:before {
+  content: "\e600";
+}
+
+.icon-saoma:before {
+  content: "\e749";
+}
diff --git a/src/assets/font/iconfont.ttf b/src/assets/font/iconfont.ttf
new file mode 100644
index 0000000..0ed3ab5
--- /dev/null
+++ b/src/assets/font/iconfont.ttf
Binary files differ
diff --git a/src/assets/font/iconfont.woff b/src/assets/font/iconfont.woff
new file mode 100644
index 0000000..71bb5d9
--- /dev/null
+++ b/src/assets/font/iconfont.woff
Binary files differ
diff --git a/src/assets/font/iconfont.woff2 b/src/assets/font/iconfont.woff2
new file mode 100644
index 0000000..66aa294
--- /dev/null
+++ b/src/assets/font/iconfont.woff2
Binary files differ
diff --git a/src/components/QrScanner/index.vue b/src/components/QrScanner/index.vue
new file mode 100644
index 0000000..a3f7882
--- /dev/null
+++ b/src/components/QrScanner/index.vue
@@ -0,0 +1,222 @@
+<template>
+  <div v-if="show" class="qr-scanner">
+    <div class="qr-header">
+      <i class="iconfont icon-dituweizhixinxi_chahao" @click="close"></i>
+      <div class="title">扫一扫</div>
+    </div>
+    <div class="qr-video-box">
+      <video ref="videoRef" autoplay playsinline muted class="qr-video"></video>
+      <!-- 四个角标 -->
+      <span class="corner tl"></span>
+      <span class="corner tr"></span>
+      <span class="corner bl"></span>
+      <span class="corner br"></span>
+
+      <!-- 扫描线 -->
+      <span class="scan-line"></span>
+    </div>
+    <!-- <p v-if="code">识别结果:{{ code }}</p>
+    <button @click="startScan">开始扫描</button>
+    <button @click="stopScan" v-if="isScanning">停止扫描</button> -->
+  </div>
+</template>
+
+<script lang="ts">
+import { ref, onBeforeUnmount } from "vue";
+import { BrowserQRCodeReader, IScannerControls } from "@zxing/browser";
+import { ElMessage } from "element-plus";
+
+export default {
+  name: "QrScanner",
+  emits: ["scan"],
+  setup(props, { emit }) {
+    const show = ref(false);
+    const videoRef = ref<HTMLVideoElement | null>(null);
+    const code = ref("");
+    const isScanning = ref(false);
+    let codeReader: BrowserQRCodeReader | null = null;
+    let controls: IScannerControls | null = null;
+
+    const startScan = async () => {
+      if (isScanning.value) return;
+      isScanning.value = true;
+
+      codeReader = new BrowserQRCodeReader();
+
+      try {
+        const devices = await BrowserQRCodeReader.listVideoInputDevices();
+        if (devices.length === 0) {
+          console.error("扫码失败:", "未找到摄像头");
+          ElMessage({ message: "未找到摄像头", type: "warning" });
+          return;
+        }
+
+        // 选择后置摄像头(mobile label 中常带 'back' 或 'rear')
+        let rearCamera = devices.find((d) => /back|rear|后/i.test(d.label));
+
+        const selectedDeviceId = rearCamera
+          ? rearCamera.deviceId
+          : devices[0].deviceId;
+
+        controls = await codeReader.decodeFromVideoDevice(
+          selectedDeviceId,
+          videoRef.value!,
+          (res) => {
+            if (res) {
+              code.value = res.getText();
+              console.log("code: ", code.value);
+              emit("scan", { code: code.value, success: true });
+              // stopScan(); // 识别成功后停止扫描
+              close();
+            }
+          }
+        );
+      } catch (err) {
+        console.error("扫码失败:", err);
+        ElMessage({
+          message: "无法访问摄像头,请检查权限或使用 HTTPS",
+          type: "warning",
+        });
+      }
+    };
+
+    const stopScan = () => {
+      isScanning.value = false;
+      controls?.stop();
+      controls = null;
+    };
+
+    const close = () => {
+      stopScan();
+      show.value = false;
+      emit("scan", { code: "", success: false });
+    };
+
+    const open = () => {
+      show.value = true;
+      startScan();
+    };
+
+    onBeforeUnmount(() => {
+      stopScan();
+    });
+
+    return {
+      show,
+      videoRef,
+      code,
+      isScanning,
+      startScan,
+      stopScan,
+      close,
+      open,
+    };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.qr-scanner {
+  position: fixed;
+  width: 100vw;
+  height: 100vh;
+  top: 0;
+  left: 0;
+  z-index: 9999;
+  background: rgba(0, 0, 0, 0.8);
+  .qr-header {
+    position: relative;
+    padding: 8px 5px;
+    color: #fff;
+    .title {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translateX(-50%) translateY(-50%);
+      font-size: 6px;
+      font-weight: bold;
+    }
+    .iconfont {
+      position: absolute;
+      left: 5px;
+      top: 50%;
+      transform: translateY(-50%);
+      font-size: 9px;
+    }
+  }
+  .qr-video-box {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    width: 130px;
+    height: 130px;
+    background: rgba(0, 0, 0, 0);
+    .qr-video {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+    /* 角标通用样式 */
+    .corner {
+      position: absolute;
+      width: 26px; // 角标边长
+      height: 26px;
+      border: 3px solid #409eff; // 角标颜色/粗细
+    }
+    .corner.tl {
+      top: 0;
+      left: 0;
+      border-right: none;
+      border-bottom: none;
+    }
+    .corner.tr {
+      top: 0;
+      right: 0;
+      border-left: none;
+      border-bottom: none;
+    }
+    .corner.bl {
+      bottom: 0;
+      left: 0;
+      border-right: none;
+      border-top: none;
+    }
+    .corner.br {
+      bottom: 0;
+      right: 0;
+      border-left: none;
+      border-top: none;
+    }
+
+    /* 扫描线 */
+    .scan-line {
+      position: absolute;
+      left: 0;
+      right: 0;
+      height: 2px;
+      top: 0;
+      background: linear-gradient(
+        90deg,
+        rgba(0, 0, 0, 0) 0%,
+        rgba(64, 158, 255, 0.7) 15%,
+        rgba(64, 158, 255, 0.95) 50%,
+        rgba(64, 158, 255, 0.7) 85%,
+        rgba(0, 0, 0, 0) 100%
+      );
+      box-shadow: 0 0 8px rgba(64, 158, 255, 0.8),
+        0 0 16px rgba(64, 158, 255, 0.5);
+      animation: scan-move 2.2s linear infinite alternate;
+      will-change: transform;
+    }
+  }
+}
+@keyframes scan-move {
+  0% {
+    transform: translateY(0);
+  }
+  100% {
+    transform: translateY(128px);
+  }
+}
+</style>
diff --git a/src/main.ts b/src/main.ts
index 9147bbe..267b11f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -9,11 +9,13 @@
 import App from './App.vue'
 import VConsole from 'vconsole'
 import { createPinia } from 'pinia'
-
+import '@/assets/css/iconfont.css'
 if (import.meta.env.VITE_ENV === 'development') {
 // 如果需要在手机平板上打开控制台,安装一个这个
     const vConsole = new VConsole()
 }
 const pinia = createPinia()
 
-createApp(App).use(router).use(pinia).use(ElementPlus).use(Vant).mount('#app')
+const app = createApp(App)
+
+app.use(router).use(pinia).use(ElementPlus).use(Vant).mount('#app')
diff --git a/src/views/mobile/bedsideAuxiliaryScreen/components/SettingDeviceDialog.vue b/src/views/mobile/bedsideAuxiliaryScreen/components/SettingDeviceDialog.vue
index 9394352..e2817cc 100644
--- a/src/views/mobile/bedsideAuxiliaryScreen/components/SettingDeviceDialog.vue
+++ b/src/views/mobile/bedsideAuxiliaryScreen/components/SettingDeviceDialog.vue
@@ -23,7 +23,7 @@
       </template>
       <div class="setting-device-dialog-content">
         <div class="content-row1">
-          <div class="row1-label">设备编号</div>
+          <div class="row1-label" @click="openQrScanner">设备编号<i class="iconfont icon-saoma"></i></div>
           <div class="row1-inp-box">
             <input
               v-model="devcieCode"
@@ -57,6 +57,9 @@
         <div class="my-button refresh" @click="handleRefresh">检查更新</div>
       </template>
     </el-dialog>
+    <!-- 长识别二维码 -->
+    <QrScanner ref="QrScannerRef" @scan="onQrScan" />
+
   </div>
 </template>
 
@@ -72,8 +75,11 @@
 import closeImg from "@/img/close.png";
 import uploadImg from "@/img/upload.png";
 import { useBedsideAuxiliaryScreenStore } from "@/store/bedsideAuxiliaryScreen";
+import QrScanner from "@/components/QrScanner/index.vue";
 
 const bedsideAuxiliaryScreenStore = useBedsideAuxiliaryScreenStore();
+
+const QrScannerRef = ref(null);
 
 const isShow = ref(false);
 const isUploading = ref(false);
@@ -146,6 +152,16 @@
 const handleRefresh = () => {
   window.location.reload();
   ElMessage.success('已更新至最新版本')
+};
+
+const openQrScanner = () => {
+  QrScannerRef.value?.open();
+};
+
+const onQrScan = ({ success, code}) => {
+  if (!success) return;
+  devcieCode.value = code;
+  ElMessage.success("识别成功");
 };
 
 defineExpose({
@@ -227,6 +243,10 @@
         line-height: 16px;
         color: #ffffff;
         font-style: normal;
+        .iconfont {
+          margin-left: 2px;
+          font-size: 9px;
+        }
       }
       .row1-inp-box {
         flex: 1;
diff --git a/vite.config.ts b/vite.config.ts
index 97f5764..c8b97de 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,3 +1,4 @@
+// @ts-nocheck
 import { defineConfig } from 'vite';
 import vue from '@vitejs/plugin-vue';
 import path from 'path';
@@ -10,7 +11,8 @@
   server: {
     port: 3034, // 指定端口号为 3000
     strictPort: true, // 如果端口被占用,则抛出错误而不是尝试下一个可用端口
-    host: true // 允许通过ip访问,要不然平板测试不了
+    host: true, // 允许通过ip访问,要不然平板测试不了
+    // https: true
   },
   resolve: {
     alias: {

--
Gitblit v1.8.0