Просмотр исходного кода

feat: 自定义扫码页面 相机预览+对焦框+扫描线动画

- 新scan.vue: camera全屏+四角对焦框+上下扫描线动画
- scancode事件自动识别QR 解析deviceId开门+选项
- home页扫码按钮改为navigateTo跳转扫码页

Co-Authored-By: Claude <noreply@anthropic.com>
skyline 2 дней назад
Родитель
Сommit
233be494a1

+ 6 - 0
haha-admin-mp/src/pages.json

@@ -448,6 +448,12 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/replenish/scan",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
     {
       "path": "pages/replenish/audit",
       "style": {

+ 2 - 50
haha-admin-mp/src/pages/replenish/home.vue

@@ -63,7 +63,7 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import { onShow } from '@dcloudio/uni-app';
-import { getDeviceList, openDeviceDoor } from '@/api/replenish';
+import { getDeviceList } from '@/api/replenish';
 import { clearAuth } from '@/utils/auth';
 
 const statusBarHeight = ref(0);
@@ -88,55 +88,7 @@ const goToAudit = (device: any) => {
 };
 
 const handleScan = () => {
-  uni.scanCode({
-    onlyFromCamera: true,
-    scanType: ['qrCode', 'barCode'],
-    success: async (res: any) => {
-      // 与 haha-mp 一致的 deviceId 提取逻辑
-      let deviceId = '';
-      try {
-        const urlPattern = /\/([A-Z0-9]+)(\?|$)/;
-        const match = res.result.match(urlPattern);
-        if (match && match[1]) {
-          deviceId = match[1];
-        } else {
-          try {
-            const qrData = JSON.parse(res.result);
-            if (qrData.deviceId) deviceId = qrData.deviceId;
-          } catch (e) {
-            deviceId = res.result;
-          }
-        }
-        if (!deviceId) throw new Error('无法解析设备ID');
-      } catch (error) {
-        uni.showToast({ title: '二维码格式错误', icon: 'none' });
-        return;
-      }
-
-      // 先开门
-      uni.showLoading({ title: '开门中...', mask: true });
-      try {
-        await openDeviceDoor(deviceId);
-      } catch {
-        uni.hideLoading();
-        uni.showToast({ title: '开门失败,请重试', icon: 'none' });
-        return;
-      }
-      uni.hideLoading();
-
-      uni.showActionSheet({
-        itemList: ['设备补货', '库存盘点'],
-        success: (sheetRes) => {
-          if (sheetRes.tapIndex === 0) {
-            uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${deviceId}` });
-          } else if (sheetRes.tapIndex === 1) {
-            uni.navigateTo({ url: `/pages/replenish/audit?deviceId=${deviceId}` });
-          }
-        }
-      });
-    },
-    fail: () => { /* 用户取消扫码 */ }
-  });
+  uni.navigateTo({ url: '/pages/replenish/scan' });
 };
 
 const handleLogout = () => {

+ 146 - 0
haha-admin-mp/src/pages/replenish/scan.vue

@@ -0,0 +1,146 @@
+<template>
+  <view class="page">
+    <camera
+      class="camera-view"
+      device-position="back"
+      flash="off"
+      @scancode="onScanCode"
+      @error="onCameraError"
+    />
+
+    <!-- 对焦框覆盖层 -->
+    <view class="scan-overlay">
+      <view class="scan-mask top"></view>
+      <view class="scan-mid">
+        <view class="scan-mask left"></view>
+        <view class="scan-frame">
+          <!-- 四角 -->
+          <view class="corner tl"></view>
+          <view class="corner tr"></view>
+          <view class="corner bl"></view>
+          <view class="corner br"></view>
+          <!-- 扫描线 -->
+          <view class="scan-line"></view>
+        </view>
+        <view class="scan-mask right"></view>
+      </view>
+      <view class="scan-mask bottom">
+        <text class="scan-hint">将二维码放入框内,即可自动扫描</text>
+      </view>
+    </view>
+
+    <!-- 底部关闭 -->
+    <view class="close-btn" @click="handleClose">
+      <text>关闭</text>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { openDeviceDoor } from '@/api/replenish';
+
+let scanned = false;
+
+function onScanCode(e: any) {
+  if (scanned) return;
+  scanned = true;
+
+  const result = (e.detail?.result || '').trim();
+  if (!result) {
+    uni.showToast({ title: '未识别到二维码', icon: 'none' });
+    scanned = false;
+    return;
+  }
+
+  // 与 haha-mp 一致的 deviceId 提取逻辑
+  let deviceId = '';
+  try {
+    const urlPattern = /\/([A-Z0-9]+)(\?|$)/;
+    const match = result.match(urlPattern);
+    if (match && match[1]) {
+      deviceId = match[1];
+    } else {
+      try {
+        const qrData = JSON.parse(result);
+        if (qrData.deviceId) deviceId = qrData.deviceId;
+      } catch (e) {
+        deviceId = result;
+      }
+    }
+    if (!deviceId) throw new Error('无法解析设备ID');
+  } catch {
+    uni.showToast({ title: '二维码格式错误', icon: 'none' });
+    scanned = false;
+    return;
+  }
+
+  // 开门
+  uni.showLoading({ title: '开门中...', mask: true });
+  openDeviceDoor(deviceId).then(() => {
+    uni.hideLoading();
+    uni.showActionSheet({
+      itemList: ['设备补货', '库存盘点'],
+      success: (sheetRes: any) => {
+        if (sheetRes.tapIndex === 0) {
+          uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${deviceId}` });
+        } else if (sheetRes.tapIndex === 1) {
+          uni.navigateTo({ url: `/pages/replenish/audit?deviceId=${deviceId}` });
+        }
+      },
+      fail: () => { scanned = false; }
+    });
+  }).catch(() => {
+    uni.hideLoading();
+    uni.showToast({ title: '开门失败,请重试', icon: 'none' });
+    scanned = false;
+  });
+}
+
+function onCameraError(e: any) {
+  console.error('相机错误:', e.detail);
+  uni.showToast({ title: '相机启动失败,请授权后重试', icon: 'none' });
+  setTimeout(() => uni.navigateBack(), 1000);
+}
+
+function handleClose() {
+  uni.navigateBack();
+}
+</script>
+
+<style lang="scss" scoped>
+.page { width: 100vw; height: 100vh; background: #000; position: relative; }
+
+.camera-view { width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
+
+.scan-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 10; display: flex; flex-direction: column; }
+
+.scan-mask { background: rgba(0, 0, 0, 0.5); }
+.scan-mask.top, .scan-mask.bottom { flex: 1; }
+.scan-mask.left, .scan-mask.right { flex: 1; }
+
+.scan-mid { display: flex; height: 500rpx; }
+
+.scan-frame { width: 500rpx; height: 500rpx; position: relative; }
+
+.corner { position: absolute; width: 40rpx; height: 40rpx; border-color: $primary-color; border-style: solid; z-index: 1;
+  &.tl { top: 0; left: 0; border-width: 6rpx 0 0 6rpx; border-radius: 8rpx 0 0 0; }
+  &.tr { top: 0; right: 0; border-width: 6rpx 6rpx 0 0; border-radius: 0 8rpx 0 0; }
+  &.bl { bottom: 0; left: 0; border-width: 0 0 6rpx 6rpx; border-radius: 0 0 0 8rpx; }
+  &.br { bottom: 0; right: 0; border-width: 0 6rpx 6rpx 0; border-radius: 0 0 8rpx 0; }
+}
+
+.scan-line { position: absolute; left: 20rpx; right: 20rpx; height: 3rpx; background: $primary-color; box-shadow: 0 0 10rpx rgba(255,193,7,0.6); animation: scanAnim 2s ease-in-out infinite; }
+
+@keyframes scanAnim {
+  0%, 100% { top: 20rpx; }
+  50% { top: calc(100% - 20rpx); }
+}
+
+.scan-mask.bottom { display: flex; align-items: center; justify-content: center; padding-bottom: 120rpx; }
+.scan-hint { font-size: 28rpx; color: rgba(255,255,255,0.8); text-align: center; }
+
+.close-btn { position: absolute; bottom: calc(60rpx + env(safe-area-inset-bottom)); left: 50%; transform: translateX(-50%); z-index: 20; padding: 20rpx 64rpx; border-radius: 48rpx; background: rgba(255,255,255,0.15);
+  text { font-size: 30rpx; color: #fff; }
+  &:active { background: rgba(255,255,255,0.25); }
+}
+</style>