| | |
| | | "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", |
| | |
| | | } |
| | | }, |
| | | "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" |
| | |
| | | "@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": { |
| | |
| | | "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": { |
| | |
| | | } |
| | | } |
| | | }, |
| | | "@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", |
| | |
| | | }, |
| | | "dependencies": { |
| | | "@vant/icons": "^3.0.2", |
| | | "@zxing/browser": "^0.1.5", |
| | | "@zxing/library": "^0.21.3", |
| | | "axios": "^1.9.0", |
| | | "dayjs": "^1.11.13", |
| New file |
| | |
| | | @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"; |
| | | } |
| New file |
| | |
| | | <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> |
| | |
| | | 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') |
| | |
| | | </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" |
| | |
| | | <div class="my-button refresh" @click="handleRefresh">检查更新</div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 长识别二维码 --> |
| | | <QrScanner ref="QrScannerRef" @scan="onQrScan" /> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | 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); |
| | |
| | | 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({ |
| | |
| | | line-height: 16px; |
| | | color: #ffffff; |
| | | font-style: normal; |
| | | .iconfont { |
| | | margin-left: 2px; |
| | | font-size: 9px; |
| | | } |
| | | } |
| | | .row1-inp-box { |
| | | flex: 1; |
| | |
| | | // @ts-nocheck |
| | | import { defineConfig } from 'vite'; |
| | | import vue from '@vitejs/plugin-vue'; |
| | | import path from 'path'; |
| | |
| | | server: { |
| | | port: 3034, // 指定端口号为 3000 |
| | | strictPort: true, // 如果端口被占用,则抛出错误而不是尝试下一个可用端口 |
| | | host: true // 允许通过ip访问,要不然平板测试不了 |
| | | host: true, // 允许通过ip访问,要不然平板测试不了 |
| | | // https: true |
| | | }, |
| | | resolve: { |
| | | alias: { |