单应用项目,可以创建很多独立工具类页面 ,不用登录 初始化的页面
276209035343a0b009fb528df50b5f6fb83f469c..cad424a66fb99ef516534a562505b97c54ce2c8b
2025-09-01 zhangchen
Merge branch 'ID1766-添加推送登录功能' into test
cad424 对比 | 目录
2025-09-01 zhangchen
ID1766-添加设备长识别二维码
c1129f 对比 | 目录
5个文件已修改
5个文件已添加
310 ■■■■■ 已修改文件
package-lock.json 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/css/iconfont.css 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/font/iconfont.ttf 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/font/iconfont.woff 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/font/iconfont.woff2 补丁 | 查看 | 原始文档 | blame | 历史
src/components/QrScanner/index.vue 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mobile/bedsideAuxiliaryScreen/components/SettingDeviceDialog.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
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",
src/assets/css/iconfont.css
New file
@@ -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";
}
src/assets/font/iconfont.ttf
Binary files differ
src/assets/font/iconfont.woff
Binary files differ
src/assets/font/iconfont.woff2
Binary files differ
src/components/QrScanner/index.vue
New file
@@ -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>
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')
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;
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: {