Browse Source

web端扫码开发

jack.zhou 3 weeks ago
parent
commit
0dee7d298a

+ 8 - 8
.env.development

@@ -10,19 +10,19 @@
 # 本地环境
 # 本地环境
 ENV = development
 ENV = development
 # 本地环境接口地址
 # 本地环境接口地址
-VITE_API_URL = http://192.168.0.216:9954/
+VITE_API_URL = http://192.168.0.216:9947/
 VITE_API_WECHAT = /wechat/
 VITE_API_WECHAT = /wechat/
 VITE_TENANT = default
 VITE_TENANT = default
 
 
 # 微服务地址
 # 微服务地址
-VITE_ADMIN = dashoo.labsop.admin-34000
-VITE_WORKFLOW = dashoo.labsop.workflow-34000
-VITE_FINANCE = dashoo.labsop.finance-34000
-VITE_SCI = dashoo.labsop.scientific-34000
-VITE_INSTR_ADMIN = dashoo.labsop.apparatus-34000
-VITE_LEARNING = dashoo.labsop.learning-34000
+VITE_ADMIN = dashoo.labsop.admin-62000
+VITE_WORKFLOW = dashoo.labsop.workflow-62000
+VITE_FINANCE = dashoo.labsop.finance-62000
+VITE_SCI = dashoo.labsop.scientific-62000
+VITE_INSTR_ADMIN = dashoo.labsop.apparatus-62000
+VITE_LEARNING = dashoo.labsop.learning-62000
 VITE_PLATFORM_API = dashoo.labsop.platform-3400
 VITE_PLATFORM_API = dashoo.labsop.platform-3400
-VITE_LABORATORY: dashoo.labsop.laboratory-34000
+VITE_LABORATORY: dashoo.labsop.laboratory-62000
 
 
 #公共配置
 #公共配置
 VITE_UPLOAD = http://192.168.0.218:9933/weedfs/upload
 VITE_UPLOAD = http://192.168.0.218:9933/weedfs/upload

+ 8 - 8
.env.production

@@ -16,14 +16,14 @@ VITE_API_WECHAT = /wechat/
 VITE_TENANT = default
 VITE_TENANT = default
 
 
 # 微服务地址
 # 微服务地址
-VITE_ADMIN = dashoo.labsop.admin-59000
-VITE_FINANCE = dashoo.labsop.finance-59000
-VITE_SCI = dashoo.labsop.scientific-59000
-VITE_WORKFLOW = dashoo.labsop.workflow-59000
-VITE_INSTR_ADMIN = dashoo.labsop.apparatus-59000
-VITE_LEARNING = dashoo.labsop.learning-59000
-VITE_PLATFORM_API = dashoo.labsop.platform-59000
-VITE_LABORATORY: dashoo.labsop.laboratory-59000
+VITE_ADMIN = dashoo.labsop.admin-62000
+VITE_FINANCE = dashoo.labsop.finance-62000
+VITE_SCI = dashoo.labsop.scientific-62000
+VITE_WORKFLOW = dashoo.labsop.workflow-62000
+VITE_INSTR_ADMIN = dashoo.labsop.apparatus-62000
+VITE_LEARNING = dashoo.labsop.learning-62000
+VITE_PLATFORM_API = dashoo.labsop.platform-62000
+VITE_LABORATORY: dashoo.labsop.laboratory-62000
 
 
 
 
 #公共配置
 #公共配置

+ 1 - 0
components.d.ts

