zhangwencui
2026-07-09 9f1324fb9e73e5b53ac67542227e63cdc0b505ba
手机扫码改为PDA扫码
已添加3个文件
已修改4个文件
1270 ■■■■ 文件已修改
doc/uniapp-pda-scan.md 341 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/pda-media-upload/pda-media-upload.vue 396 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/pda-scan/pda-scan.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/upkeep/add.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/upload.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/works.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/uniapp-pda-scan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,341 @@
# uni-app PDA æ‰«ç ï¼ˆAndroid å¹¿æ’­ï¼‰æŽ¥å…¥æ‰‹å†Œ
适用场景:工业 PDA / æ‰«ææžªè®¾å¤‡â€œæŒ‰ä¸‹ç‰©ç†æ‰«ç é”®åŽï¼Œç”±ç³»ç»Ÿä»¥å¹¿æ’­ï¼ˆBroadcast)方式输出扫码内容”的模式。
目标效果:页面只需要引入一个组件并监听一个事件,就能像本项目一样接收 PDA æ‰«ç ç»“果;同时在没有 PDA èƒ½åŠ›çš„é¡¹ç›®ä¸­å¯é™çº§ä¸ºç›¸æœºæ‰«ç ï¼ˆ`uni.scanCode`)。
---
## 1. åŽŸç†æ¦‚è§ˆï¼ˆæœ¬é¡¹ç›®çš„ç”¨æ³•ï¼‰
本项目的 PDA æ‰«ç é“¾è·¯æ˜¯ï¼š
- ç»„件监听 Android å¹¿æ’­ï¼Œæ‹¿åˆ° `intent.getStringExtra(extraKey)` çš„字符串
- å°†æ‰«ç å­—符串通过 `uni.$emit('scan', { code })` æŠ›ä¸ºå…¨å±€äº‹ä»¶
- é¡µé¢åœ¨ `onShow` æ—¶ `uni.$on('scan', cb)` æŽ¥æ”¶ï¼Œä¸šåŠ¡é‡Œè§£æž `data.code` å¹¶è°ƒç”¨æŽ¥å£
对应实现文件:
- PDA å¹¿æ’­ç›‘听组件:[scan.vue](file:///d:/newcode/mes-ocea-unapp/components/scan/scan.vue#L18-L91)
- é¡µé¢æŽ¥æ”¶ç¤ºä¾‹ï¼š[productBack/index.vue](file:///d:/newcode/mes-ocea-unapp/pages/wareHouse/productBack/index.vue#L96-L127)
---
## 2. æŽ¥å…¥å‰ç½®æ¡ä»¶
**平台限制**
- ä»…支持 `APP-PLUS`(打包成 App)且 `Android` è®¾å¤‡
- H5 / å°ç¨‹åº / iOS ä¸æ”¯æŒ `plus.android`,需要走相机扫码降级方案
**你必须拿到设备的两项配置**
- `action`:扫码广播的 Action(例如本项目某些 PDA ä¸º `com.service.scanner.data`)
- `extraKey`:广播 Intent ä¸­å­˜æ”¾æ‰«ç å­—符串的 key(例如本项目为 `ScanCode`)
获取方式(推荐顺序):
- æŸ¥çœ‹ PDA åŽ‚å•†æ‰«ææœåŠ¡/扫描设置里的“广播输出”配置(通常能直接看到 Action ä¸Ž extraKey)
- å‘设备厂商/集成商索要广播协议(最可靠)
- ä¸´æ—¶åœ¨é¡¹ç›®é‡ŒæŠŠ `action/extraKey` æ‰“日志(本项目已打印),结合厂商文档确认
---
## 3. å¯è¿ç§»çš„通用组件(Vue2/Vue3 é€šç”¨ï¼‰
uni-app çš„ Vue3 é¡¹ç›®ä¾ç„¶æ”¯æŒ Options API,因此推荐用 Options API å†™ä¸€ä¸ªç»„件,达到一份组件同时兼容 Vue2 ä¸Ž Vue3。
### 3.1 æ–°å¢žç»„件文件
在你的目标项目中新建:
- `components/pda-scan/pda-scan.vue`
内容如下(可直接复制):
```vue
<template>
    <view />
</template>
<script>
let mainActivity;
let receiver;
let intentFilter;
let isRegistered = false;
let lock = false;
export default {
    name: "pda-scan",
    props: {
        eventName: {
            type: String,
            default: "scan"
        },
        action: {
            type: String,
            default: ""
        },
        extraKey: {
            type: String,
            default: ""
        },
        debounceMs: {
            type: Number,
            default: 150
        },
        autoStart: {
            type: Boolean,
            default: true
        }
    },
    created() {
        this.init();
    },
    onHide() {
        this.stop();
    },
    beforeDestroy() {
        this.stop();
    },
    beforeUnmount() {
        this.stop();
    },
    methods: {
        init() {
            // #ifdef APP-PLUS
            if (!this.autoStart) return;
            if (!plus || plus.os.name !== "Android") return;
            uni.getSystemInfo({
                success: (sys) => {
                    const cfg = this.resolveBroadcastConfig(sys);
                    if (!cfg.action || !cfg.extraKey) return;
                    this.initAndroidReceiver(cfg.action, cfg.extraKey);
                    this.start();
                }
            });
            // #endif
        },
        resolveBroadcastConfig(sys) {
            const brand = String(sys.brand || "").toUpperCase();
            const model = String(sys.model || "").toUpperCase();
            if (this.action && this.extraKey) {
                return { action: this.action, extraKey: this.extraKey };
            }
            if (brand.indexOf("DROI") !== -1 || model.indexOf("MV-IDP5204") !== -1) {
                return { action: "com.service.scanner.data", extraKey: "ScanCode" };
            }
            return { action: "", extraKey: "" };
        },
        initAndroidReceiver(action, extraKey) {
            const _this = this;
            mainActivity = plus.android.runtimeMainActivity();
            const IntentFilter = plus.android.importClass("android.content.IntentFilter");
            intentFilter = new IntentFilter();
            intentFilter.addAction(action);
            receiver = plus.android.implements("io.dcloud.feature.internal.reflect.BroadcastReceiver", {
                onReceive(context, intent) {
                    plus.android.importClass(intent);
                    const code = intent.getStringExtra(extraKey);
                    _this.emitCode(code);
                }
            });
        },
        start() {
            // #ifdef APP-PLUS
            if (!plus || plus.os.name !== "Android") return;
            if (!mainActivity || !receiver || !intentFilter) return;
            if (isRegistered) return;
            try {
                mainActivity.registerReceiver(receiver, intentFilter);
                isRegistered = true;
            } catch (e) {}
            // #endif
        },
        stop() {
            // #ifdef APP-PLUS
            if (!plus || plus.os.name !== "Android") return;
            if (!mainActivity || !receiver) return;
            if (!isRegistered) return;
            try {
                mainActivity.unregisterReceiver(receiver);
            } catch (e) {}
            isRegistered = false;
            // #endif
        },
        emitCode(code) {
            const value = code == null ? "" : String(code);
            if (!value) return;
            if (lock) return;
            lock = true;
            setTimeout(() => {
                lock = false;
            }, this.debounceMs);
            uni.$emit(this.eventName, { code: value });
        }
    }
};
</script>
```
### 3.2 ä½ éœ€è¦æ”¹å“ªé‡Œ
1) è®¾å¤‡é€‚配(最关键)
修改 `resolveBroadcastConfig()` é‡Œå¯¹ `action/extraKey` çš„返回逻辑,加入你设备的广播配置。
2) ä¹Ÿå¯ä»¥ä¸æ”¹ä»£ç ï¼Œç›´æŽ¥ä¼ å‚覆盖(推荐用于多设备/多环境)
```vue
<pda-scan action="你的action" extraKey="你的extraKey" />
```
---
## 4. é¡µé¢ä¾§ç”¨æ³•(PDA å¹¿æ’­æ‰«ç ï¼‰
### 4.1 Vue2 é¡µé¢ï¼ˆOptions API)示例
```vue
<template>
    <view>
        <pda-scan />
    </view>
</template>
<script>
import PdaScan from "@/components/pda-scan/pda-scan.vue";
export default {
    components: { PdaScan },
    data() {
        return {
            lastCode: ""
        };
    },
    onShow() {
        uni.$off("scan");
        uni.$on("scan", (data) => {
            this.handleScan(data.code);
        });
    },
    onHide() {
        uni.$off("scan");
    },
    methods: {
        handleScan(code) {
            this.lastCode = code;
        }
    }
};
</script>
```
### 4.2 Vue3 é¡µé¢ï¼ˆ`<script setup>`)示例
```vue
<template>
    <view>
        <pda-scan />
    </view>
</template>
<script setup>
import PdaScan from "@/components/pda-scan/pda-scan.vue";
import { onShow, onHide } from "@dcloudio/uni-app";
function handleScan(code) {}
onShow(() => {
    uni.$off("scan");
    uni.$on("scan", (data) => {
        handleScan(data.code);
    });
});
onHide(() => {
    uni.$off("scan");
});
</script>
```
---
## 5. ç›¸æœºæ‰«ç é™çº§ï¼ˆæ²¡æœ‰ PDA ä¾èµ–也能用)
当项目跑在非 `APP-PLUS`、或 iOS、或没有配置 `action/extraKey` æ—¶ï¼ŒPDA å¹¿æ’­ä¸ä¼šç”Ÿæ•ˆã€‚建议提供一个按钮走相机扫码:
```js
function goCameraScan() {
    uni.scanCode({
        success: (res) => {
            handleScan(res.result);
        }
    });
}
```
这样你的业务处理函数只要写一次:`handleScan(code)`。
---
## 6. ä¸šåŠ¡ä¾§è§£æžå»ºè®®ï¼ˆå…¼å®¹â€œçº¯å­—ç¬¦ä¸²æ¡ç  / JSON äºŒç»´ç â€ï¼‰
本项目多个页面的惯用模式是:
- å¦‚æžœ `code` æ˜¯ JSON(例如包含 `sn_no` / `lot_batch_no`),先 `JSON.parse`
- ä¸æ˜¯ JSON å°±å½“做普通条码字符串
可直接复用下面的解析函数:
```js
function normalizeScanText(raw) {
    if (raw == null) return "";
    return String(raw).replace(/\n/g, "").replace(/\s*/g, "").trim();
}
function parseScan(raw) {
    const text = normalizeScanText(raw);
    if (!text) return { text: "", data: null };
    try {
        const data = JSON.parse(text);
        return { text, data };
    } catch (e) {
        return { text, data: null };
    }
}
```
---
## 7. å¸¸è§é—®é¢˜æŽ’查清单
- App è¿è¡Œåœ¨ H5/小程序环境:不会触发广播监听(只能相机扫码)
- iOS:不支持 `plus.android`
- Android è®¾å¤‡æ‰«ç æ²¡ååº”:
  - è®¾å¤‡æ˜¯å¦å¼€å¯äº†â€œå¹¿æ’­è¾“出”
  - `action/extraKey` æ˜¯å¦åŒ¹é…ï¼ˆä¸€ä¸ªå­—符不对都收不到/取不到码)
  - æ˜¯å¦å­˜åœ¨å¤šä¸ªé¡µé¢åŒæ—¶ `uni.$on('scan')` å¯¼è‡´é€»è¾‘串扰(建议页面 `onShow` å…ˆ `uni.$off`)
