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 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 222 insertions(+), 0 deletions(-)

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>

--
Gitblit v1.8.0