@@ -7,6 +7,7 @@ export {}
 
 
 declare module 'vue' {
 declare module 'vue' {
   export interface GlobalComponents {
   export interface GlobalComponents {
+    BrowserCodeScanner: typeof import('./src/components/BrowserCodeScanner.vue')['default']
     CustomForm: typeof import('./src/components/CustomForm.vue')['default']
     CustomForm: typeof import('./src/components/CustomForm.vue')['default']
     FlowTable: typeof import('./src/components/FlowTable.vue')['default']
     FlowTable: typeof import('./src/components/FlowTable.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterLink: typeof import('vue-router')['RouterLink']

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "@fullcalendar/timegrid": "^6.1.17",
     "@fullcalendar/timegrid": "^6.1.17",
     "@fullcalendar/vue3": "^6.1.17",
     "@fullcalendar/vue3": "^6.1.17",
     "@vitejs/plugin-basic-ssl": "^2.0.0",
     "@vitejs/plugin-basic-ssl": "^2.0.0",
+    "@zxing/browser": "^0.1.5",
     "await-to-js": "^3.0.0",
     "await-to-js": "^3.0.0",
     "axios": "^1.8.2",
     "axios": "^1.8.2",
     "cropperjs": "1.5.13",
     "cropperjs": "1.5.13",

+ 33 - 0
pnpm-lock.yaml

@@ -26,6 +26,9 @@ dependencies:
   '@vitejs/plugin-basic-ssl':
   '@vitejs/plugin-basic-ssl':
     specifier: ^2.0.0
     specifier: ^2.0.0
     version: 2.0.0(vite@4.5.14)
     version: 2.0.0(vite@4.5.14)
+  '@zxing/browser':
+    specifier: ^0.1.5
+    version: 0.1.5(@zxing/library@0.21.3)
   await-to-js:
   await-to-js:
     specifier: ^3.0.0
     specifier: ^3.0.0
     version: 3.0.0
     version: 3.0.0
@@ -683,6 +686,31 @@ packages:
       - vue
       - vue
     dev: false
     dev: false
 
 
+  /@zxing/browser@0.1.5(@zxing/library@0.21.3):
+    resolution: {integrity: sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==}
+    peerDependencies:
+      '@zxing/library': ^0.21.0
+    dependencies:
+      '@zxing/library': 0.21.3
+    optionalDependencies:
+      '@zxing/text-encoding': 0.9.0
+    dev: false
+
+  /@zxing/library@0.21.3:
+    resolution: {integrity: sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==}
+    engines: {node: '>= 10.4.0'}
+    dependencies:
+      ts-custom-error: 3.3.1
+    optionalDependencies:
+      '@zxing/text-encoding': 0.9.0
+    dev: false
+
+  /@zxing/text-encoding@0.9.0:
+    resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
+    requiresBuild: true
+    dev: false
+    optional: true
+
   /acorn@8.15.0:
   /acorn@8.15.0:
     resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
     resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
     engines: {node: '>=0.4.0'}
     engines: {node: '>=0.4.0'}
@@ -1322,6 +1350,11 @@ packages:
     dependencies:
     dependencies:
       is-number: 7.0.0
       is-number: 7.0.0
 
 
+  /ts-custom-error@3.3.1:
+    resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==}
+    engines: {node: '>=14.0.0'}
+    dev: false
+
   /typescript@4.6.4:
   /typescript@4.6.4:
     resolution: {integrity: sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==}
     resolution: {integrity: sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==}
     engines: {node: '>=4.2.0'}
     engines: {node: '>=4.2.0'}

+ 219 - 0
src/components/BrowserCodeScanner.vue

@@ -0,0 +1,219 @@
+<template>
+  <van-popup
+    v-model:show="visible"
+    class="browser-scanner-popup"
+    position="bottom"
+    closeable
+    @opened="startScanner"
+    @closed="stopScanner"
+  >
+    <div class="scanner-header">扫码上机</div>
+    <div class="scanner-content">
+      <div v-if="errorMessage" class="scanner-error">
+        <van-icon name="warning-o" size="36" />
+        <div>{{ errorMessage }}</div>
+        <van-button type="primary" size="small" @click="startScanner">重试</van-button>
+      </div>
+      <div v-else class="scanner-preview">
+        <video ref="videoRef" class="scanner-video" muted playsinline></video>
+        <div class="scanner-frame" aria-hidden="true"></div>
+        <div class="scanner-status">正在识别二维码</div>
+      </div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup lang="ts">
+import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
+import { BrowserMultiFormatReader, type IScannerControls } from '@zxing/browser'
+
+const props = defineProps<{
+  show: boolean
+}>()
+
+const emit = defineEmits<{
+  (event: 'update:show', value: boolean): void
+  (event: 'scan', value: string): void
+}>()
+
+const visible = computed({
+  get: () => props.show,
+  set: (value: boolean) => emit('update:show', value)
+})
+
+const videoRef = ref<HTMLVideoElement>()
+const errorMessage = ref('')
+let controls: IScannerControls | undefined
+let scanned = false
+let scannerSession = 0
+
+const stopScanner = () => {
+  scannerSession += 1
+  controls?.stop()
+  controls = undefined
+
+  const stream = videoRef.value?.srcObject
+  if (stream instanceof MediaStream) {
+    stream.getTracks().forEach((track) => track.stop())
+    videoRef.value!.srcObject = null
+  }
+}
+
+const getCameraErrorMessage = (error: unknown) => {
+  if (!window.isSecureContext) {
+    return '浏览器摄像头需要通过 HTTPS 访问'
+  }
+
+  const errorName = error instanceof DOMException ? error.name : ''
+  if (errorName === 'NotAllowedError' || errorName === 'SecurityError') {
+    return '摄像头权限未开启,请在浏览器设置中允许访问'
+  }
+  if (errorName === 'NotFoundError' || errorName === 'OverconstrainedError') {
+    return '未找到可用的摄像头'
+  }
+  if (errorName === 'NotReadableError' || errorName === 'AbortError') {
+    return '摄像头正被其他应用占用'
+  }
+  return '摄像头启动失败,请重试'
+}
+
+const startScanner = async () => {
+  stopScanner()
+  const currentSession = scannerSession
+  errorMessage.value = ''
+  scanned = false
+
+  await nextTick()
+
+  if (currentSession !== scannerSession || !visible.value) return
+
+  if (!window.isSecureContext) {
+    errorMessage.value = getCameraErrorMessage(undefined)
+    return
+  }
+  if (!navigator.mediaDevices?.getUserMedia) {
+    errorMessage.value = '当前浏览器不支持摄像头扫码'
+    return
+  }
+  if (!videoRef.value) {
+    errorMessage.value = '扫码视图初始化失败,请重试'
+    return
+  }
+
+  const reader = new BrowserMultiFormatReader()
+  try {
+    const scannerControls = await reader.decodeFromConstraints(
+      {
+        audio: false,
+        video: {
+          facingMode: { ideal: 'environment' },
+          width: { ideal: 1280 },
+          height: { ideal: 720 }
+        }
+      },
+      videoRef.value,
+      (result, _error, scannerControls) => {
+        if (currentSession !== scannerSession || !visible.value) {
+          scannerControls.stop()
+          return
+        }
+        if (!result || scanned) return
+
+        scanned = true
+        scannerControls.stop()
+        emit('scan', result.getText())
+        visible.value = false
+      }
+    )
+    if (currentSession !== scannerSession || !visible.value) {
+      scannerControls.stop()
+      return
+    }
+    controls = scannerControls
+  } catch (error) {
+    stopScanner()
+    errorMessage.value = getCameraErrorMessage(error)
+  }
+}
+
+onBeforeUnmount(stopScanner)
+</script>
+
+<style lang="scss" scoped>
+.browser-scanner-popup {
+  width: 100vw;
+  height: 100vh;
+  height: 100dvh;
+  background: #101214;
+  color: #fff;
+}
+
+:deep(.browser-scanner-popup .van-popup__close-icon) {
+  top: calc(16px + env(safe-area-inset-top));
+}
+
+.scanner-header {
+  height: calc(52px + env(safe-area-inset-top));
+  padding: env(safe-area-inset-top) 52px 0;
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 17px;
+  font-weight: 600;
+}
+
+.scanner-content {
+  height: calc(100% - 52px - env(safe-area-inset-top));
+  padding-bottom: env(safe-area-inset-bottom);
+  box-sizing: border-box;
+}
+
+.scanner-preview {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  background: #000;
+}
+
+.scanner-video {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.scanner-frame {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  width: min(64vw, 280px);
+  aspect-ratio: 1;
+  transform: translate(-50%, -56%);
+  border: 2px solid rgba(255, 255, 255, 0.9);
+  border-radius: 4px;
+  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.38);
+}
+
+.scanner-status {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 28px;
+  text-align: center;
+  font-size: 14px;
+}
+
+.scanner-error {
+  height: 100%;
+  padding: 24px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 18px;
+  text-align: center;
+  font-size: 14px;
+  line-height: 1.6;
+}
+</style>

+ 21 - 13
src/view/instr/appointList/soonGeton/index.vue

@@ -76,6 +76,7 @@
         </div>
         </div>
       </div>
       </div>
     </van-list>
     </van-list>
+    <browser-code-scanner v-model:show="browserScannerVisible" @scan="handleBrowserScan" />
     <!-- </van-pull-refresh> -->
     <!-- </van-pull-refresh> -->
     <!-- <blue-tooth ref="bluetoothRef"></blue-tooth> -->
     <!-- <blue-tooth ref="bluetoothRef"></blue-tooth> -->
   </div>
   </div>
@@ -93,11 +94,13 @@ import instAppointApi from '/@/api/instr/instAppoint'
 import to from 'await-to-js'
 import to from 'await-to-js'
 import { showConfirmDialog, showNotify } from 'vant'
 import { showConfirmDialog, showNotify } from 'vant'
 import { scanCodeWxUrl } from '/@/constants/pageConstants'
 import { scanCodeWxUrl } from '/@/constants/pageConstants'
+import BrowserCodeScanner from '/@/components/BrowserCodeScanner.vue'
 export default {
 export default {
-  // components: { BlueTooth },
+  components: { BrowserCodeScanner },
   data() {
   data() {
     return {
     return {
       loading: false,
       loading: false,
+      browserScannerVisible: false,
       curAppointInfo: {},
       curAppointInfo: {},
       appointList: [],
       appointList: [],
       queryForm: {
       queryForm: {
@@ -167,21 +170,26 @@ export default {
             this.handleGetOnByAppointId(this.curAppointInfo.id)
             this.handleGetOnByAppointId(this.curAppointInfo.id)
           })
           })
           .catch(() => { })
           .catch(() => { })
-      } else if (row.controlMode == '20' || row.controlMode == '50' || row.controlMode == '70') {
-        // wifi控制 和 电脑控制
-        const that = this
-        // 调起条码扫描
-        const res = await useUserInfo().scanCode()
-        that.handleDeCode(res, row, that.handleCodeInfo)
-      } else if (row.controlMode == '30' || row.controlMode == '60') {
-        // 蓝牙
-        // this.$refs.bluetoothRef.initBlue('open', row)
-        // await useUserInfo().scanCode(0)
-        const that = this
+      } else if (!this.isWechatBrowser()) {
+        this.browserScannerVisible = true
+      } else {
         const res = await useUserInfo().scanCode()
         const res = await useUserInfo().scanCode()
-        that.handleDeCode(res, row, that.handleBluetoothDeCode)
+        this.handleScannedCode(res, row)
       }
       }
     },
     },
+    isWechatBrowser() {
+      return /micromessenger/i.test(navigator.userAgent)
+    },
+    handleBrowserScan(content) {
+      if (!this.curAppointInfo?.controlMode) return
+      this.handleScannedCode(content, this.curAppointInfo)
+    },
+    handleScannedCode(content, row) {
+      const decodeHandler = ['30', '60'].includes(row.controlMode)
+        ? this.handleBluetoothDeCode
+        : this.handleCodeInfo
+      this.handleDeCode(content, row, decodeHandler)
+    },
     /**
     /**
      *  解码
      *  解码
      *  code 二维码内容
      *  code 二维码内容