scan.vue 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <template>
  2. <view class="page">
  3. <camera
  4. class="camera-view"
  5. device-position="back"
  6. flash="off"
  7. mode="scanCode"
  8. @scancode="onScanCode"
  9. @error="onCameraError"
  10. />
  11. <!-- 对焦框覆盖层 -->
  12. <view class="scan-overlay">
  13. <view class="scan-mask top"></view>
  14. <view class="scan-mid">
  15. <view class="scan-mask left"></view>
  16. <view class="scan-frame">
  17. <!-- 四角 -->
  18. <view class="corner tl"></view>
  19. <view class="corner tr"></view>
  20. <view class="corner bl"></view>
  21. <view class="corner br"></view>
  22. <!-- 扫描线 -->
  23. <view class="scan-line"></view>
  24. </view>
  25. <view class="scan-mask right"></view>
  26. </view>
  27. <view class="scan-mask bottom">
  28. <text class="scan-hint">将二维码放入框内,即可自动扫描</text>
  29. </view>
  30. </view>
  31. <!-- 底部关闭 -->
  32. <view class="close-btn" @click="handleClose">
  33. <text>关闭</text>
  34. </view>
  35. </view>
  36. </template>
  37. <script setup lang="ts">
  38. import { openDeviceDoor } from '@/api/replenish';
  39. let scanned = false;
  40. function onScanCode(e: any) {
  41. if (scanned) return;
  42. scanned = true;
  43. const result = (e.detail?.result || '').trim();
  44. if (!result) {
  45. uni.showToast({ title: '未识别到二维码', icon: 'none' });
  46. scanned = false;
  47. return;
  48. }
  49. // 与 haha-mp 一致的 deviceId 提取逻辑
  50. let deviceId = '';
  51. try {
  52. const urlPattern = /\/([A-Z0-9]+)(\?|$)/;
  53. const match = result.match(urlPattern);
  54. if (match && match[1]) {
  55. deviceId = match[1];
  56. } else {
  57. try {
  58. const qrData = JSON.parse(result);
  59. if (qrData.deviceId) deviceId = qrData.deviceId;
  60. } catch (e) {
  61. deviceId = result;
  62. }
  63. }
  64. if (!deviceId) throw new Error('无法解析设备ID');
  65. } catch {
  66. uni.showToast({ title: '二维码格式错误', icon: 'none' });
  67. scanned = false;
  68. return;
  69. }
  70. // 开门
  71. uni.showLoading({ title: '开门中...', mask: true });
  72. openDeviceDoor(deviceId).then(() => {
  73. uni.hideLoading();
  74. uni.showActionSheet({
  75. itemList: ['设备补货', '库存盘点'],
  76. success: (sheetRes: any) => {
  77. if (sheetRes.tapIndex === 0) {
  78. uni.navigateTo({ url: `/pages/replenish/operation?deviceId=${deviceId}` });
  79. } else if (sheetRes.tapIndex === 1) {
  80. uni.navigateTo({ url: `/pages/replenish/audit?deviceId=${deviceId}` });
  81. }
  82. },
  83. fail: () => { scanned = false; }
  84. });
  85. }).catch(() => {
  86. uni.hideLoading();
  87. uni.showToast({ title: '开门失败,请重试', icon: 'none' });
  88. scanned = false;
  89. });
  90. }
  91. function onCameraError(e: any) {
  92. console.error('相机错误:', e.detail);
  93. uni.showToast({ title: '相机启动失败,请授权后重试', icon: 'none' });
  94. setTimeout(() => uni.navigateBack(), 1000);
  95. }
  96. function handleClose() {
  97. uni.navigateBack();
  98. }
  99. </script>
  100. <style lang="scss" scoped>
  101. .page { width: 100vw; height: 100vh; background: #000; position: relative; }
  102. .camera-view { width: 100%; height: 100%; position: absolute; top: 0; left: 0; }
  103. .scan-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 10; display: flex; flex-direction: column; }
  104. .scan-mask { background: rgba(0, 0, 0, 0.5); }
  105. .scan-mask.top, .scan-mask.bottom { flex: 1; }
  106. .scan-mask.left, .scan-mask.right { flex: 1; }
  107. .scan-mid { display: flex; height: 500rpx; }
  108. .scan-frame { width: 500rpx; height: 500rpx; position: relative; }
  109. .corner { position: absolute; width: 40rpx; height: 40rpx; border-color: $primary-color; border-style: solid; z-index: 1;
  110. &.tl { top: 0; left: 0; border-width: 6rpx 0 0 6rpx; border-radius: 8rpx 0 0 0; }
  111. &.tr { top: 0; right: 0; border-width: 6rpx 6rpx 0 0; border-radius: 0 8rpx 0 0; }
  112. &.bl { bottom: 0; left: 0; border-width: 0 0 6rpx 6rpx; border-radius: 0 0 0 8rpx; }
  113. &.br { bottom: 0; right: 0; border-width: 0 6rpx 6rpx 0; border-radius: 0 0 8rpx 0; }
  114. }
  115. .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; }
  116. @keyframes scanAnim {
  117. 0%, 100% { top: 20rpx; }
  118. 50% { top: calc(100% - 20rpx); }
  119. }
  120. .scan-mask.bottom { display: flex; align-items: center; justify-content: center; padding-bottom: 120rpx; }
  121. .scan-hint { font-size: 28rpx; color: rgba(255,255,255,0.8); text-align: center; }
  122. .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);
  123. text { font-size: 30rpx; color: #fff; }
  124. &:active { background: rgba(255,255,255,0.25); }
  125. }
  126. </style>