- æŠ¥é”™ `unregisterReceiver`:
  - å¤šå‘生在“未 register å°± unregister”,组件里已做 `isRegistered` ä¸Ž try/catch ä¿æŠ¤ï¼›å¦‚果你改代码后又出现,优先检查 `start/stop` æ˜¯å¦è¢«é‡å¤è°ƒç”¨
---
## 8. ä¸Žæœ¬é¡¹ç›®ä¿æŒä¸€è‡´çš„事件协议
为了让你在其他项目里复刻“拿到扫码后页面直接处理”的体验,建议保持:
- äº‹ä»¶åï¼š`scan`
- äº‹ä»¶å‚数:`{ code: string }`
如果你要改事件名(例如多个扫码源区分),组件提供了 `eventName`:
```vue
<pda-scan eventName="scan_pda" />
```
src/components/pda-media-upload/pda-media-upload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,396 @@
<template>
  <view class="pda-media-upload">
    <view class="upload-buttons">
      <u-button type="primary"
                @click="pickMedia('image')"
                :loading="uploading"
                :disabled="disabled || modelValue.length >= limit"
                :customStyle="{ marginRight: '10px', flex: 1 }">
        <u-icon name="camera"
                size="18"
                color="#fff"
                style="margin-right: 5px"></u-icon>
        {{ uploading ? "上传中..." : "拍照" }}
      </u-button>
      <u-button type="success"
                @click="pickMedia('video')"
                :loading="uploading"
                :disabled="disabled || modelValue.length >= limit"
                :customStyle="{ flex: 1 }">
        <uni-icons type="videocam"
                   size="18"
                   color="#fff"
                   style="margin-right: 5px"></uni-icons>
        {{ uploading ? "上传中..." : "拍视频" }}
      </u-button>
    </view>
    <view v-if="uploading"
          class="upload-progress">
      <u-line-progress :percentage="uploadProgress"
                       :showText="true"
                       activeColor="#409eff"></u-line-progress>
    </view>
    <view v-if="modelValue.length > 0"
          class="file-list">
      <view v-for="(file, index) in modelValue"
            :key="index"
            class="file-item">
        <view class="file-preview-container">
          <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                 :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                 class="file-preview"
                 mode="aspectFill" />
          <view v-else-if="file.type === 'video'"
                class="video-preview">
            <uni-icons type="videocam"
                       size="18"
                       color="#fff"
                       style="margin-right: 5px"></uni-icons>
            <text class="video-text">视频</text>
          </view>
          <view class="delete-btn"
                @click="removeFile(index)">
            <u-icon name="close"
                    size="12"
                    color="#fff"></u-icon>
          </view>
        </view>
        <view class="file-info">
          <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频') }}</text>
          <text class="file-size">{{ formatFileSize(file.size) }}</text>
        </view>
      </view>
    </view>
    <view v-else
          class="empty-state">
      <text>请选择要上传的图片或视频</text>
    </view>
  </view>
</template>
<script setup>
  import { ref } from "vue";
  import { getToken } from "@/utils/auth";
  const props = defineProps({
    modelValue: { type: Array, default: () => [] },
    action: { type: String, default: "" },
    uploadUrl: { type: String, default: "" },
    limit: { type: Number, default: 10 },
    uploadType: { type: Number, default: 10 },
    formData: { type: Object, default: () => ({}) },
    disabled: { type: Boolean, default: false },
  });
  const emit = defineEmits([
    "update:modelValue",
    "uploaded",
    "uploading",
    "progress",
    "choose-media",
  ]);
  const uploading = ref(false);
  const uploadProgress = ref(0);
  // ç›‘听上传状态,发出事件
  const watchUploading = val => {
    emit("uploading", val);
  };
  // ç›‘听进度,发出事件
  const watchProgress = val => {
    emit("progress", val);
  };
  const showToast = message => {
    uni.showToast({ title: message, icon: "none" });
  };
  const updateList = list => {
    emit("update:modelValue", list);
  };
  const formatFileSize = size => {
    if (!size) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    let index = 0;
    let fileSize = size;
    while (fileSize >= 1024 && index < units.length - 1) {
      fileSize /= 1024;
      index++;
    }
    return `${fileSize.toFixed(2)} ${units[index]}`;
  };
  const removeFile = index => {
    const next = props.modelValue.slice();
    next.splice(index, 1);
    updateList(next);
  };
  const uploadFile = file => {
    const url = props.action || props.uploadUrl;
    if (!url) {
      showToast("未配置上传地址");
      return;
    }
    const token = getToken();
    if (!token) {
      showToast("用户未登录");
      return;
    }
    uploading.value = true;
    watchUploading(true);
    uploadProgress.value = 0;
    watchProgress(0);
    const uploadTask = uni.uploadFile({
      url: url,
      filePath: file.tempFilePath,
      name: "files",
      header: {
        Authorization: `Bearer ${token}`,
      },
      formData: {
        type: props.uploadType,
        ...props.formData,
      },
      success: res => {
        try {
          const data = JSON.parse(res.data);
          if (data.code === 200) {
            const resultData = Array.isArray(data.data)
              ? data.data[0]
              : data.data;
            const finalUrl = resultData.url || resultData.previewURL;
            const finalName = resultData.name || resultData.originalFilename;
            const finalId =
              resultData.tempId || resultData.id || resultData.tempFileId;
            const uploadedFile = {
              ...file,
              ...resultData,
              url: finalUrl,
              name: finalName,
              tempId: finalId,
              status: "success",
            };
            updateList(props.modelValue.concat([uploadedFile]));
            emit("uploaded", uploadedFile);
            uni.showToast({ title: "上传成功", icon: "success" });
          } else {
            showToast(data.msg || "上传失败");
          }
        } catch (e) {
          showToast("解析响应失败");
        }
      },
      fail: err => {
        console.error("上传失败:", err);
        showToast("上传失败");
      },
      complete: () => {
        uploading.value = false;
        watchUploading(false);
      },
    });
    uploadTask.onProgressUpdate(res => {
      uploadProgress.value = res.progress;
      watchProgress(res.progress);
    });
  };
  const pickMedia = type => {
    emit("choose-media", type);
    if (props.modelValue.length >= props.limit) {
      showToast(`最多只能选择${props.limit}个文件`);
      return;
    }
    const remaining = props.limit - props.modelValue.length;
    if (typeof uni.chooseMedia === "function") {
      uni.chooseMedia({
        count: Math.min(remaining, 1),
        mediaType: [type || "image"],
        sizeType: ["compressed", "original"],
        sourceType: ["camera"],
        success: res => {
          try {
            const files = res?.tempFiles || [];
            if (!files.length) throw new Error("未获取到文件");
            files.forEach((tf, idx) => {
              const filePath = tf.tempFilePath || tf.path || "";
              const fileType = tf.fileType || type || "image";
              const ext = fileType === "video" ? "mp4" : "jpg";
              const file = {
                tempFilePath: filePath,
                path: filePath,
                type: fileType,
                name: `${fileType}_${Date.now()}_${idx}.${ext}`,
                size: tf.size || 0,
                duration: tf.duration || 0,
                createTime: Date.now(),
              };
              uploadFile(file);
            });
          } catch (err) {
            showToast(err.message || "处理文件失败");
          }
        },
        fail: err => {
          console.error("选择媒体失败:", err);
          showToast("选择失败");
        },
      });
      return;
    }
    if (type === "video") {
      uni.chooseVideo({
        sourceType: ["camera"],
        success: res => {
          const file = {
            tempFilePath: res.tempFilePath,
            path: res.tempFilePath,
            type: "video",
            name: `video_${Date.now()}.mp4`,
            size: res.size || 0,
            duration: res.duration || 0,
            createTime: Date.now(),
          };
          uploadFile(file);
        },
        fail: () => {
          showToast("选择视频失败");
        },
      });
      return;
    }
    uni.chooseImage({
      count: Math.min(remaining, 9),
      sizeType: ["compressed"],
      sourceType: ["camera"],
      success: res => {
        const list = res.tempFilePaths || res.tempFiles || [];
        list.forEach((src, idx) => {
          const path = typeof src === "string" ? src : src.path;
          const file = {
            tempFilePath: path,
            path: path,
            type: "image",
            name: `image_${Date.now()}_${idx}.jpg`,
            size: 0,
            createTime: Date.now(),
          };
          uploadFile(file);
        });
      },
      fail: () => {
        showToast("选择图片失败");
      },
    });
  };
</script>
<style scoped>
  .upload-buttons {
    display: flex;
  }
  .upload-progress {
    margin-top: 12px;
  }
  .file-list {
    margin-top: 12px;
  }
  .file-item {
    display: flex;
    align-items: center;
    padding: 10px;
    background: #f7f8fa;
    border-radius: 10px;
    margin-bottom: 10px;
  }
  .file-preview-container {
    position: relative;
    width: 60px;
    height: 60px;
    border-radius: 8px;
    overflow: hidden;
    margin-right: 12px;
    background: #e9edf2;
  }
  .file-preview {
    width: 100%;
    height: 100%;
  }
  .video-preview {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.5);
  }
  .video-text {
    font-size: 12px;
    color: #fff;
  }
  .delete-btn {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 18px;
    height: 18px;
    border-radius: 9px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.5);
  }
  .file-info {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .file-name {
    font-size: 13px;
    color: #333;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .file-size {
    font-size: 12px;
    color: #999;
  }
  .empty-state {
    margin-top: 12px;
    padding: 14px 0;
    text-align: center;
    color: #999;
    font-size: 13px;
  }
</style>
src/components/pda-scan/pda-scan.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
<template>
  <view />
</template>
<script>
  let mainActivity;
  let receiver;
  let intentFilter;
  let isRegistered = false;
  let lock = false;
  export default {
    name: "PdaScan",
    props: {
      active: { type: Boolean, default: true },
      eventName: { type: String, default: "scan" },
      action: { type: String, default: "" },
      extraKey: { type: String, default: "" },
      debounceMs: { type: Number, default: 150 },
    },
    mounted() {
      this.init();
      if (this.active) this.start();
    },
    beforeUnmount() {
      this.stop();
    },
    watch: {
      active(val) {
        if (val) {
          this.init();
          this.start();
        } else {
          this.stop();
        }
      },
    },
    methods: {
      canUseAndroidBroadcast() {
        try {
          if (typeof plus === "undefined") return false;
          if (!plus.os || plus.os.name !== "Android") return false;
          if (!plus.android) return false;
          return true;
        } catch (e) {
          return false;
        }
      },
      resolveBroadcastConfig(sys) {
        if (this.action && this.extraKey) {
          return { action: this.action, extraKey: this.extraKey };
        }
        const brand = String(sys?.brand || "").toUpperCase();
        const model = String(sys?.model || "").toUpperCase();
        if (
          brand.includes("DROI") ||
          model.includes("MV-IDP5204") ||
          model.includes("IDP")
        ) {
          return { action: "com.service.scanner.data", extraKey: "ScanCode" };
        }
        return { action: "", extraKey: "" };
      },
      init() {
        if (!this.canUseAndroidBroadcast()) return;
        if (mainActivity && receiver && intentFilter) return;
        let sys = {};
        try {
          sys = uni.getSystemInfoSync() || {};
        } catch (e) {
          sys = {};
        }
        const cfg = this.resolveBroadcastConfig(sys);
        if (!cfg.action || !cfg.extraKey) return;
        this.initAndroidReceiver(cfg.action, cfg.extraKey);
      },
      initAndroidReceiver(action, extraKey) {
        const _this = this;
        mainActivity = plus.android.runtimeMainActivity();
        const IntentFilter = plus.android.importClass(
          "android.content.IntentFilter"
        );
        intentFilter = new IntentFilter();
        intentFilter.addAction(action);
        receiver = plus.android.implements(
          "io.dcloud.feature.internal.reflect.BroadcastReceiver",
          {
            onReceive(context, intent) {
              plus.android.importClass(intent);
              const code = intent.getStringExtra(extraKey);
              _this.emitCode(code);
            },
          }
        );
      },
      start() {
        if (!this.canUseAndroidBroadcast()) return;
        if (!mainActivity || !receiver || !intentFilter) return;
        if (isRegistered) return;
        try {
          mainActivity.registerReceiver(receiver, intentFilter);
          isRegistered = true;
        } catch (e) {}
      },
      stop() {
        if (!this.canUseAndroidBroadcast()) return;
        if (!mainActivity || !receiver) return;
        if (!isRegistered) return;
        try {
          mainActivity.unregisterReceiver(receiver);
        } catch (e) {}
        isRegistered = false;
      },
      emitCode(code) {
        const value = code == null ? "" : String(code);
        const text = value.replace(/\n/g, "").trim();
        if (!text) return;
        if (lock) return;
        lock = true;
        setTimeout(() => {
          lock = false;
        }, this.debounceMs);
        const payload = { code: text };
        uni.$emit(this.eventName, payload);
        this.$emit("scan", payload);
      },
    },
  };
</script>
src/pages/equipmentManagement/repair/add.vue
@@ -129,6 +129,8 @@
                        @confirm="onDateConfirm"
                        @cancel="showDate = false"
                        mode="date" />
    <PdaScan :active="pdaScanActive"
             eventName="scan_repair_device" />
  </view>
</template>
@@ -137,6 +139,7 @@
  import { onShow, onLoad } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import PdaScan from "@/components/pda-scan/pda-scan.vue";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import {
    addRepair,
@@ -185,6 +188,8 @@
  // æ‰«ç ç›¸å…³çŠ¶æ€
  const isScanning = ref(false);
  const scanTimer = ref(null);
  const pdaScanActive = ref(false);
  const pdaScanTimer = ref(null);
  // è¡¨å•验证规则
  const formRules = {
@@ -296,7 +301,51 @@
      return;
    }
    // è°ƒç”¨uni-app的扫码API
    const canUsePdaBroadcast = () => {
      try {
        if (typeof plus === "undefined") return false;
        if (plus?.os?.name !== "Android") return false;
        if (!plus?.android) return false;
        const sys = uni.getSystemInfoSync?.() || {};
        const brand = String(sys?.brand || "").toUpperCase();
        const model = String(sys?.model || "").toUpperCase();
        return (
          brand.includes("DROI") ||
          model.includes("MV-IDP5204") ||
          model.includes("IDP")
        );
      } catch (e) {
        return false;
      }
    };
    if (canUsePdaBroadcast()) {
      if (pdaScanTimer.value) {
        clearTimeout(pdaScanTimer.value);
        pdaScanTimer.value = null;
      }
      uni.$off("scan_repair_device");
      uni.$on("scan_repair_device", data => {
        pdaScanActive.value = false;
        uni.$off("scan_repair_device");
        if (pdaScanTimer.value) {
          clearTimeout(pdaScanTimer.value);
          pdaScanTimer.value = null;
        }
        handleScanResult(data?.code);
      });
      pdaScanActive.value = true;
      uni.showToast({ title: "请使用PDA扫码", icon: "none" });
      pdaScanTimer.value = setTimeout(() => {
        pdaScanActive.value = false;
        uni.$off("scan_repair_device");
        pdaScanTimer.value = null;
      }, 20000);
      return;
    }
    uni.scanCode({
      scanType: ["qrCode", "barCode"],
      success: res => {
@@ -388,6 +437,10 @@
    if (scanTimer.value) {
      clearTimeout(scanTimer.value);
    }
    if (pdaScanTimer.value) {
      clearTimeout(pdaScanTimer.value);
    }
    uni.$off("scan_repair_device");
  });
  // æäº¤è¡¨å•
@@ -522,4 +575,4 @@
    margin-left: 8px;
    cursor: pointer;
  }
</style>
</style>
src/pages/equipmentManagement/upkeep/add.vue
@@ -85,6 +85,8 @@
                        @confirm="onDateConfirm"
                        @cancel="showDate = false"
                        mode="date" />
    <PdaScan :active="pdaScanActive"
             eventName="scan_upkeep_device" />
  </view>
</template>
@@ -93,6 +95,7 @@
  import { onShow } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import CommonUpload from "@/components/CommonUpload.vue";
  import PdaScan from "@/components/pda-scan/pda-scan.vue";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import {
    addUpkeep,
@@ -140,6 +143,8 @@
  // æ‰«ç ç›¸å…³çŠ¶æ€
  const isScanning = ref(false);
  const scanTimer = ref(null);
  const pdaScanActive = ref(false);
  const pdaScanTimer = ref(null);
  // è¡¨å•验证规则
  const formRules = {
@@ -210,7 +215,51 @@
      return;
    }
    // è°ƒç”¨uni-app的扫码API
    const canUsePdaBroadcast = () => {
      try {
        if (typeof plus === "undefined") return false;
        if (plus?.os?.name !== "Android") return false;
        if (!plus?.android) return false;
        const sys = uni.getSystemInfoSync?.() || {};
        const brand = String(sys?.brand || "").toUpperCase();
        const model = String(sys?.model || "").toUpperCase();
        return (
          brand.includes("DROI") ||
          model.includes("MV-IDP5204") ||
          model.includes("IDP")
        );
      } catch (e) {
        return false;
      }
    };
    if (canUsePdaBroadcast()) {
      if (pdaScanTimer.value) {
        clearTimeout(pdaScanTimer.value);
        pdaScanTimer.value = null;
      }
      uni.$off("scan_upkeep_device");
      uni.$on("scan_upkeep_device", data => {
        pdaScanActive.value = false;
        uni.$off("scan_upkeep_device");
        if (pdaScanTimer.value) {
          clearTimeout(pdaScanTimer.value);
          pdaScanTimer.value = null;
        }
        handleScanResult(data?.code);
      });
      pdaScanActive.value = true;
      uni.showToast({ title: "请使用PDA扫码", icon: "none" });
      pdaScanTimer.value = setTimeout(() => {
        pdaScanActive.value = false;
        uni.$off("scan_upkeep_device");
        pdaScanTimer.value = null;
      }, 20000);
      return;
    }
    uni.scanCode({
      scanType: ["qrCode", "barCode"],
      success: res => {
@@ -309,6 +358,10 @@
    if (scanTimer.value) {
      clearTimeout(scanTimer.value);
    }
    if (pdaScanTimer.value) {
      clearTimeout(pdaScanTimer.value);
    }
    uni.$off("scan_upkeep_device");
  });
  // æäº¤è¡¨å•
@@ -441,4 +494,4 @@
    margin-left: 8px;
    cursor: pointer;
  }
</style>
</style>
src/pages/inspectionUpload/upload.vue
@@ -61,77 +61,18 @@
      <view class="section-card"
            v-if="hasException === true">
        <view class="section-title">巡检照片/视频</view>
        <!-- å½“前分类的上传区域 -->
        <view class="upload-area">
          <view class="upload-buttons">
            <u-button type="primary"
                      @click="chooseMedia('image')"
                      :loading="uploading"
                      :disabled="beforeModelValue.length >= uploadConfig.limit"
                      :customStyle="{ marginRight: '10px', flex: 1 }">
              <u-icon name="camera"
                      size="18"
                      color="#fff"
                      style="margin-right: 5px"></u-icon>
              {{ uploading ? '上传中...' : '拍照' }}
            </u-button>
            <u-button type="success"
                      @click="chooseMedia('video')"
                      :loading="uploading"
                      :disabled="beforeModelValue.length >= uploadConfig.limit"
                      :customStyle="{ flex: 1 }">
              <uni-icons type="videocam"
                         size="18"
                         color="#fff"
                         style="margin-right: 5px"></uni-icons>
              {{ uploading ? '上传中...' : '拍视频' }}
            </u-button>
          </view>
          <!-- ä¸Šä¼ è¿›åº¦ -->
          <view v-if="uploading"
                class="upload-progress">
            <u-line-progress :percentage="uploadProgress"
                             :showText="true"
                             activeColor="#409eff"></u-line-progress>
          </view>
          <!-- å½“前分类的文件列表 -->
          <view v-if="beforeModelValue.length > 0"
                class="file-list">
            <view v-for="(file, index) in beforeModelValue"
                  :key="index"
                  class="file-item">
              <view class="file-preview-container">
                <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
                       :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
                       class="file-preview"
                       mode="aspectFill" />
                <view v-else-if="file.type === 'video'"
                      class="video-preview">
                  <uni-icons type="videocam"
                             size="18"
                             color="#fff"
                             style="margin-right: 5px"></uni-icons>
                  <text class="video-text">视频</text>
                </view>
                <!-- åˆ é™¤æŒ‰é’® -->
                <view class="delete-btn"
                      @click="removeFile(index)">
                  <u-icon name="close"
                          size="12"
                          color="#fff"></u-icon>
                </view>
              </view>
              <view class="file-info">
                <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频') }}</text>
                <text class="file-size">{{ formatFileSize(file.size) }}</text>
              </view>
            </view>
          </view>
          <view v-if="beforeModelValue.length === 0"
                class="empty-state">
            <text>请选择要上传的图片或视频</text>
          </view>
        </view>
        <!-- ä½¿ç”¨ PDA åª’体上传组件 -->
        <PdaMediaUpload v-model="beforeModelValue"
                        :action="uploadFileUrl"
                        :limit="uploadConfig.limit"
                        :fileSize="uploadConfig.fileSize"
                        :formData="{ type: 10 }"
                        :allowCamera="true"
                        :allowVideo="true"
                        @update:modelValue="handleFilesUpdate"
                        @choose-media="handleChooseMedia"
                        @uploading="uploading = $event"
                        @progress="uploadProgress = $event" />
        <!-- ç»Ÿè®¡ä¿¡æ¯ -->
        <view class="upload-summary">
          <text class="summary-text">
@@ -166,8 +107,9 @@
<script setup>
  import { ref, computed, onMounted } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
  import PageHeader from "@/components/PageHeader.vue";
  import PdaMediaUpload from "@/components/pda-media-upload/pda-media-upload.vue";
  import { uploadInspectionTask } from "@/api/inspectionManagement";
  import { getToken } from "@/utils/auth";
  import config from "@/config";
@@ -590,10 +532,34 @@
    });
  };
  // åˆ é™¤æ–‡ä»¶
  const removeFile = index => {
    beforeModelValue.value.splice(index, 1);
  // ç›‘听文件列表变化(组件内部处理删除,父组件只同步)
  const handleFilesUpdate = files => {
    beforeModelValue.value = files;
  };
  // ç›‘听组件的 choose-media äº‹ä»¶ï¼ˆç”¨äºŽå…¼å®¹æ—§é€»è¾‘,或者在这里添加自定义操作)
  const handleChooseMedia = type => {
    // å¯ä»¥åœ¨è¿™é‡Œæ·»åŠ è‡ªå®šä¹‰çš„å‰ç½®æ“ä½œ
    console.log("用户选择上传类型:", type);
  };
  // PDA æ‰«ç ç›‘听(可选:如果需要通过扫码添加某个标签或信息,在这里处理)
  const handlePdaScan = data => {
    console.log("PDA æ‰«ç ç»“æžœ:", data);
    uni.showToast({
      title: `扫码结果: ${data.code}`,
      icon: "none",
    });
  };
  onShow(() => {
    // é¡µé¢æ˜¾ç¤ºæ—¶ï¼Œç›‘听 PDA æ‰«ç ï¼ˆå¦‚果有需要的话)
    uni.$on("scan_inspection", handlePdaScan);
  });
  onHide(() => {
    uni.$off("scan_inspection");
  });
</script>
<style scoped>
src/pages/works.vue
@@ -299,6 +299,8 @@
        </up-grid>
      </view>
    </view>
    <PdaScan :active="pdaScanActive"
             eventName="scan_work_report" />
    <DownloadProgressMask />
  </view>
</template>
@@ -308,6 +310,7 @@
  import { userLoginFacotryList } from "@/api/login";
  import { getProductWorkOrderById } from "@/api/productionManagement/productionReporting";
  import DownloadProgressMask from "@/components/DownloadProgressMask.vue";
  import PdaScan from "@/components/pda-scan/pda-scan.vue";
  import modal from "@/plugins/modal";
  import useUserStore from "@/store/modules/user";
@@ -315,6 +318,8 @@
  const show = ref(false);
  const factoryList = ref([]);
  const factoryListTem = ref([]);
  const pdaScanActive = ref(false);
  let scanWorkReportTimer = null;
  const iconStyle = {
    fontSize: "1.125rem",
    color: "#2979ff",
@@ -1072,78 +1077,121 @@
      });
  }
  const getcode = async () => {
    uni.scanCode({
      success: async res => {
        // è§£æžäºŒç»´ç å†…容
        const scanResult = res.result;
        let orderRow = "";
    const canUsePdaBroadcast = () => {
      try {
        if (typeof plus === "undefined") return false;
        if (plus?.os?.name !== "Android") return false;
        if (!plus?.android) return false;
        // åˆ¤æ–­æ‰«æç»“果是否为纯数字(id)
        const isNumericId = /^\d+$/.test(scanResult.trim());
        const sys = uni.getSystemInfoSync?.() || {};
        const brand = String(sys?.brand || "").toUpperCase();
        const model = String(sys?.model || "").toUpperCase();
        if (isNumericId) {
          // å¦‚果是纯数字,根据 id èŽ·å–å·¥å•æ•°æ®
          const workOrderId = scanResult.trim();
          modal.loading("正在获取工单信息...");
          try {
            const workRes = await getProductWorkOrderById({ id: workOrderId });
            modal.closeLoading();
        return (
          brand.includes("DROI") ||
          model.includes("MV-IDP5204") ||
          model.includes("IDP")
        );
      } catch (e) {
        return false;
      }
    };
            console.log("工单查询结果:", workRes);
    const handleWorkReportScan = async scanResultRaw => {
      const scanResult = scanResultRaw == null ? "" : String(scanResultRaw);
      let orderRow = "";
            if (workRes.code === 200 && workRes.data) {
              // æ–°æŽ¥å£è¿”回的是单个对象,不是数组
              const workData = workRes.data;
              console.log("工单数据:", workData);
      const isNumericId = /^\d+$/.test(scanResult.trim());
              if (workData.endOrder === true) {
                modal.msgError("该订单已结束,无法报工");
                return;
              }
      if (isNumericId) {
        const workOrderId = scanResult.trim();
        modal.loading("正在获取工单信息...");
        try {
          const workRes = await getProductWorkOrderById({ id: workOrderId });
          modal.closeLoading();
              orderRow = JSON.stringify(workData);
          console.log("工单查询结果:", workRes);
              console.log("构造的orderRow:", orderRow);
            } else {
              modal.msgError("未找到对应的工单信息");
          if (workRes.code === 200 && workRes.data) {
            const workData = workRes.data;
            console.log("工单数据:", workData);
            if (workData.endOrder === true) {
              modal.msgError("该订单已结束,无法报工");
              return;
            }
          } catch (error) {
            modal.closeLoading();
            console.error("获取工单信息失败:", error);
            modal.msgError("获取工单信息失败: " + (error.message || "未知错误"));
            return;
          }
        } else {
          // å¦‚果不是纯数字,尝试从扫码结果中提取orderRow参数
          try {
            // å¤„理混合格式: http://...?orderRow={...}
            const orderRowStart = scanResult.indexOf("orderRow={");
            if (orderRowStart !== -1) {
              // æå–从orderRow={开始的JSON内容
              const jsonPart = scanResult.substring(orderRowStart + 9); // 9是"orderRow=".length
              orderRow = jsonPart;
            } else {
              // å¦‚果直接是JSON字符串,尝试解析
              orderRow = scanResult;
            }
          } catch (e) {
            console.error(e, "解析失败====????=====");
            orderRow = "";
          }
          // éªŒè¯æ˜¯å¦ä¸ºæœ‰æ•ˆçš„JSON
          try {
            JSON.parse(orderRow);
          } catch (error) {
            modal.msgError("订单解析失败,请检查二维码格式");
            orderRow = JSON.stringify(workData);
            console.log("构造的orderRow:", orderRow);
          } else {
            modal.msgError("未找到对应的工单信息");
            return;
          }
        } catch (error) {
          modal.closeLoading();
          console.error("获取工单信息失败:", error);
          modal.msgError("获取工单信息失败: " + (error.message || "未知错误"));
          return;
        }
        // æ‰«ç æˆåŠŸåŽè·³è½¬åˆ°ç”Ÿäº§æŠ¥å·¥é¡µé¢ï¼Œå¹¶ä¼ é€’orderRow参数
        uni.navigateTo({
          url: `/pages/productionManagement/productionReport/index?orderRow=${orderRow}`,
        });
      } else {
        try {
          const orderRowStart = scanResult.indexOf("orderRow={");
          if (orderRowStart !== -1) {
            const jsonPart = scanResult.substring(orderRowStart + 9);
            orderRow = jsonPart;
          } else {
            orderRow = scanResult;
          }
        } catch (e) {
          console.error(e, "解析失败====????=====");
          orderRow = "";
        }
        try {
          JSON.parse(orderRow);
        } catch (error) {
          modal.msgError("订单解析失败,请检查二维码格式");
          return;
        }
      }
      uni.navigateTo({
        url: `/pages/productionManagement/productionReport/index?orderRow=${orderRow}`,
      });
    };
    if (canUsePdaBroadcast()) {
      if (scanWorkReportTimer) {
        clearTimeout(scanWorkReportTimer);
        scanWorkReportTimer = null;
      }
      uni.$off("scan_work_report");
      uni.$on("scan_work_report", async data => {
        pdaScanActive.value = false;
        uni.$off("scan_work_report");
        if (scanWorkReportTimer) {
          clearTimeout(scanWorkReportTimer);
          scanWorkReportTimer = null;
        }
        await handleWorkReportScan(data?.code);
      });
      pdaScanActive.value = true;
      uni.showToast({
        title: "请使用PDA扫码",
        icon: "none",
      });
      scanWorkReportTimer = setTimeout(() => {
        pdaScanActive.value = false;
        uni.$off("scan_work_report");
        scanWorkReportTimer = null;
      }, 20000);
      return;
    }
    uni.scanCode({
      success: async res => {
        await handleWorkReportScan(res.result);
      },
      fail: err => {
        uni.showToast({