spring
2025-11-19 af4f45eaa2703ecf991bd10f07f6df179f2677d9
Merge branch 'refs/heads/yyb'

# Conflicts:
# src/pages/production/twist/receive/monofil.vue
已添加10个文件
已修改25个文件
4592 ■■■■■ 文件已修改
src/App.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/product/twist.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/routingInspection/routingInspection.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/product_card/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/useScanCode.ts 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/manifest.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/components/ProductionCard.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/components/Statistics.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/detail/twistDetail.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/detail/wireDetail.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/index.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/list/index.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/attachment/index.vue 221 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/components/MonofilCard.vue 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/components/TwistReportCard.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/monofil.vue 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/steelCore/edit.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/steelCore/form.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/steelCore/index.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/report/draw.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/report/form.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/report/index.vue 205 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/wire/attachment/index.vue 232 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/routingInspection/detail/indexJX.vue 867 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/routingInspection/detail/indexLS.vue 788 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/routingInspection/index.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/routingInspection/list/index.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/routingInspection/product_card/index.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/routingInspection/upload.vue 587 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/icons/routingInspection.png 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/cache.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue
@@ -1,22 +1,81 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { useThemeStore } from "@/store";
import { ref } from "vue";
// ä¸»é¢˜åˆå§‹åŒ–
const themeStore = useThemeStore();
// å…¨å±€æ‰«ç å¹¿æ’­æŽ¥æ”¶å™¨
let main: any = null;
let receiver: any = null;
let filter: any = null;
// åˆå§‹åŒ–扫码广播接收
const initGlobalScan = () => {
  // #ifdef APP-PLUS
  try {
    main = plus.android.runtimeMainActivity();
    const IntentFilter = plus.android.importClass("android.content.IntentFilter");
    filter = new IntentFilter();
    filter.addAction("com.dwexample.ACTION");
    receiver = plus.android.implements("io.dcloud.feature.internal.reflect.BroadcastReceiver", {
      onReceive: (context: any, intent: any) => {
        console.log("🔍 [全局Scan] onReceive è§¦å‘:", context, intent);
        plus.android.importClass(intent);
        const scanResult = intent.getStringExtra("com.motorolasolutions.emdk.datawedge.data_string");
        console.log("🔍 [全局Scan] æ‰«æç»“æžœ:", scanResult);
        // å‘送到所有可能的事件
        const eventNames = ["scan", "scanIndex", "scanJX", "scanLS"];
        eventNames.forEach((eventName) => {
          uni.$emit(eventName, { code: scanResult });
          console.log(`🔍 [全局Scan] å·²å‘送 ${eventName} äº‹ä»¶`);
        });
      },
    });
    // æ³¨å†Œå¹¿æ’­æŽ¥æ”¶å™¨
    main.registerReceiver(receiver, filter);
    console.log("🔍 [全局Scan] å…¨å±€æ‰«ç å¹¿æ’­æŽ¥æ”¶å™¨å·²å¯åЍ");
  } catch (error) {
    console.error("🔍 [全局Scan] åˆå§‹åŒ–失败:", error);
  }
  // #endif
};
// åœæ­¢æ‰«ç å¹¿æ’­æŽ¥æ”¶
const stopGlobalScan = () => {
  // #ifdef APP-PLUS
  try {
    if (main && receiver) {
      main.unregisterReceiver(receiver);
      console.log("🔍 [全局Scan] å…¨å±€æ‰«ç å¹¿æ’­æŽ¥æ”¶å™¨å·²åœæ­¢");
    }
  } catch (error) {
    console.error("🔍 [全局Scan] åœæ­¢å¤±è´¥:", error);
  }
  // #endif
};
onLaunch(() => {
  console.log("App Launch");
  // åˆå§‹åŒ–主题
  themeStore.initTheme();
  // åˆå§‹åŒ–全局扫码广播接收器
  initGlobalScan();
});
onShow(() => {
  console.log("App Show");
  // åº”用显示时重新启动广播接收器
  initGlobalScan();
});
onHide(() => {
  console.log("App Hide");
  // åº”用隐藏时不停止广播(保持后台接收)
});
</script>
src/api/product/twist.ts
@@ -79,6 +79,24 @@
      data: data,
    });
  },
  // åˆ é™¤å•丝领用
  deleteStrandedWireDish(id: number) {
    return request<BaseResult<any>>({
      url: `/strandedWire/deleteStrandedWireDish/${id}`,
      method: "DELETE",
    });
  },
  // åˆ é™¤ç»žçº¿æŠ¥å·¥
  deleteWireOutput(params: { id: number }) {
    // å°†å‚数拼接到 URL ä½œä¸º query å‚æ•°
    const queryString = `?id=${params.id}`;
    return request<BaseResult<any>>({
      url: `/strandedWire/deleteWireOutput${queryString}`,
      method: "DELETE",
    });
  },
};
export default TwistApi;
src/api/routingInspection/routingInspection.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
import request from "@/utils/request";
import { BaseResult } from "@/models/base";
const RoutingInspectionApi = {
  // æŸ¥è¯¢å·¡æ£€è®°å½•
  getDeviceInspectListByPatrol(params: any) {
    return request<BaseResult<any>>({
      url: "/wireInspection/getDeviceInspectListByPatrol",
      method: "GET",
      data: params,
    });
  },
  // èŽ·å–å·¡æ£€æ•°æ®
  getInspectListByPatrol(data: any) {
    return request<BaseResult<any>>({
      url: "/wireInspection/getInspectListByPatrol",
      method: "POST",
      data: data,
    });
  },
  // èŽ·å–æ‹‰ä¸å•ä¸ªç»“æž„è®°å½• 0
  getDrawInspectInfoById(params: any) {
    return request<BaseResult<any>>({
      url: "/wireInspection/getDrawInspectInfoById/" + params.id,
      method: "GET",
      // data: params,
    });
  },
  // èŽ·å–ç»žçº¿å•ä¸ªç»“æž„è®°å½• 1
  getStrandedInspectionStructureInfoById(params: any) {
    return request<BaseResult<any>>({
      url: "/wireInspection/getStrandedInspectionStructureInfoById/" + params.id,
      method: "GET",
      // data: params,
    });
  },
  // æ‹‰ä¸å·¡æ£€ä¿å­˜
  drawPatrolCheckInspection(data: any) {
    return request<BaseResult<any>>({
      url: "/wireInspection/drawPatrolCheckInspection?deviceUid=" + data.deviceUid,
      method: "POST",
      data: data,
    });
  },
  // ç»žçº¿å·¡æ£€ä¿å­˜
  strandedPatrolCheckInspection(data: any) {
    return request<BaseResult<any>>({
      url: "/wireInspection/strandedPatrolCheckInspection?deviceUid=" + data.deviceUid,
      method: "POST",
      data: data,
    });
  },
  // éªŒè¯äºŒç»´ç 
  assertScanQR(params: { deviceUid: string }) {
    return request<BaseResult<any>>({
      url: "/wireInspection/assertScanQR?deviceUid=" + params.deviceUid,
      method: "GET",
    });
  },
};
export default RoutingInspectionApi;
src/components/product_card/index.vue
@@ -8,6 +8,19 @@
        </wd-tag>
      </view>
    </template>
    <wd-row class="my-2" v-if="data[map.systemNo]">
      <wd-col :span="24">
        <view class="flex">
          <view class="icon_box">
            <wd-icon name="folder" color="#0D867F"></wd-icon>
          </view>
          <text class="text-[#646874] mx-2">
            è´¨é‡è¿½æº¯å·:
            <text class="text-[#252525]">{{ data[map.systemNo] }}</text>
          </text>
        </view>
      </wd-col>
    </wd-row>
    <wd-row class="my-2">
      <wd-col :span="12">
        <view class="flex">
src/composables/useScanCode.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,249 @@
import { ref, computed, onUnmounted } from "vue";
import { onShow, onHide } from "@dcloudio/uni-app";
import RoutingInspectionApi from "@/api/routingInspection/routingInspection";
/**
 * æ‰«ç æ•°æ®æŽ¥å£
 */
interface ScanCodeData {
  uid?: string;
  deviceModel?: string;
  [key: string]: any;
}
// å…¨å±€ç›‘听器状态映射(确保每个事件名只有一个监听器)
const globalListeners = new Map<string, boolean>();
// æ·»åŠ ä¸€ä¸ªå…¨å±€çš„æ•èŽ·æ‰€æœ‰æ‰«ç äº‹ä»¶çš„ç›‘å¬å™¨ï¼ˆç”¨äºŽè°ƒè¯•ï¼‰
let debugListenerInitialized = false;
const initDebugListener = () => {
  if (debugListenerInitialized) return;
  // ç›‘听所有可能的扫码事件
  const eventNames = ["scan", "scanIndex", "scanJX", "scanLS"];
  eventNames.forEach((eventName) => {
    uni.$on(eventName, (data: any) => {
      console.log(`🔍 [全局调试] æ•获到 ${eventName} äº‹ä»¶:`, data);
    });
  });
  debugListenerInitialized = true;
  console.log("🔍 [全局调试] è°ƒè¯•监听器已初始化");
};
/**
 * æ‰«ç ç®¡ç† Composable
 * ç»Ÿä¸€ç®¡ç†æ‰«ç äº‹ä»¶ç›‘听、缓存存储和数据读取
 * å…¨å±€ç›‘听器,不随页面切换而关闭
 * @param eventName ç›‘听的事件名称,默认为 "scan"
 */
export function useScanCode(eventName: string = "scan") {
  // å½“前扫码的设备 UID
  const deviceUid = ref<string>("");
  // å½“前扫码的机台型号
  const deviceModel = ref<string>("");
  // å®Œæ•´çš„æ‰«ç æ•°æ®
  const scanData = ref<ScanCodeData>({});
  // ä¿å­˜äº‹ä»¶åç§°
  const currentEventName = eventName;
  /**
   * ä»Žæœ¬åœ°ç¼“存加载扫码数据
   */
  const loadFromCache = () => {
    try {
      const cachedData = uni.getStorageSync("scanCodeData");
      if (cachedData) {
        scanData.value = cachedData;
        deviceUid.value = cachedData.uid || "";
        deviceModel.value = cachedData.deviceModel || "";
        console.log("[useScanCode] ä»Žç¼“存加载扫码数据:", cachedData);
        return cachedData;
      }
    } catch (error) {
      console.error("[useScanCode] è¯»å–缓存失败:", error);
    }
    return null;
  };
  /**
   * ä¿å­˜æ‰«ç æ•°æ®åˆ°ç¼“å­˜
   */
  const saveToCache = (data: ScanCodeData) => {
    try {
      uni.setStorageSync("scanCodeData", data);
      console.log("[useScanCode] å·²ä¿å­˜åˆ°ç¼“å­˜:", data);
      return true;
    } catch (error) {
      console.error("[useScanCode] ä¿å­˜ç¼“存失败:", error);
      return false;
    }
  };
  /**
   * æ¸…空扫码数据和缓存
   */
  const clearScanData = () => {
    try {
      uni.removeStorageSync("scanCodeData");
      deviceUid.value = "";
      deviceModel.value = "";
      scanData.value = {};
      console.log("[useScanCode] å·²æ¸…空扫码数据");
    } catch (error) {
      console.error("[useScanCode] æ¸…空缓存失败:", error);
    }
  };
  /**
   * éªŒè¯äºŒç»´ç ï¼ˆä»…调用接口,不处理返回结果)
   */
  const validateQRCode = async (uid: string): Promise<void> => {
    try {
      console.log("[useScanCode] è°ƒç”¨éªŒè¯äºŒç»´ç æŽ¥å£, deviceUid:", uid);
      await RoutingInspectionApi.assertScanQR({ deviceUid: uid });
      console.log("[useScanCode] éªŒè¯æŽ¥å£è°ƒç”¨å®Œæˆ");
    } catch (error: any) {
      console.error("[useScanCode] éªŒè¯æŽ¥å£è°ƒç”¨å¼‚常:", error);
      // å³ä½¿å¼‚常也不影响后续流程
    }
  };
  /**
   * å¤„理扫码事件
   */
  const handleScanEvent = async (params: any) => {
    console.log(`========== [useScanCode][${currentEventName}] æ”¶åˆ°æ‰«ç äº‹ä»¶ ==========`);
    console.log(`[useScanCode][${currentEventName}] æŽ¥æ”¶å‚æ•°:`, params);
    console.log(`[useScanCode][${currentEventName}] è§¦å‘æ—¶é—´:`, new Date().toLocaleTimeString());
    try {
      if (!params?.code) {
        console.warn("[useScanCode] æ‰«ç å†…容为空");
        return;
      }
      let codeObj: ScanCodeData = {};
      try {
        codeObj = JSON.parse(params.code);
        console.log("[useScanCode] è§£æžåŽçš„对象:", codeObj);
      } catch (err) {
        console.error("[useScanCode] JSON è§£æžå¤±è´¥:", err);
        console.log("[useScanCode] åŽŸå§‹å­—ç¬¦ä¸²:", params.code);
        // å¦‚果不是 JSON,尝试作为普通字符串处理
        codeObj = { code: params.code };
      }
      // æ£€æŸ¥æ˜¯å¦æœ‰å¿…要的字段(uid æˆ– deviceModel)
      if (!codeObj.uid && !codeObj.deviceModel) {
        console.warn("[useScanCode] æ‰«ç æ•°æ®ç¼ºå°‘ uid å’Œ deviceModel,不保存缓存");
        uni.showToast({
          title: "扫码数据无效",
          icon: "none",
        });
        return;
      }
      // æ›´æ–°æœ¬åœ°çŠ¶æ€
      scanData.value = codeObj;
      deviceUid.value = codeObj.uid || "";
      deviceModel.value = codeObj.deviceModel || "";
      // ä¿å­˜åˆ°ç¼“å­˜
      saveToCache(codeObj);
      // å¦‚果有 uid,调用验证接口(不等待结果)
      if (codeObj.uid) {
        validateQRCode(codeObj.uid);
      }
      // æ˜¾ç¤ºæˆåŠŸæç¤º
      uni.showToast({
        title: "扫码成功",
        icon: "success",
      });
      console.log("[useScanCode] æ‰«ç æ•°æ®å·²æ›´æ–°:", {
        uid: deviceUid.value,
        deviceModel: deviceModel.value,
      });
    } catch (error) {
      console.error("[useScanCode] å¤„理扫码数据异常:", error);
    }
  };
  /**
   * å¯ç”¨æ‰«ç ç›‘听(全局,每次都重新注册以确保有效)
   */
  const enableListener = () => {
    // å…ˆç§»é™¤å¯èƒ½å­˜åœ¨çš„æ—§ç›‘听器
    uni.$off(currentEventName, handleScanEvent);
    // æ·»åŠ æ–°çš„ç›‘å¬å™¨
    uni.$on(currentEventName, handleScanEvent);
    // æ ‡è®°ä¸ºå…¨å±€å·²å¯ç”¨
    globalListeners.set(currentEventName, true);
    console.log(
      `[useScanCode][${currentEventName}] âœ… å…¨å±€ç›‘听器已启用/刷新(不会随页面切换关闭)`
    );
  };
  /**
   * ç¦ç”¨æ‰«ç ç›‘听(仅用于应用退出时清理,正常页面切换不调用)
   */
  const disableListener = () => {
    if (!globalListeners.get(currentEventName)) {
      console.log(`[useScanCode][${currentEventName}] ç›‘听器未启用,跳过`);
      return;
    }
    uni.$off(currentEventName, handleScanEvent);
    globalListeners.delete(currentEventName);
    console.log(`[useScanCode][${currentEventName}] âŒ å…¨å±€ç›‘听器已禁用`);
  };
  /**
   * è®¾ç½®é¡µé¢ç”Ÿå‘½å‘¨æœŸé’©å­ï¼ˆå…¨å±€ç›‘听器模式)
   * ç›‘听器全局启用一次,不随页面切换关闭
   * @param options é…ç½®é€‰é¡¹
   */
  const setupLifecycle = (
    options: {
      loadCacheOnShow?: boolean; // æ˜¯å¦åœ¨ onShow æ—¶åŠ è½½ç¼“å­˜
    } = {}
  ) => {
    const { loadCacheOnShow = true } = options;
    // åªåœ¨ onShow æ—¶åŠ è½½ç¼“å­˜ï¼Œä¸ç¦ç”¨ç›‘å¬å™¨
    if (loadCacheOnShow) {
      onShow(() => {
        console.log(`[useScanCode][${currentEventName}] onShow è§¦å‘`);
        loadFromCache();
      });
    }
    // æ³¨æ„ï¼šä¸å†åœ¨ onHide å’Œ onUnmounted ä¸­ç¦ç”¨ç›‘听器
    // ç›‘听器保持全局激活状态
  };
  // é¡µé¢åŠ è½½æ—¶è‡ªåŠ¨ä»Žç¼“å­˜è¯»å–
  loadFromCache();
  return {
    // çŠ¶æ€
    deviceUid,
    deviceModel,
    scanData,
    // è®¡ç®—属性
    hasScanned: computed(() => !!deviceUid.value),
    displayText: computed(() => deviceModel.value || "未扫码"),
    // æ–¹æ³•
    loadFromCache,
    saveToCache,
    clearScanData,
    validateQRCode,
    enableListener,
    disableListener,
    setupLifecycle,
  };
}
src/manifest.json
@@ -2,7 +2,7 @@
    "name" : "线缆上报",
    "appid" : "__UNI__F64E0A4",
    "description" : "",
    "versionName" : "1.0.12",
    "versionName" : "1.0.15",
    "versionCode" : "100",
    "transformPx" : false,
    /* 5+App特有相关 */
src/pages.json
@@ -6,7 +6,6 @@
      "^cu-(.*)": "@/components/cu-$1/index.vue"
    }
  },
  "pages": [
    {
      "path": "pages/index/index",
@@ -98,7 +97,6 @@
        "navigationBarTitleText": "个人资料"
      }
    },
    {
      "path": "pages/work/user/index",
      "style": {
@@ -284,6 +282,24 @@
      "style": {
        "navigationBarTitleText": "时效报工"
      }
    },
    {
      "path": "pages/routingInspection/index",
      "style": {
        "navigationBarTitleText": "巡检"
      }
    },
    {
      "path": "pages/routingInspection/detail/indexJX",
      "style": {
        "navigationBarTitleText": "绞线巡检详情"
      }
    },
    {
      "path": "pages/routingInspection/detail/indexLS",
      "style": {
        "navigationBarTitleText": "拉丝巡检详情"
      }
    }
  ],
  "globalStyle": {
@@ -317,4 +333,4 @@
      }
    ]
  }
}
}
src/pages/index/index.vue
@@ -18,8 +18,8 @@
    </wd-notice-bar>
    <!-- å¿«æ·å¯¼èˆª -->
    <wd-grid clickable :column="1" class="mt-2">
      <view v-for="(item, index) in navList">
        <wd-grid-item v-if="item.show" :key="index" use-slot link-type="navigateTo" :url="item.url">
      <view v-for="(item, index) in navList" :key="index">
        <wd-grid-item v-if="item.show" use-slot link-type="navigateTo" :url="item.url">
          <view class="p-2">
            <image class="w-72rpx h-72rpx rounded-8rpx" :src="item.icon" />
          </view>
@@ -87,11 +87,12 @@
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { reactive, computed } from "vue";
import { dayjs, useMessage, useToast } from "wot-design-uni";
import LogAPI, { VisitStatsVO } from "@/api/system/log";
import WorkerCallingCard from "@/components/worker-calling-card/index.vue";
import HomeApi from "@/api/home";
import { useUserStore } from "@/store/modules/user";
const visitStatsData = ref<VisitStatsVO>({
  todayUvCount: 0,
@@ -104,6 +105,22 @@
const message = useMessage();
const toast = useToast();
// èŽ·å–å½“å‰ç™»å½•ç”¨æˆ·ä¿¡æ¯
const userStore = useUserStore();
const userInfo: any = computed(() => userStore.userInfo);
// åˆ¤æ–­æ˜¯å¦ä¸ºå·¡æ£€å‘˜è§’色
const isInspector = computed(() => {
  if (!userInfo.value || !userInfo.value.roles || !Array.isArray(userInfo.value.roles)) {
    return false;
  }
  console.log(
    "userInfo.value.roles",
    userInfo.value.roles.some((role: any) => role.roleKey === "qualitative-inspector")
  );
  return userInfo.value.roles.some((role: any) => role.roleKey === "qualitative-inspector");
});
const fileProgress = reactive({
  show: false,
@@ -177,6 +194,12 @@
    url: "/pages/timely/index",
    show: false,
  },
  {
    icon: "/static/icons/routingInspection.png",
    title: "巡检",
    url: "/pages/routingInspection/index",
    show: false,
  },
]);
// åŠ è½½è®¿é—®ç»Ÿè®¡æ•°æ®
@@ -225,11 +248,17 @@
const init = async () => {
  checkVersion();
  const { data } = await HomeApi.getIndex();
  // åˆ¤æ–­æ˜¯å¦ä¸ºå·¡æ£€å‘˜è§’色
  if (data.deviceGroupName == "时效组") {
    navList[1].show = true;
  } else {
    navList[0].show = true;
  }
  if (isInspector.value) {
    // å¦‚果是巡检员,显示巡检菜单
    navList[2].show = true;
  }
};
/**
src/pages/production/components/ProductionCard.vue
@@ -1,6 +1,6 @@
<template>
  <wd-row>
    <wd-col v-for="(item, index) in data" :key="index" :span="item.span ?? 12" class="my-1">
    <wd-col v-for="(item, index) in data" :key="index" :span="24" class="my-1">
      <view class="flex w-full h-[20px]">
        <view class="text-[#646874] pl-1 mr-3">{{ item.label }}</view>
        <view class="font-medium pr-1" :style="{ color: item.color ?? color }">
src/pages/production/components/Statistics.vue
@@ -1,6 +1,6 @@
<template>
  <view class="statistics_box">
    <wd-row>
    <!-- <wd-row>
      <wd-col :span="12">
        <view class="h_48 px-4 flex items-center">
          <view class="icon_box">
@@ -19,7 +19,7 @@
          <text class="text-lg text-[#339599] ml-2 font-semibold">87%</text>
        </view>
      </wd-col>
    </wd-row>
    </wd-row> -->
  </view>
</template>
<style scoped lang="scss">
src/pages/production/detail/twistDetail.vue
@@ -5,6 +5,7 @@
      :map="{
        deviceModel: 'deviceModel',
        model: 'model',
        systemNo: 'systemNo',
        totalAmount: 'totalAmount',
        amount: 'amount',
        unAmount: 'unAmount',
@@ -50,6 +51,7 @@
const cardData = reactive({
  deviceModel: undefined,
  model: undefined,
  systemNo: undefined,
  totalAmount: undefined,
  amount: undefined,
  unAmount: undefined,
@@ -61,6 +63,7 @@
  });
  cardData.deviceModel = data.deviceModel;
  cardData.model = data.model;
  cardData.systemNo = data.systemNo;
  cardData.totalAmount = data.totalLength;
  cardData.amount = data.length;
  cardData.unAmount = data.unLength;
src/pages/production/detail/wireDetail.vue
@@ -5,6 +5,7 @@
      :map="{
        deviceModel: 'deviceModel',
        model: 'model',
        systemNo: 'systemNo',
        totalAmount: 'totalAmount',
        amount: 'amount',
        unAmount: 'unAmount',
@@ -50,6 +51,7 @@
const cardData = reactive({
  deviceModel: undefined,
  model: undefined,
  systemNo: undefined,
  totalAmount: undefined,
  amount: undefined,
  unAmount: undefined,
@@ -62,6 +64,7 @@
  });
  cardData.deviceModel = data.deviceModel;
  cardData.model = data.model;
  cardData.systemNo = data.systemNo;
  cardData.totalAmount = data.totalAmount;
  cardData.amount = data.amount;
  cardData.unAmount = data.unAmount;
src/pages/production/index.vue
@@ -2,7 +2,14 @@
  <view>
    <wd-row>
      <wd-col :span="21">
        <wd-search placeholder-left hide-cancel></wd-search>
        <wd-search
          v-model="searchKeyword"
          placeholder="请输入规格型号"
          placeholder-left
          hide-cancel
          @search="handleSearch"
          @clear="handleClear"
        ></wd-search>
      </wd-col>
      <wd-col :span="3">
        <view class="scan_box" @click="openScan">
@@ -14,16 +21,30 @@
      <wd-tab :title="`待生产(${total.wait})`" class="tab_bg">
        <ProductList
          ref="waitRef"
          :key="`wait-${searchKey}`"
          :api="ManageApi.getProductList"
          state="待完成"
          :model="searchKeyword"
          @ok="changeWait"
        />
      </wd-tab>
      <wd-tab :title="`部分完成(${total.partial})`" class="tab_bg">
        <ProductList
          ref="partialRef"
          :key="`partial-${searchKey}`"
          :api="ManageApi.getProductList"
          state="部分完成"
          :model="searchKeyword"
          @ok="changePartial"
        />
      </wd-tab>
      <wd-tab :title="`已生产(${total.already})`" class="tab_bg">
        <ProductList
          ref="alreadyRef"
          :key="`already-${searchKey}`"
          :api="ManageApi.getProductList"
          state="已完成"
          :model="searchKeyword"
          @ok="changeAlready"
        />
      </wd-tab>
@@ -48,10 +69,14 @@
const toast = useToast();
const waitRef = ref();
const partialRef = ref();
const alreadyRef = ref();
const tab = ref<number>(0);
const searchKeyword = ref("");
const searchKey = ref(0);
const total = reactive({
  wait: 0,
  partial: 0,
  already: 0,
});
@@ -59,10 +84,23 @@
  total.wait = num;
};
const changePartial = (num: number) => {
  total.partial = num;
};
const changeAlready = (num: number) => {
  total.already = num;
};
const handleSearch = () => {
  searchKey.value++;
};
const handleClear = () => {
  searchKeyword.value = "";
  handleSearch();
};
const openScan = () => {
  scanRef.value.triggerScan();
};
src/pages/production/list/index.vue
@@ -36,6 +36,7 @@
const map = reactive({
  deviceModel: "deviceModel",
  model: "model",
  systemNo: "systemNo",
  totalAmount: "totalAmount",
  amount: "amount",
  unAmount: "unAmount",
@@ -50,13 +51,17 @@
    type: String,
    default: "",
  },
  model: {
    type: String,
    default: "",
  },
});
const emits = defineEmits(["ok"]);
const list = ref<any[]>([]);
const toDetail = (id: number, type: string) => {
  toast.show("点击卡片");
  // toast.show("点击卡片");
  if (type == "拉丝") {
    uni.navigateTo({
      url: `/pages/production/detail/wireDetail?id=${id}`,
@@ -69,22 +74,28 @@
};
const getList = async (pageNo: number, pageSize: number) => {
  const { code, data } = await props.api({
  const params: any = {
    userName: userInfo.value.userName,
    state: props.state,
    current: pageNo,
    size: pageSize,
  });
  };
  if (props.model) {
    params.model = props.model;
  }
  const { code, data } = await props.api(params);
  if (code == 200) {
    if (data.type == "绞线") {
      map.deviceModel = "deviceModel";
      map.model = "model";
      map.systemNo = "systemNo";
      map.totalAmount = "totalLength";
      map.amount = "length";
      map.unAmount = "unLength";
    } else if (data.type == "拉丝") {
      map.deviceModel = "deviceModel";
      map.model = "model";
      map.systemNo = "systemNo";
      map.totalAmount = "totalAmount";
      map.amount = "amount";
      map.unAmount = "unAmount";
src/pages/production/twist/attachment/index.vue
@@ -17,26 +17,54 @@
    <view class="attachment-list">
      <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="暂无附件" />
      <wd-card
        v-for="item in attachmentList"
        :key="item.id"
        type="rectangle"
        custom-class="attachment-card"
        :border="false"
      >
        <view class="attachment-item" @click="previewAttachment(item)">
          <view class="attachment-info">
            <view class="attachment-name">{{ item.bucketFileName || item.name }}</view>
            <view class="attachment-meta">
              <text class="file-type">{{ getFileType(item.bucketFileName) }}</text>
              <text class="upload-time">{{ formatTime(item.createTime) }}</text>
      <view v-for="item in attachmentList" :key="item.id" class="attachment-card">
        <view class="media-wrapper" @click="previewAttachment(item)">
          <!-- å›¾ç‰‡é¢„览 -->
          <template v-if="isImageType(item.url)">
            <image
              v-if="!item.loadError"
              :src="getFullUrl(item.url)"
              mode="aspectFill"
              class="media-preview"
              @error="onImageError(item)"
              @load="onImageLoad(item)"
            />
            <!-- å›¾ç‰‡åŠ è½½å¤±è´¥æ˜¾ç¤ºé»˜è®¤å›¾æ ‡ -->
            <view v-else class="file-icon-wrapper">
              <wd-icon name="picture" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- è§†é¢‘预览 -->
          <template v-else-if="isVideoType(item.url)">
            <video
              v-if="!item.loadError"
              :src="getFullUrl(item.url)"
              class="media-preview"
              :controls="false"
              :show-center-play-btn="false"
              @error="onVideoError(item)"
            />
            <!-- è§†é¢‘加载失败显示默认图标 -->
            <view v-else class="file-icon-wrapper">
              <wd-icon name="video" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- å…¶ä»–文件类型显示图标 -->
          <view v-else class="file-icon-wrapper">
            <wd-icon name="file-outline" size="48px" color="#999" />
            <text class="file-name">文件</text>
          </view>
          <view class="attachment-actions" @click.stop>
            <wd-icon name="delete" color="#ff4757" @click="deleteAttachment(item.id)" />
          <!-- åˆ é™¤æŒ‰é’® -->
          <view class="delete-btn" @click.stop="deleteAttachment(item.id)">
            <wd-icon name="delete" color="#fff" size="20px" />
          </view>
        </view>
      </wd-card>
      </view>
    </view>
    <wd-toast />
@@ -48,6 +76,12 @@
import { useToast } from "wot-design-uni";
import AttachmentAPI from "@/api/product/attachment";
// H5 ä½¿ç”¨ VITE_APP_BASE_API ä½œä¸ºä»£ç†è·¯å¾„,其他平台使用 VITE_APP_API_URL ä½œä¸ºè¯·æ±‚路径
let baseUrl = import.meta.env.VITE_APP_API_URL;
// #ifdef H5
baseUrl = import.meta.env.VITE_APP_BASE_API;
// #endif
const toast = useToast();
// é¡µé¢å‚æ•°
@@ -56,6 +90,57 @@
const attachmentList = ref<any[]>([]);
const detailData = ref<any>({});
// èŽ·å–å®Œæ•´çš„å›¾ç‰‡/视频 URL
const getFullUrl = (url: string) => {
  if (!url) return "";
  // å¦‚果已经是完整的 URL(http æˆ– https å¼€å¤´ï¼‰ï¼Œç›´æŽ¥è¿”回
  if (url.startsWith("http://") || url.startsWith("https://")) {
    return url;
  }
  // å¦‚果是相对路径,拼接基础 URL
  return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
};
// ä»Ž URL æˆ–文件名中提取扩展名
const getExtension = (urlOrFileName: string) => {
  if (!urlOrFileName) return "";
  // ç§»é™¤æŸ¥è¯¢å‚数和哈希
  const cleanUrl = urlOrFileName.split("?")[0].split("#")[0];
  // èŽ·å–æœ€åŽä¸€ä¸ªç‚¹åŽé¢çš„å†…å®¹
  const extension = cleanUrl.split(".").pop()?.toLowerCase();
  return extension || "";
};
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡ç±»åž‹
const isImageType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(extension);
};
// åˆ¤æ–­æ˜¯å¦ä¸ºè§†é¢‘类型
const isVideoType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["mp4", "mov", "avi", "wmv", "flv", "mkv", "webm"].includes(extension);
};
// å›¾ç‰‡åŠ è½½æˆåŠŸ
const onImageLoad = (item: any) => {
  item.loadError = false;
};
// å›¾ç‰‡åŠ è½½å¤±è´¥
const onImageError = (item: any) => {
  console.error("图片加载失败:", item.url);
  item.loadError = true;
};
// è§†é¢‘加载失败
const onVideoError = (item: any) => {
  console.error("视频加载失败:", item.url);
  item.loadError = true;
};
// èŽ·å–é™„ä»¶åˆ—è¡¨
const getAttachmentList = async (data: any) => {
  try {
@@ -252,24 +337,24 @@
// é¢„览附件
const previewAttachment = (item: any) => {
  // æ ¹æ®æ–‡ä»¶ç±»åž‹è¿›è¡Œé¢„览
  const fileName = item.bucketFileName || item.name;
  const fileType = getFileType(fileName);
  const fileType = getFileType(item.url);
  const fullUrl = getFullUrl(item.url);
  if (fileType.startsWith("image")) {
    // å›¾ç‰‡é¢„览
    uni.previewImage({
      urls: [item.url],
      current: item.url,
      urls: [fullUrl],
      current: fullUrl,
    });
  } else {
    // å…¶ä»–文件类型,可以下载或打开
    uni.downloadFile({
      url: item.url,
      url: fullUrl,
      success: (res) => {
        uni.openDocument({
          filePath: res.tempFilePath,
          success: () => {
            console.log("打开文档成功");
            // æ‰“开文档成功
          },
          fail: (error) => {
            console.error("打开文档失败:", error);
@@ -286,9 +371,9 @@
};
// èŽ·å–æ–‡ä»¶ç±»åž‹
const getFileType = (fileName: string) => {
  if (!fileName) return "unknown";
  const extension = fileName.split(".").pop()?.toLowerCase();
const getFileType = (urlOrFileName: string) => {
  if (!urlOrFileName) return "unknown";
  const extension = getExtension(urlOrFileName);
  switch (extension) {
    case "jpg":
    case "jpeg":
@@ -297,6 +382,14 @@
    case "bmp":
    case "webp":
      return "image";
    case "mp4":
    case "mov":
    case "avi":
    case "wmv":
    case "flv":
    case "mkv":
    case "webm":
      return "video";
    case "pdf":
      return "pdf";
    case "doc":
@@ -357,43 +450,69 @@
}
.attachment-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  .attachment-card {
    margin-bottom: 12px;
    border-radius: 4px;
    width: 100%;
    aspect-ratio: 1;
  }
}
.attachment-item {
  display: flex;
  align-items: center;
  padding: 12px;
.media-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
  .attachment-info {
    flex: 1;
  .media-preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
    .attachment-name {
      font-size: 16px;
      font-weight: 500;
      color: #333;
      margin-bottom: 4px;
      word-break: break-all;
    }
  .file-icon-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding: 8px;
    text-align: center;
    .attachment-meta {
      display: flex;
      gap: 12px;
    .file-name {
      margin-top: 8px;
      font-size: 12px;
      color: #999;
      color: #666;
      word-break: break-all;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
      &.error-text {
        color: #ff4757;
      }
    }
  }
  .attachment-actions {
    margin-left: 12px;
    :deep(.wd-icon) {
      font-size: 20px;
      cursor: pointer;
    }
  .delete-btn {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10;
  }
}
</style>
src/pages/production/twist/components/MonofilCard.vue
@@ -1,21 +1,153 @@
<template>
  <wd-card>
    <wd-cell-group :border="true">
      <wd-cell title="单丝编号" :value="data.monofilamentNumber" />
      <wd-cell title="理论长度" :value="data.amount + ' (m)'" />
      <wd-cell title="生产长度" :value="data.actuallyLength + ' (m)'" />
      <wd-cell title="重量" :value="data.actuallyWeight + ' (kg)'" />
    </wd-cell-group>
  </wd-card>
  <view class="swipe-container">
    <view
      class="swipe-content"
      :style="{ transform: `translateX(${translateX}px)` }"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    >
      <wd-card>
        <wd-cell-group :border="true">
          <wd-cell title="单丝编号" :value="data.monofilamentNumber" />
          <wd-cell title="理论长度" :value="data.amount + ' (m)'" />
          <wd-cell title="生产长度" :value="data.actuallyLength + ' (m)'" />
          <wd-cell title="重量" :value="data.actuallyWeight + ' (kg)'" />
        </wd-cell-group>
      </wd-card>
    </view>
    <view class="swipe-delete" @click="handleDelete">
      <text class="delete-text">删除</text>
    </view>
  </view>
</template>
<script setup lang="ts">
defineProps({
import { ref } from "vue";
const props = defineProps({
  data: {
    type: Object,
    default: () => {},
  },
});
const emit = defineEmits(["delete", "swipe-open"]);
const translateX = ref(0);
const startX = ref(0);
const startY = ref(0);
const currentX = ref(0);
const isSwipeOpen = ref(false);
const deleteWidth = 80; // åˆ é™¤æŒ‰é’®å®½åº¦
const isHorizontalSwipe = ref(false);
const handleTouchStart = (e: any) => {
  startX.value = e.touches[0].clientX;
  startY.value = e.touches[0].clientY;
  currentX.value = translateX.value;
  isHorizontalSwipe.value = false;
};
const handleTouchMove = (e: any) => {
  const moveX = e.touches[0].clientX - startX.value;
  const moveY = e.touches[0].clientY - startY.value;
  // åˆ¤æ–­æ˜¯å¦ä¸ºæ°´å¹³æ»‘动(水平移动距离大于垂直移动距离)
  if (!isHorizontalSwipe.value && Math.abs(moveX) > Math.abs(moveY) && Math.abs(moveX) > 10) {
    isHorizontalSwipe.value = true;
  }
  // åªæœ‰æ°´å¹³æ»‘动时才处理删除滑动
  if (isHorizontalSwipe.value) {
    e.stopPropagation();
    const newTranslateX = currentX.value + moveX;
    // é™åˆ¶æ»‘动范围:只能向左滑动,最大滑动距离为删除按钮宽度
    if (newTranslateX <= 0 && newTranslateX >= -deleteWidth) {
      translateX.value = newTranslateX;
    } else if (newTranslateX < -deleteWidth) {
      translateX.value = -deleteWidth;
    } else if (newTranslateX > 0) {
      translateX.value = 0;
    }
  }
};
const handleTouchEnd = (e: any) => {
  // åªæœ‰æ°´å¹³æ»‘动时才处理结束逻辑
  if (isHorizontalSwipe.value) {
    e.stopPropagation();
    // åˆ¤æ–­æ˜¯å¦åº”该打开或关闭删除按钮
    if (translateX.value < -deleteWidth / 2) {
      // æ»‘动超过一半,打开删除按钮
      translateX.value = -deleteWidth;
      isSwipeOpen.value = true;
      emit("swipe-open", props.data);
    } else {
      // æ»‘动不足一半,关闭删除按钮
      translateX.value = 0;
      isSwipeOpen.value = false;
    }
  }
  isHorizontalSwipe.value = false;
};
const handleDelete = () => {
  // å…ˆå…³é—­æ»‘动
  translateX.value = 0;
  isSwipeOpen.value = false;
  emit("delete", props.data);
};
// å…³é—­æ»‘动的方法,供外部调用
const closeSwipe = () => {
  if (isSwipeOpen.value) {
    translateX.value = 0;
    isSwipeOpen.value = false;
  }
};
// æš´éœ²æ–¹æ³•供父组件调用
defineExpose({
  closeSwipe,
  isSwipeOpen,
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.swipe-container {
  position: relative;
  overflow: hidden;
  margin-bottom: 8px;
}
.swipe-content {
  position: relative;
  transition: transform 0.3s ease;
  z-index: 2;
  background: #fff;
  touch-action: pan-y;
}
.swipe-delete {
  position: absolute;
  right: 0;
  top: 12px;
  bottom: 12px;
  width: 80px;
  background: #ff4444;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1;
  border-radius: 4px;
  box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
  .delete-text {
    color: #fff;
    font-size: 14px;
    font-weight: 500;
  }
}
</style>
src/pages/production/twist/components/TwistReportCard.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
<template>
  <wd-row>
    <wd-col v-for="(item, index) in data" :key="index" :span="item.span || 24" class="my-1">
      <view class="flex w-full h-[20px]">
        <view class="text-[#646874] pl-1 mr-3">{{ item.label }}</view>
        <view class="font-medium pr-1" :style="{ color: item.color ?? color }">
          {{ value[item.prop] }} {{ value[item.unitProp] }} {{ item.unit }}
        </view>
      </view>
    </wd-col>
  </wd-row>
</template>
<script lang="ts" setup>
defineProps({
  data: {
    type: Array as any,
    default: () => {
      return [];
    },
  },
  value: {
    type: Object,
    default: () => {
      return {};
    },
  },
  color: {
    type: String,
    default: "#333333",
  },
  unit: {
    type: String,
    default: "",
  },
});
</script>
src/pages/production/twist/receive/index.vue
@@ -1,16 +1,16 @@
<template>
  <wd-tabs v-model="tab" auto-line-width>
    <wd-tab title="单丝领用" name="单丝领用">
      <Monofil />
      <Monofil v-if="tab === '单丝领用'" />
    </wd-tab>
    <wd-tab title="盘具领用" name="盘具领用">
      <view class="content">
        <Plate />
        <Plate v-if="tab === '盘具领用'" />
      </view>
    </wd-tab>
    <wd-tab title="钢芯领用" name="钢芯领用">
    <wd-tab title="芯线领用" name="芯线领用">
      <view class="content">
        <SteelCore />
        <SteelCore v-if="tab === '芯线领用'" />
      </view>
    </wd-tab>
  </wd-tabs>
src/pages/production/twist/receive/monofil.vue
@@ -25,7 +25,12 @@
        <block v-for="item in nodeList" :key="item">
          <wd-tab :title="item.twistedLayer" :name="item.twistedLayer">
            <scroll-view class="content" scroll-y>
              <MonofilCard v-for="(m, i) in item.strandedWireDish" :key="i" :data="m" />
              <MonofilCard
                v-for="(m, i) in item.strandedWireDish"
                :key="i"
                :data="m"
                @delete="handleDeleteCard(item, m)"
              />
            </scroll-view>
          </wd-tab>
        </block>
@@ -79,7 +84,7 @@
import MonofilCard from "../components/MonofilCard.vue";
import StatisticsModal from "../components/StatisticsModal.vue";
import { useToast } from "wot-design-uni";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import { onLoad, onUnload, onShow, onHide } from "@dcloudio/uni-app";
import Scan from "@/components/scan/index.vue";
import ManageApi from "@/api/product/manage";
import TwistApi from "@/api/product/twist";
@@ -95,20 +100,21 @@
const showStatisticsModal = ref(false);
const showManualInput = ref(false);
const manualOutPutId = ref("");
const isPageVisible = ref(false); // æ ‡è®°é¡µé¢æ˜¯å¦å¯è§
// ç›‘听标签切换
watch(tab, () => {
  if (tab.value) {
    console.log("tab.value:===========1", tab.value);
    getList();
  }
});
const getScanCode = async (code: any) => {
  console.log("自定义扫描的结果回调函数:", code);
  // let parseData = code.trim();
  console.log("code:===========", JSON.parse(code.code));
  console.log("id:=============", JSON.parse(code.code).id);
  // æ£€æŸ¥é¡µé¢æ˜¯å¦å¯è§ï¼Œå¦‚果不可见则不处理扫码数据
  if (!isPageVisible.value) {
    return;
  }
  try {
    // æ£€æŸ¥æ˜¯å¦å·²é€‰æ‹©æ ‡ç­¾
    if (!tab.value) {
@@ -117,24 +123,25 @@
    }
    // æ‰¾åˆ°å½“前选中的层
    console.log("tab.value:===========2", tab.value);
    const currentLayer = nodeList.value.find((node) => node.twistedLayer === tab.value);
    if (!currentLayer) {
      toast.error("未找到当前选中的层");
      return;
    }
    console.log("tab.value:===========3", currentLayer);
    // åœ¨å‘起请求前,先校验该单丝是否已在当前或其他层级被领用
    const scannedOutputId = JSON.parse(code.code).id;
    const alreadyUsed = nodeList.value.some((node) =>
      (node.strandedWireDish || []).some((item: any) => item.outputId === scannedOutputId)
    );
    if (alreadyUsed) {
      toast.error("该单丝已领用,请勿重复扫码");
      return;
    // è§£æžæ‰«ç æ•°æ®
    const scanData = JSON.parse(code.code);
    // åˆ¤æ–­å±‚级是否匹配
    if (scanData.layer && scanData.layer !== currentLayer.twistedLayer) {
      toast.error(
        `领用层级不对,当前层是:${currentLayer.twistedLayer},领用单丝层是:${scanData.layer}`
      );
      // return;
    }
    const { data } = await TwistApi.getScarn({
      outPutId: scannedOutputId,
      outPutId: scanData.id,
      twistId: currentLayer.twistId,
    });
@@ -268,7 +275,6 @@
    // è®¾ç½®é»˜è®¤ç¬¬ä¸€å±‚
    if (nodeList.value && nodeList.value.length > 0 && !tab.value) {
      tab.value = nodeList.value[0].twistedLayer;
      console.log("设置默认第一层:", tab.value);
      // è®¾ç½®é»˜è®¤æ ‡ç­¾åŽï¼ŒåŠ è½½ç¬¬ä¸€å±‚çš„æ•°æ®
      getList();
    }
@@ -344,6 +350,44 @@
  }
};
// åˆ é™¤å¡ç‰‡
const handleDeleteCard = async (layer: any, cardData: any) => {
  // æ˜¾ç¤ºç¡®è®¤æç¤º
  uni.showModal({
    title: "提示",
    content: "确定要删除该单丝吗?",
    success: async (res) => {
      if (res.confirm) {
        try {
          // å¦‚果有id,调用接口删除
          if (cardData.id !== undefined && cardData.id !== null) {
            const { code, msg } = await TwistApi.deleteStrandedWireDish(cardData.id);
            if (code !== 200) {
              toast.error(msg || "删除失败");
              return;
            }
          }
          // å‰ç«¯ç›´æŽ¥åˆ é™¤ï¼ˆæ— è®ºæ˜¯å¦æœ‰id,都从前端删除)
          if (layer.strandedWireDish && Array.isArray(layer.strandedWireDish)) {
            const index = layer.strandedWireDish.findIndex(
              (item: any) => item.monofilamentNumber === cardData.monofilamentNumber
            );
            if (index !== -1) {
              layer.strandedWireDish.splice(index, 1);
              toast.success("删除成功");
              // åˆ·æ–°å½“前层的数据显示
              getList();
            }
          }
        } catch (error: any) {
          toast.error(error.msg || "删除失败");
        }
      }
    },
  });
};
onLoad(async (options: any) => {
  // å¼€å¯å¹¿æ’­ç›‘听事件
  uni.$on("scanMono", getScanCode);
@@ -353,9 +397,21 @@
  getRootNumber(options.id);
  // getRootNumber(118);
});
onShow(() => {
  // é¡µé¢æ˜¾ç¤ºæ—¶æ ‡è®°ä¸ºå¯è§
  isPageVisible.value = true;
});
onHide(() => {
  // é¡µé¢éšè—æ—¶æ ‡è®°ä¸ºä¸å¯è§
  isPageVisible.value = false;
});
onUnload(() => {
  // å¼€å¯å¹¿æ’­ç›‘听事件
  // å–消广播监听事件
  uni.$off("scanMono", getScanCode);
  isPageVisible.value = false;
});
</script>
src/pages/production/twist/receive/steelCore/edit.vue
@@ -1,6 +1,6 @@
<template>
  <view>
    <CardTitle title="绞线钢芯领用" :hideAction="false" />
    <CardTitle title="绞线芯线领用" :hideAction="false" />
    <SteelCoreForm ref="formRef" class="mx-4" />
    <view class="footer">
      <wd-button
@@ -37,13 +37,10 @@
// æŽ¥æ”¶åˆ—表页传递的数据
const receiveEditData = (data: any) => {
  console.log("receiveEditData æŽ¥æ”¶åˆ°çš„æ•°æ®:", data);
  if (data && formRef.value) {
    // ç¡®ä¿ list å’Œ editId éƒ½å­˜åœ¨
    if (data.list && data.editId) {
      formRef.value.setFormData(data.list, data.editId);
    } else {
      console.error("数据格式错误:", data);
    }
  }
};
src/pages/production/twist/receive/steelCore/form.vue
@@ -1,6 +1,16 @@
<template>
  <wd-form ref="form" :model="model" class="relative form_box">
    <wd-cell-group :border="true">
      <wd-picker
        v-model="diskMaterialValue"
        :columns="diskMaterialOptions"
        label="芯线类型"
        label-width="100px"
        prop="diskMaterial"
        placeholder="请选择芯线类型"
        clearable
        @confirm="handleDiskMaterialChange"
      />
      <wd-input
        v-model="model.model"
        label="规格型号"
@@ -48,6 +58,7 @@
<script lang="ts" setup>
import useFormData from "@/hooks/useFormData";
import TwistApi from "@/api/product/twist";
import ManageApi from "@/api/product/manage";
import { useToast } from "wot-design-uni";
const props = defineProps({
@@ -59,6 +70,10 @@
    type: Object,
    default: null,
  },
  wireId: {
    type: [String, Number],
    default: undefined,
  },
});
const emits = defineEmits(["refresh"]);
@@ -67,6 +82,7 @@
const allListData = ref<any[]>([]); // å­˜å‚¨å®Œæ•´åˆ—表数据
const toast = useToast();
const { form: model } = useFormData({
  diskMaterial: undefined, // èŠ¯çº¿ç±»åž‹
  model: undefined, // è§„格型号
  monofilamentNumber: undefined, // æ ·å“ç¼–号
  amount: undefined, // æ•°é‡
@@ -75,11 +91,45 @@
  type: "钢芯",
});
// èŠ¯çº¿ç±»åž‹å­—å…¸æ•°æ®
const diskMaterialOptions = ref<Array<{ label: string; value: string }>>([]);
const diskMaterialValue = ref("");
// åŠ è½½èŠ¯çº¿ç±»åž‹å­—å…¸æ•°æ®
const loadDiskMaterialDict = async () => {
  try {
    const res = await ManageApi.dictAPI("core_wire_type");
    if (res.data && Array.isArray(res.data)) {
      diskMaterialOptions.value = res.data.map((item: any) => ({
        label: item.dictLabel || "",
        value: item.dictValue || "",
      }));
    }
  } catch (error) {
    // åŠ è½½å­—å…¸å¤±è´¥ï¼Œé™é»˜å¤„ç†
  }
};
// å¤„理芯线类型选择
const handleDiskMaterialChange = (val: any) => {
  model.diskMaterial = val.value;
};
// ç›‘听 model.diskMaterial å˜åŒ–,同步选择器显示
watch(
  () => model.diskMaterial,
  (newValue) => {
    diskMaterialValue.value = newValue || "";
  },
  { immediate: true }
);
// æ–°å¢žæäº¤
const submit = async () => {
  const currentWireId = props.wireId || paramsId.value;
  const { code } = await TwistApi.addStrandedWireDish([
    {
      wireId: paramsId.value,
      wireId: currentWireId,
      ...model,
    },
  ]);
@@ -107,6 +157,7 @@
      // ä¿ç•™åŽŸæœ‰æ•°æ®ï¼Œç„¶åŽæ›´æ–°ä¿®æ”¹çš„å­—æ®µ
      const updatedItem = {
        ...item, // å…ˆä¿ç•™åŽŸæœ‰çš„æ‰€æœ‰æ•°æ®
        diskMaterial: model.diskMaterial,
        model: model.model,
        monofilamentNumber: model.monofilamentNumber,
        amount: model.amount,
@@ -133,7 +184,6 @@
const setFormData = (list: any[], currentEditId: number) => {
  // å®‰å…¨æ£€æŸ¥ï¼šç¡®ä¿list是数组
  if (!Array.isArray(list)) {
    console.error("setFormData: list å‚数不是数组", list);
    return;
  }
@@ -144,12 +194,15 @@
  // æ‰¾åˆ°å½“前编辑项并回显到表单
  const currentItem = list.find((item) => item.id === currentEditId);
  if (currentItem) {
    model.diskMaterial = currentItem.diskMaterial;
    model.model = currentItem.model;
    model.monofilamentNumber = currentItem.monofilamentNumber;
    model.amount = currentItem.amount;
    model.weight = currentItem.weight;
    model.supplier = currentItem.supplier;
    model.type = currentItem.type || "钢芯";
    // è®¾ç½®èŠ¯çº¿ç±»åž‹çš„å›žæ˜¾å€¼
    diskMaterialValue.value = currentItem.diskMaterial || "";
  }
};
@@ -158,12 +211,14 @@
  () => props.editData,
  (newData) => {
    if (newData && props.mode === "edit") {
      model.diskMaterial = newData.diskMaterial || "";
      model.model = newData.model || "";
      model.monofilamentNumber = newData.monofilamentNumber || "";
      model.amount = newData.amount || "";
      model.weight = newData.weight || "";
      model.supplier = newData.supplier || "";
      model.type = newData.type || "钢芯";
      diskMaterialValue.value = newData.diskMaterial || "";
    }
  },
  { immediate: true, deep: true }
@@ -171,16 +226,36 @@
// é‡ç½®è¡¨å•数据
const resetFormData = () => {
  model.diskMaterial = undefined;
  model.model = undefined;
  model.monofilamentNumber = undefined;
  model.amount = undefined;
  model.weight = undefined;
  model.supplier = undefined;
  model.type = "钢芯";
  diskMaterialValue.value = "";
};
// å¡«å……表单数据(用于扫码后回显)
const fillFormData = (data: any) => {
  if (data) {
    model.diskMaterial = data.diskMaterial || "";
    model.model = data.model || "";
    model.monofilamentNumber = data.monofilamentNumber || "";
    model.amount = data.oneLength || data.amount || "";
    model.weight = data.weight || "";
    model.supplier = data.supplier || "";
    model.type = data.type || "钢芯";
    diskMaterialValue.value = data.diskMaterial || "";
  }
};
onLoad((options: any) => {
  paramsId.value = options.id;
});
onMounted(async () => {
  await loadDiskMaterialDict();
});
defineExpose({
@@ -188,6 +263,7 @@
  submitEdit,
  setFormData,
  resetFormData,
  fillFormData,
});
</script>
src/pages/production/twist/receive/steelCore/index.vue
@@ -8,7 +8,12 @@
      @query="getList"
    >
      <template #top>
        <CardTitle title="钢芯领用" :hideAction="true" :full="false" @action="addReport" />
        <CardTitle title="芯线领用" :hideAction="false" :full="false">
          <template #action>
            <wd-button type="icon" icon="scan" color="#0D867F" @click="openScan"></wd-button>
            <wd-button type="icon" icon="add-circle" color="#0D867F" @click="addReport"></wd-button>
          </template>
        </CardTitle>
      </template>
      <wd-card v-for="(item, index) in cardList" :key="index" type="rectangle" custom-class="round">
        <template #title>
@@ -28,7 +33,7 @@
        <wd-button type="text" @click="cancelAdd">取消</wd-button>
        <wd-button type="text" @click="submitAdd">确定</wd-button>
      </view>
      <SteelCore ref="addFormRef" mode="add" @refresh="reloadList" />
      <SteelCore ref="addFormRef" mode="add" :wireId="paramsId" @refresh="reloadList" />
    </wd-popup>
    <wd-popup v-model="editDialog.visible" position="bottom" custom-class="yl-popup">
      <view class="action px-3">
@@ -38,10 +43,12 @@
      <SteelCore
        ref="editFormRef"
        mode="edit"
        :wireId="paramsId"
        :editData="editDialog.currentItem"
        @refresh="reloadList"
      />
    </wd-popup>
    <Scan ref="scanRef" emitName="scanSteelCore" />
    <wd-toast />
  </view>
</template>
@@ -51,15 +58,19 @@
import ProductionCard from "../../../components/ProductionCard.vue";
import { useToast } from "wot-design-uni";
import SteelCore from "./form.vue";
import { onLoad } from "@dcloudio/uni-app";
import { onLoad, onUnload, onShow, onHide } from "@dcloudio/uni-app";
import ManageApi from "@/api/product/manage";
import TwistApi from "@/api/product/twist";
import zPaging from "@/components/z-paging/z-paging.vue";
import Scan from "@/components/scan/index.vue";
const paramsId = ref();
const pagingRef = ref();
const addFormRef = ref();
const editFormRef = ref();
const scanRef = ref();
const toast = useToast();
const isPageVisible = ref(false); // æ ‡è®°é¡µé¢æ˜¯å¦å¯è§
const addDialog = reactive({
  visible: false,
});
@@ -147,8 +158,69 @@
  pagingRef.value.refresh();
};
// æ‰«ç ç›¸å…³æ–¹æ³•
const openScan = () => {
  scanRef.value.triggerScan();
};
const getScanCode = async (code: any) => {
  // æ£€æŸ¥é¡µé¢æ˜¯å¦å¯è§ï¼Œå¦‚果不可见则不处理扫码数据
  if (!isPageVisible.value) {
    return;
  }
  try {
    const parseData = JSON.parse(code.code);
    // æ£€æŸ¥å¿…需字段:model、supplier、diskMaterial
    const requiredFields = ["model", "supplier", "diskMaterial"];
    const missingFields = requiredFields.filter((field) => !parseData[field]);
    if (missingFields.length > 0) {
      toast.error(`二维码错误,请更换二维码!`);
      return;
    }
    // æ‰“开新增弹框并填充扫码获取的信息
    addDialog.visible = true;
    // ç­‰å¾…弹框打开后填充表单数据
    // ä½¿ç”¨åŒé‡ç­‰å¾…:nextTick + setTimeout ç¡®ä¿ç»„件已完全挂载
    nextTick(() => {
      setTimeout(() => {
        if (addFormRef.value) {
          addFormRef.value.fillFormData(parseData);
          toast.success("扫码成功,请确认信息");
        } else {
          toast.error("表单加载失败,请重试");
        }
      }, 200); // å»¶è¿Ÿ200ms确保弹框和组件已完全渲染
    });
  } catch (error) {
    toast.error("二维码异常,请更换二维码!");
  }
};
onLoad((options: any) => {
  // å¼€å¯å¹¿æ’­ç›‘听事件
  uni.$on("scanSteelCore", getScanCode);
  paramsId.value = options.id;
});
onShow(() => {
  // é¡µé¢æ˜¾ç¤ºæ—¶æ ‡è®°ä¸ºå¯è§
  isPageVisible.value = true;
});
onHide(() => {
  // é¡µé¢éšè—æ—¶æ ‡è®°ä¸ºä¸å¯è§
  isPageVisible.value = false;
});
onUnload(() => {
  // å–消广播监听事件
  uni.$off("scanSteelCore", getScanCode);
  isPageVisible.value = false;
});
</script>
@@ -169,4 +241,8 @@
  display: flex;
  justify-content: space-between;
}
:deep(.wd-button__content) {
  color: #0d867f;
}
</style>
src/pages/production/twist/report/draw.vue
@@ -62,7 +62,7 @@
            </template>
          </view>
        </wd-tab>
        <wd-tab title="钢芯领用自检" name="steel">
        <wd-tab title="芯线领用自检" name="steel">
          <view class="form-section">
            <wd-form :model="localSteelData">
              <wd-form-item label="规格型号" prop="model" required>
src/pages/production/twist/report/form.vue
@@ -2,103 +2,139 @@
  <wd-form ref="form" :model="model" class="relative form_box">
    <wd-cell-group :border="true">
      <wd-input
        v-model="model.contractNo"
        label="领用杆号"
        label-width="100px"
        prop="contractNo"
        clearable
        placeholder="请输入领用杆号"
      />
      <wd-input
        v-model="model.status"
        label="杆重(kg)"
        label-width="100px"
        prop="status"
        clearable
        placeholder="请输入杆重"
      />
      <wd-input
        v-model="model.clientName"
        label="单丝盘号"
        label-width="100px"
        prop="clientName"
        clearable
        placeholder="请输入单丝盘号"
      />
      <wd-input
        v-model="model.workbench"
        label="实际重量(kg)"
        label-width="100px"
        prop="workbench"
        clearable
        placeholder="请输入实际重量"
      />
      <wd-input
        v-model="model.quality"
        label="盘长(m)"
        label-width="100px"
        prop="quality"
        clearable
        placeholder="请输入盘长"
      />
      <wd-input
        v-model="model.specification"
        label="理论重量(kg)"
        label-width="100px"
        prop="specification"
        clearable
        placeholder="请输入理论重量"
      />
      <wd-input
        v-model="model.disc"
        label="规格型号"
        label-width="100px"
        prop="disc"
        clearable
        placeholder="请输入规格型号"
      />
      <wd-input
        v-model="model.actuallyLength"
        label="实际盘长(m)"
        label="生产长度(m)"
        label-width="100px"
        prop="actuallyLength"
        clearable
        placeholder="请输入实际盘长"
      />
        placeholder="请输入生产长度"
        type="digit"
      >
        <template #label>
          <span style="color: #f56c6c">生产长度(m)</span>
        </template>
      </wd-input>
      <wd-input
        v-model="model.tare"
        label="盘具皮重(kg)"
        label-width="100px"
        prop="tare"
        :disabled="!isFirstReport"
        :clearable="isFirstReport"
        :placeholder="isFirstReport ? '请输入盘具皮重' : '盘具皮重自动带出'"
        type="digit"
      >
        <template #label>
          <span style="color: #f56c6c">盘具皮重(kg)</span>
        </template>
      </wd-input>
    </wd-cell-group>
  </wd-form>
</template>
<script lang="ts" setup>
import { computed, watch } from "vue";
import useFormData from "@/hooks/useFormData";
import { useToast } from "wot-design-uni";
import { useToast, dayjs } from "wot-design-uni";
import TwistApi from "@/api/product/twist";
// å®šä¹‰ props
const props = defineProps<{
  firstTareValue?: number;
  teamId?: string | number | null;
  isFirstReport?: boolean; // æ˜¯å¦æ˜¯ç¬¬ä¸€æ¡æŠ¥å·¥
}>();
// è®¡ç®—是否是第一条报工
const isFirstReport = computed(() => props.isFirstReport ?? true);
const paramsId = ref();
const toast = useToast();
const { form: model } = useFormData({
  poleNumber: undefined, // é¢†ç”¨æ†å·
  poleWeight: undefined, // æ†é‡(kg)
  monofilamentNumber: undefined, // å•丝盘号
  actuallyWeight: undefined, // å®žé™…重量(kg)
  oneLength: undefined, // ç›˜é•¿(m)
  theoryWeight: undefined, // ç†è®ºé‡é‡(kg)
  model: undefined, // è§„格型号
  actuallyLength: undefined, // å®žé™…盘长(m)
const { form: model, resetForm } = useFormData({
  actuallyLength: undefined, // ç”Ÿäº§é•¿åº¦(m)
  tare: undefined, // ç›˜å…·çš®é‡(kg)
});
// ä¸»è¡¨æ•°æ®ï¼ˆä»Žçˆ¶ç»„件传入)
const mainTableData = ref<any>({});
// è®¾ç½®ä¸»è¡¨æ•°æ®
const setMainTableData = (data: any) => {
  mainTableData.value = data;
};
// ç›‘听 firstTareValue å˜åŒ–,如果不是第一条,自动填充
watch(
  () => props.firstTareValue,
  (newVal) => {
    if (!isFirstReport.value && newVal !== undefined) {
      model.tare = newVal;
    }
  },
  { immediate: true }
);
const submit = async () => {
  const { code } = await TwistApi.addTwistOutput({
  // èŽ·å–ç¬¬ä¸€æ¡æ•°æ®çš„çš®é‡å€¼ï¼Œç”¨äºŽåŽç»­æ–°å¢žçš„æŠ¥å·¥
  const firstTareValue = props.firstTareValue;
  // å¦‚果主表数据未获取,尝试重新获取
  if (!mainTableData.value.model) {
    try {
      const { data } = await TwistApi.getTwistDetailById({
        id: paramsId.value,
      });
      mainTableData.value = {
        model: data.model,
        totalLength: data.totalLength,
        systemNo: data.systemNo,
      };
    } catch (error) {
      console.error("获取主表数据失败:", error);
      toast.error("获取规格型号数据失败,请重试");
      return false;
    }
  }
  // å†æ¬¡æ£€æŸ¥ä¸»è¡¨æ•°æ®
  if (!mainTableData.value.model) {
    toast.error("规格型号数据未获取,请重试");
    return false;
  }
  const submitData = {
    teamId: props.teamId || null,
    wireId: paramsId.value,
    ...model,
  });
    type: "绞线",
    actuallyLength: model.actuallyLength,
    tare: model.tare || firstTareValue,
    // ä»Žä¸»è¡¨èŽ·å–çš„å­—æ®µ
    model: mainTableData.value.model, // è§„格型号
    oneLength: mainTableData.value.totalLength,
    systemNo: mainTableData.value.systemNo,
    monofilamentNumber: undefined, // æ‰¹æ¬¡å·ï¼ˆåŽç«¯è‡ªåŠ¨ç”Ÿæˆï¼‰
    // ç”Ÿäº§æ—¥æœŸè‡ªåŠ¨è®¾ç½®ä¸ºå½“å‰æ—¶é—´
    productTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
  };
  // è°ƒè¯•日志
  console.log("提交数据:", submitData);
  console.log("主表数据:", mainTableData.value);
  const { code } = await TwistApi.addTwistOutput(submitData);
  if (code == 200) {
    toast.success("提交成功");
    resetForm();
    return true;
  } else {
    toast.error("提交失败");
    return false;
  }
};
// èŽ·å–è¡¨å•æ•°æ®
const getFormData = () => {
  return { ...model };
};
onLoad((options: any) => {
@@ -107,12 +143,12 @@
defineExpose({
  submit,
  getFormData,
  setMainTableData,
});
</script>
<style lang="scss" scoped>
.form_box {
}
.submit_btn {
  position: absolute;
  bottom: 0;
src/pages/production/twist/report/index.vue
@@ -12,15 +12,23 @@
              <text class="text-[#0D867F] ml-2 font-medium">生产人</text>
              <text class="text-[#333333] ml-2">{{ item.productUser }}</text>
            </view>
            <view class="text-[#A8A8A8]" @click="toEdit">编辑</view>
            <!-- <view class="text-[#A8A8A8]" @click="toEdit">编辑</view> -->
          </view>
        </template>
        <ProductionCard :data="cardAttr" :value="item" />
        <TwistReportCard :data="cardAttr" :value="item" />
        <template #footer>
          <wd-button size="small" plain style="margin-right: 10px" @click="toAttachment(item)">
            é™„ä»¶
          </wd-button>
          <wd-button size="small" plain @click="handleSelfCheck(item.id)">自检</wd-button>
          <wd-button
            size="small"
            plain
            style="margin-right: 10px"
            @click="handleSelfCheck(item.id)"
          >
            è‡ªæ£€
          </wd-button>
          <wd-button size="small" plain type="error" @click="handleDelete(item)">删除</wd-button>
        </template>
      </wd-card>
    </z-paging>
@@ -30,7 +38,12 @@
        <wd-button type="text" @click="cancel">取消</wd-button>
        <wd-button type="text" @click="submit">确定</wd-button>
      </view>
      <TwistForm ref="twistFormRef" />
      <TwistForm
        ref="twistFormRef"
        :first-tare-value="twistReportList.length > 0 ? twistReportList[0].tare : undefined"
        :team-id="teamId"
        :is-first-report="twistReportList.length === 0"
      />
    </wd-popup>
    <wd-popup v-model="drawFormRef.visible" position="bottom" custom-class="yl-popup">
      <Draw
@@ -47,13 +60,15 @@
<script setup lang="ts">
import CardTitle from "@/components/card-title/index.vue";
import TwistForm from "./form.vue";
import { useToast } from "wot-design-uni";
import ProductionCard from "../../components/ProductionCard.vue";
import { useToast, dayjs } from "wot-design-uni";
import TwistReportCard from "../components/TwistReportCard.vue";
import { onLoad } from "@dcloudio/uni-app";
import { ref, reactive } from "vue";
import { ref, reactive, nextTick } from "vue";
import ManageApi from "@/api/product/manage";
import TwistApi from "@/api/product/twist";
import Draw from "./draw.vue";
import HomeApi from "@/api/home";
import { setTeamId, getTeamId } from "@/utils/cache";
const drawFormRef = reactive({
  visible: false,
@@ -95,36 +110,65 @@
const cardAttr = ref<any[]>([
  {
    label: "领用杆号",
    prop: "poleNumber",
  },
  {
    label: "杆重(kg)",
    prop: "poleWeight",
  },
  {
    label: "单丝盘号",
    label: "批次号",
    prop: "monofilamentNumber",
    span: 24,
  },
  {
    label: "实际重量(kg)",
    prop: "actuallyWeight",
    label: "质量追溯号",
    prop: "systemNo",
    span: 24,
  },
  {
    label: "盘长(m)",
    prop: "oneLength",
  },
  {
    label: "理论重量(kg)",
    prop: "theoryWeight",
  },
  {
    label: "规格型号",
    prop: "model",
  },
  {
    label: "生产长度(m)",
    prop: "actuallyLength",
  },
  {
    label: "盘具皮重(kg)",
    prop: "tare",
  },
  {
    label: "生产日期",
    prop: "productTime",
    span: 24,
  },
  {
    label: "加工时间(h)",
    prop: "processHour",
  },
]);
const twistReportList = ref<any[]>([]);
const teamId = ref<string | number | null>(null);
// èŽ·å–å¹¶ç¼“å­˜ç­ç»„ID
const initTeamId = async () => {
  // å…ˆå°è¯•从缓存获取
  const cachedTeamId = getTeamId();
  if (cachedTeamId) {
    teamId.value = cachedTeamId;
    return;
  }
  // å¦‚果缓存中没有,则调用接口获取
  try {
    const { data } = await HomeApi.getIndex();
    if (data && data.team) {
      teamId.value = data.team;
      setTeamId(data.team);
    }
  } catch (error) {
    console.error("获取班组ID失败:", error);
  }
};
const toEdit = () => {
  uni.navigateTo({
@@ -132,23 +176,83 @@
  });
};
// ä¸»è¡¨æ•°æ®
const mainTableData = ref<any>({});
// èŽ·å–ä¸»è¡¨æ•°æ®
const getMainTableData = async () => {
  try {
    const { data } = await TwistApi.getTwistDetailById({
      id: paramsId.value,
    });
    mainTableData.value = {
      model: data.model,
      totalLength: data.totalLength,
      systemNo: data.systemNo,
    };
    // è®¾ç½®ä¸»è¡¨æ•°æ®åˆ°è¡¨å•组件
    if (twistFormRef.value) {
      twistFormRef.value.setMainTableData(mainTableData.value);
    }
  } catch (error) {
    console.error("获取主表数据失败:", error);
  }
};
const addReport = async () => {
  dialog.visible = true;
  // æ‰“开新增弹窗时自动执行
  // await showDrawPopup();
  // æ£€æŸ¥æ˜¯å¦æ‰€æœ‰æ•°æ®éƒ½å·²ä¿å­˜ï¼ˆéƒ½æœ‰id)
  if (twistReportList.value.length > 0 && twistReportList.value.every((item) => item.id)) {
    // ç¡®ä¿ä¸»è¡¨æ•°æ®å·²èŽ·å–
    if (!mainTableData.value.model) {
      await getMainTableData();
    }
    dialog.visible = true;
    // ç­‰å¾…弹窗打开后设置数据
    await nextTick();
    if (twistFormRef.value) {
      twistFormRef.value.setMainTableData(mainTableData.value);
    }
  } else if (twistReportList.value.length === 0) {
    // ç¡®ä¿ä¸»è¡¨æ•°æ®å·²èŽ·å–
    if (!mainTableData.value.model) {
      await getMainTableData();
    }
    dialog.visible = true;
    // ç­‰å¾…弹窗打开后设置数据
    await nextTick();
    if (twistFormRef.value) {
      twistFormRef.value.setMainTableData(mainTableData.value);
    }
  } else {
    toast.warning("请先保存本条数据,再新增");
  }
};
const submit = async () => {
  // éªŒè¯å¿…填字段 - æ ¹æ®å‚考代码,需要检查生产长度和盘具皮重
  const formData = twistFormRef.value?.getFormData?.() || {};
  const firstTareValue =
    twistReportList.value.length > 0 ? twistReportList.value[0].tare : undefined;
  if (!formData.actuallyLength) {
    toast.warning("请输入生产长度后再提交");
    return;
  }
  if (!formData.tare && !firstTareValue) {
    toast.warning("请输入盘具皮重后再提交");
    return;
  }
  const isSuccess = await twistFormRef.value.submit();
  dialog.visible = !isSuccess; // å¦‚果提交成功,关闭弹窗
  if (isSuccess) {
    // æäº¤æˆåŠŸåŽæ‰§è¡Œ
    // showDrawPopup();
    dialog.visible = false;
    // æäº¤æˆåŠŸåŽåˆ·æ–°åˆ—è¡¨
    pagingRef.value?.reload();
  }
};
const cancel = () => {
  toast.show("取消");
  dialog.visible = false;
};
@@ -197,6 +301,35 @@
  });
};
// åˆ é™¤æŠ¥å·¥è®°å½•
const handleDelete = (item: any) => {
  uni.showModal({
    title: "提示",
    content: "确定删除吗?",
    success: async (res) => {
      if (res.confirm) {
        try {
          if (item.id) {
            const { code } = await TwistApi.deleteWireOutput({ id: item.id });
            if (code == 200) {
              toast.success("删除成功");
              // åˆ·æ–°åˆ—表
              pagingRef.value?.reload();
            } else {
              toast.error("删除失败");
            }
          } else {
            toast.warning("该记录尚未保存,无法删除");
          }
        } catch (error) {
          console.error("删除失败:", error);
          toast.error("删除失败,请重试");
        }
      }
    },
  });
};
// ä¿ç•™åŽŸæœ‰çš„confirm函数,用于其他地方调用
// const confirm = async () => {
//   await showDrawPopup();
@@ -208,11 +341,23 @@
    wireId: paramsId.value,
    type: "绞线",
  });
  // æ ¼å¼åŒ–生产日期
  if (Array.isArray(data)) {
    data.forEach((item: any) => {
      if (item.productTime) {
        item.productTime = dayjs(item.productTime).format("YYYY-MM-DD HH:mm:ss");
      }
    });
  }
  pagingRef.value.complete(data);
};
onLoad((options: any) => {
onLoad(async (options: any) => {
  paramsId.value = options.id;
  // èŽ·å–å¹¶ç¼“å­˜ç­ç»„ID
  await initTeamId();
  // èŽ·å–ä¸»è¡¨æ•°æ®
  await getMainTableData();
  showDrawPopup();
});
</script>
src/pages/production/wire/attachment/index.vue
@@ -17,26 +17,54 @@
    <view class="attachment-list">
      <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="暂无附件" />
      <wd-card
        v-for="item in attachmentList"
        :key="item.id"
        type="rectangle"
        custom-class="attachment-card"
        :border="false"
      >
        <view class="attachment-item" @click="previewAttachment(item)">
          <view class="attachment-info">
            <view class="attachment-name">{{ item.bucketFileName || item.name }}</view>
            <view class="attachment-meta">
              <text class="file-type">{{ getFileType(item.bucketFileName) }}</text>
              <text class="upload-time">{{ formatTime(item.createTime) }}</text>
      <view v-for="item in attachmentList" :key="item.id" class="attachment-card">
        <view class="media-wrapper" @click="previewAttachment(item)">
          <!-- å›¾ç‰‡é¢„览 -->
          <template v-if="isImageType(item.url)">
            <image
              v-if="!item.loadError"
              :src="getFullUrl(item.url)"
              mode="aspectFill"
              class="media-preview"
              @error="onImageError(item)"
              @load="onImageLoad(item)"
            />
            <!-- å›¾ç‰‡åŠ è½½å¤±è´¥æ˜¾ç¤ºé»˜è®¤å›¾æ ‡ -->
            <view v-else class="file-icon-wrapper">
              <wd-icon name="picture" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- è§†é¢‘预览 -->
          <template v-else-if="isVideoType(item.url)">
            <video
              v-if="!item.loadError"
              :src="getFullUrl(item.url)"
              class="media-preview"
              :controls="false"
              :show-center-play-btn="false"
              @error="onVideoError(item)"
            />
            <!-- è§†é¢‘加载失败显示默认图标 -->
            <view v-else class="file-icon-wrapper">
              <wd-icon name="video" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- å…¶ä»–文件类型显示图标 -->
          <view v-else class="file-icon-wrapper">
            <wd-icon name="file-outline" size="48px" color="#999" />
            <text class="file-name">文件</text>
          </view>
          <view class="attachment-actions" @click.stop>
            <wd-icon name="delete" color="#ff4757" @click="deleteAttachment(item.id)" />
          <!-- åˆ é™¤æŒ‰é’® -->
          <view class="delete-btn" @click.stop="deleteAttachment(item.id)">
            <wd-icon name="delete" color="#fff" size="20px" />
          </view>
        </view>
      </wd-card>
      </view>
    </view>
    <wd-toast />
@@ -48,6 +76,12 @@
import { useToast } from "wot-design-uni";
import AttachmentAPI from "@/api/product/attachment";
// H5 ä½¿ç”¨ VITE_APP_BASE_API ä½œä¸ºä»£ç†è·¯å¾„,其他平台使用 VITE_APP_API_URL ä½œä¸ºè¯·æ±‚路径
let baseUrl = import.meta.env.VITE_APP_API_URL;
// #ifdef H5
baseUrl = import.meta.env.VITE_APP_BASE_API;
// #endif
const toast = useToast();
// é¡µé¢å‚æ•°
@@ -56,6 +90,57 @@
const attachmentList = ref<any[]>([]);
const detailData = ref<any>({});
// èŽ·å–å®Œæ•´çš„å›¾ç‰‡/视频 URL
const getFullUrl = (url: string) => {
  if (!url) return "";
  // å¦‚果已经是完整的 URL(http æˆ– https å¼€å¤´ï¼‰ï¼Œç›´æŽ¥è¿”回
  if (url.startsWith("http://") || url.startsWith("https://")) {
    return url;
  }
  // å¦‚果是相对路径,拼接基础 URL
  return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
};
// ä»Ž URL æˆ–文件名中提取扩展名
const getExtension = (urlOrFileName: string) => {
  if (!urlOrFileName) return "";
  // ç§»é™¤æŸ¥è¯¢å‚数和哈希
  const cleanUrl = urlOrFileName.split("?")[0].split("#")[0];
  // èŽ·å–æœ€åŽä¸€ä¸ªç‚¹åŽé¢çš„å†…å®¹
  const extension = cleanUrl.split(".").pop()?.toLowerCase();
  return extension || "";
};
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡ç±»åž‹
const isImageType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(extension);
};
// åˆ¤æ–­æ˜¯å¦ä¸ºè§†é¢‘类型
const isVideoType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["mp4", "mov", "avi", "wmv", "flv", "mkv", "webm"].includes(extension);
};
// å›¾ç‰‡åŠ è½½æˆåŠŸ
const onImageLoad = (item: any) => {
  item.loadError = false;
};
// å›¾ç‰‡åŠ è½½å¤±è´¥
const onImageError = (item: any) => {
  console.error("图片加载失败:", item.url);
  item.loadError = true;
};
// è§†é¢‘加载失败
const onVideoError = (item: any) => {
  console.error("视频加载失败:", item.url);
  item.loadError = true;
};
// èŽ·å–é™„ä»¶åˆ—è¡¨
const getAttachmentList = async (data: any) => {
  try {
@@ -252,24 +337,24 @@
// é¢„览附件
const previewAttachment = (item: any) => {
  // æ ¹æ®æ–‡ä»¶ç±»åž‹è¿›è¡Œé¢„览
  const fileName = item.bucketFileName || item.name;
  const fileType = getFileType(fileName);
  const fileType = getFileType(item.url);
  const fullUrl = getFullUrl(item.url);
  if (fileType.startsWith("image")) {
    // å›¾ç‰‡é¢„览
    uni.previewImage({
      urls: [item.url],
      current: item.url,
      urls: [fullUrl],
      current: fullUrl,
    });
  } else {
    // å…¶ä»–文件类型,可以下载或打开
    uni.downloadFile({
      url: item.url,
      url: fullUrl,
      success: (res) => {
        uni.openDocument({
          filePath: res.tempFilePath,
          success: () => {
            console.log("打开文档成功");
            // æ‰“开文档成功
          },
          fail: (error) => {
            console.error("打开文档失败:", error);
@@ -286,9 +371,9 @@
};
// èŽ·å–æ–‡ä»¶ç±»åž‹
const getFileType = (fileName: string) => {
  if (!fileName) return "unknown";
  const extension = fileName.split(".").pop()?.toLowerCase();
const getFileType = (urlOrFileName: string) => {
  if (!urlOrFileName) return "unknown";
  const extension = getExtension(urlOrFileName);
  switch (extension) {
    case "jpg":
    case "jpeg":
@@ -297,6 +382,14 @@
    case "bmp":
    case "webp":
      return "image";
    case "mp4":
    case "mov":
    case "avi":
    case "wmv":
    case "flv":
    case "mkv":
    case "webm":
      return "video";
    case "pdf":
      return "pdf";
    case "doc":
@@ -357,43 +450,80 @@
}
.attachment-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  .attachment-card {
    margin-bottom: 12px;
    border-radius: 4px;
    width: 100%;
    aspect-ratio: 1;
  }
}
.attachment-item {
  display: flex;
  align-items: center;
  padding: 12px;
.media-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
  .attachment-info {
    flex: 1;
  .media-preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
    .attachment-name {
      font-size: 16px;
      font-weight: 500;
      color: #333;
      margin-bottom: 4px;
      word-break: break-all;
    }
  .file-icon-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding: 8px;
    text-align: center;
    .attachment-meta {
      display: flex;
      gap: 12px;
    .file-name {
      margin-top: 8px;
      font-size: 12px;
      color: #999;
      color: #666;
      word-break: break-all;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
      &.error-text {
        color: #ff4757;
      }
    }
  }
  .attachment-actions {
    margin-left: 12px;
    :deep(.wd-icon) {
      font-size: 20px;
      cursor: pointer;
    }
  .delete-btn {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10;
  }
}
</style>
src/pages/routingInspection/detail/indexJX.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,867 @@
<template>
  <view class="fixed-header">
    <view class="header-container">
      <wd-button
        icon="file-add"
        :round="false"
        size="small"
        custom-class="add_btn"
        @click="editList"
        v-if="!isEdit"
      >
        ç¼–辑
      </wd-button>
      <wd-button
        icon="close"
        type="info"
        :round="false"
        size="small"
        custom-class="add_btn"
        @click="close"
        v-if="isEdit"
      >
        å–消
      </wd-button>
      <wd-button
        icon="check"
        type="success"
        :round="false"
        size="small"
        custom-class="add_btn"
        @click="saveList"
        v-if="isEdit"
      >
        ä¿å­˜
      </wd-button>
      <view class="placeholder"></view>
      <view class="scan-info">
        <text class="scan-device-text">当前扫码机台: {{ scannedDeviceModel || "未扫码" }}</text>
      </view>
      <view class="scan-wrapper" @click="openScan">
        <wd-icon name="scan" size="24px" color="#0D867F"></wd-icon>
      </view>
    </view>
  </view>
  <view class="list">
    <!-- åŸºæœ¬ä¿¡æ¯æ¨¡å— -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "基本信息" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="日期" prop="recordDate">
          {{ formatDate(recordData.fixedInfo?.recordDate) }}
        </wd-form-item>
        <wd-form-item label="班次" prop="workShift">
          {{ formatValue(recordData.fixedInfo?.workShift) }}
        </wd-form-item>
        <wd-form-item label="型号规格" prop="model">
          {{ formatValue(recordData.fixedInfo?.model) }}
        </wd-form-item>
        <wd-form-item label="成品线盘号" prop="systemNo">
          {{ formatValue(recordData.fixedInfo?.systemNo) }}
        </wd-form-item>
        <wd-form-item label="记录人" prop="createUserName">
          {{ formatValue(recordData.fixedInfo?.createUserName) }}
        </wd-form-item>
        <wd-form-item label="机台" prop="deviceModel">
          {{ formatValue(recordData.fixedInfo?.deviceModel) }}
        </wd-form-item>
        <wd-form-item label="产品类别" prop="productType">
          {{ formatValue(recordData.fixedInfo?.productType) }}
        </wd-form-item>
        <wd-form-item label="生产长度" prop="actuallyLength">
          {{ formatValue(recordData.fixedInfo?.actuallyLength, "m") }}
        </wd-form-item>
        <wd-form-item label="张力设置" prop="tensionSetting">
          {{ formatValue(recordData.fixedInfo?.tensionSetting, "N/m") }}
        </wd-form-item>
        <!-- ç»žåˆ¶å¤–径(可编辑) -->
        <wd-form-item label="绞合外径" prop="twistedOuterDiameter" required>
          <template v-if="isEdit">
            <wd-input
              v-model="formData.twistedOuterDiameter"
              placeholder="请输入绞合外径(mm)"
              type="number"
            />
          </template>
          <template v-else>
            {{ formatValue(formData.twistedOuterDiameter, "mm") }}
          </template>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- å·¥è‰ºè®°å½•详情模块 -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "工艺记录详情" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="记录位置" prop="recordPosition">
          {{ recordData.structureInfo?.recordPosition || "-" }}
        </wd-form-item>
        <wd-form-item label="记录人" prop="createUserName">
          {{ recordData.structureInfo?.createUserName || "-" }}
        </wd-form-item>
        <wd-form-item label="状态" prop="status">
          <wd-tag custom-class="space" :type="getStatusType(recordData.structureInfo?.status)">
            {{ getStatusText(recordData.structureInfo?.status) }}
          </wd-tag>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- ç»“构检查模块 -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "结构检查" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="成品结构" prop="structureFormula" required>
          <template v-if="isEdit">
            <wd-input v-model="formData.structureFormula" placeholder="请输入成品结构" />
          </template>
          <template v-else>
            {{ formData.structureFormula || "-" }}
          </template>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- ç»“构标准值和实测(可编辑) -->
    <wd-row v-if="formData.structureItems.length">
      <view style="margin: 10rpx">
        <text class="title">{{ "结构标准值和实测" }}</text>
      </view>
      <wd-col
        :span="24"
        v-for="(item, index) in formData.structureItems"
        :key="index"
        style="padding-bottom: 10px"
      >
        <wd-form-item
          prop="structureItemsGroup"
          :label="formatValue(item.structureName)"
          label-width="400rpx"
          style="color: red"
          required
        ></wd-form-item>
        <wd-form-item label="标准值" prop="structureValue" required>
          {{ formatValue(item.structureValue) }}
        </wd-form-item>
        <wd-form-item label="实测根数" prop="actualValue1" required>
          <template v-if="isEdit">
            <wd-input v-model="item.actualValue1" placeholder="请输入实测根数" type="number" />
          </template>
          <template v-else>
            {{ formatValue(item.actualValue1, "æ ¹") }}
          </template>
        </wd-form-item>
        <wd-form-item label="实测直径" prop="actualValue2" required>
          <template v-if="isEdit">
            <wd-input
              v-model="item.actualValue2"
              placeholder="请输入实测直径(mm)"
              type="number"
            />
          </template>
          <template v-else>
            {{ formatValue(item.actualValue2, "mm") }}
          </template>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- ç»žçº¿å·¥è‰ºè´¨é‡æŽ§åˆ¶ï¼ˆå¯ç¼–辑) -->
    <wd-row v-if="formData.inspectTwist.length">
      <view style="margin: 10rpx">
        <text class="title">{{ "绞线工艺质量控制" }}</text>
      </view>
      <wd-col
        :span="24"
        v-for="(item, index) in formData.inspectTwist"
        :key="index"
        style="padding-bottom: 10px"
      >
        <wd-form-item
          :label="formatValue(item.twistName)"
          label-width="400rpx"
          style="color: red"
          prop="inspectTwistGroup"
          required
        ></wd-form-item>
        <wd-form-item label="绞向" prop="direction" required>
          <template v-if="isEdit">
            <wd-select-picker
              label=""
              v-model="item.direction"
              :columns="twistDirectionOptions"
              type="radio"
              placeholder="请选择绞向"
              :clearable="false"
            ></wd-select-picker>
          </template>
          <template v-else>
            {{ formatValue(item.direction) }}
          </template>
        </wd-form-item>
        <wd-form-item label="节距" prop="pitch" required>
          <template v-if="isEdit">
            <wd-input
              v-model="item.pitch"
              placeholder="请输入节距(mm)"
              type="number"
              @input="updatePitchRatio(item)"
            />
          </template>
          <template v-else>
            {{ formatValue(item.pitch, "mm") }}
          </template>
        </wd-form-item>
        <wd-form-item label="节径比" prop="pitchRatio" required>
          {{ formatValue(item.pitchRatio) }}
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- å¤–观和结论(可编辑) -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "外观和结论" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="产品外观" prop="productAppearance" required>
          <template v-if="isEdit">
            <view style="display: flex; flex-wrap: wrap; gap: 10px">
              <wd-checkbox
                v-for="(opt, idx) in appearanceOptions"
                :key="idx"
                :value="opt.value"
                :modelValue="formData.productAppearance.includes(opt.value)"
                @click="handleAppearanceClick(opt.value)"
                style="width: 100px"
              >
                {{ opt.label }}
              </wd-checkbox>
            </view>
          </template>
          <template v-else>
            {{ formatProductAppearance(formData.productAppearance) }}
          </template>
        </wd-form-item>
        <wd-form-item label="结论" prop="conclusion" required>
          <template v-if="isEdit">
            <wd-radio-group v-model="formData.conclusion" inline class="conclusion-radio-group">
              <wd-radio
                v-for="(opt, idx) in conclusionOptions"
                :key="idx"
                :value="opt.value"
                shape="dot"
              >
                {{ opt.label }}
              </wd-radio>
            </wd-radio-group>
          </template>
          <template v-else>
            {{ formatValue(formData.conclusion) }}
          </template>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- å·¡æ£€ç»“果模块 -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "巡检结果" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="样品是否齐全" prop="isFully" required>
          <template v-if="isEdit">
            <wd-radio-group v-model="formData.isFully" inline class="conclusion-radio-group">
              <wd-radio
                v-for="(opt, idx) in sampleCompleteOptions"
                :key="idx"
                :value="opt.value"
                shape="dot"
              >
                {{ opt.label }}
              </wd-radio>
            </wd-radio-group>
          </template>
          <template v-else>
            {{ formatValue(formData.isFully) }}
          </template>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- é™„件模块(含上传功能) -->
    <wd-row class="attachment-section">
      <view style="margin: 10rpx">
        <text class="title">{{ "附件" }}</text>
      </view>
      <wd-col :span="24">
        <AttachmentUpload
          :detailData="detailData"
          :isEdit="isEdit"
          :deviceType="paramsType"
          ref="attachmentRef"
          v-if="detailDataLoaded"
        />
      </wd-col>
    </wd-row>
    <wd-popup v-model="show" custom-style="border-radius:32rpx;" @close="handleClose">
      <div class="image-preview">
        <img :src="previewImageUrl" alt="预览图片" style="width: 100%; height: auto" />
      </div>
    </wd-popup>
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onUnmounted } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import RoutingInspectionApi from "@/api/routingInspection/routingInspection";
import { useToast } from "wot-design-uni";
import AttachmentUpload from "../upload.vue";
import { useUserStore } from "@/store/modules/user";
import { useScanCode } from "@/composables/useScanCode";
const paramsType = ref("");
const paramsId = ref("");
const recordData = ref<any>({ structureInfo: { files: [], structureRecordResult: {} } });
const show = ref(false);
const previewImageUrl = ref("");
const isEdit = ref(false);
const tempFiles = ref<any[]>([]); // ä¸´æ—¶å­˜å‚¨æ–°ä¸Šä¼ çš„附件
const toast = useToast();
const attachmentRef = ref<any>(null);
const detailData = reactive<any>({});
const detailDataLoaded = ref(false);
// èŽ·å–å½“å‰ç™»å½•ç”¨æˆ·ä¿¡æ¯
const userStore = useUserStore();
const userInfo: any = computed(() => userStore.userInfo);
// ä½¿ç”¨æ‰«ç ç®¡ç† composable(全局监听器,不随页面切换关闭)
const {
  deviceUid,
  deviceModel: scannedDeviceModel,
  loadFromCache,
  enableListener,
} = useScanCode("scanJX");
const formData = reactive({
  twistedOuterDiameter: "", // ç»žåˆ¶å¤–径
  structureFormula: "", // æˆå“ç»“æž„
  structureItems: [], // ç»“构标准值和实测
  inspectTwist: [], // ç»žçº¿å·¥è‰ºè´¨é‡æŽ§åˆ¶
  productAppearance: [] as string[], // äº§å“å¤–观(改为数组存储选中值)
  conclusion: "", // ç»“论(改为数组存储选中值)
  isFully: "", // æ ·å“æ˜¯å¦é½å…¨
});
const twistDirectionOptions = [
  { label: "左向", value: "左向" },
  { label: "右向", value: "右向" },
];
const appearanceOptions = [
  { label: "无外观问题", value: "无外观问题" },
  { label: "表面划伤", value: "表面划伤" },
  { label: "直径不均", value: "直径不均" },
  { label: "其他缺陷", value: "其他缺陷" },
];
const conclusionOptions = [
  { label: "合格", value: "合格" },
  { label: "不合格", value: "不合格" },
];
const sampleCompleteOptions = [
  { label: "是", value: "是" },
  { label: "否", value: "否" },
];
const initFormData = () => {
  const structureResult = recordData.value.structureInfo?.structureRecordResult || {};
  const inspectionResult = recordData.value.inspectionResult || {};
  formData.twistedOuterDiameter =
    recordData.value.structureInfo.structureRecordResult.twistedOuterDiameter || "";
  formData.structureFormula = structureResult.inspectStructure?.structureFormula || "";
  formData.isFully = inspectionResult.isFully || "";
  formData.conclusion = structureResult.conclusion || "";
  // åˆå§‹åŒ–产品外观
  const appearance = Array.isArray(structureResult.productAppearance)
    ? structureResult.productAppearance
    : structureResult.productAppearance
      ? [structureResult.productAppearance]
      : [];
  formData.productAppearance = appearance;
  formData.structureItems = JSON.parse(
    JSON.stringify(structureResult.inspectStructure?.structureItems || [])
  );
  formData.inspectTwist = JSON.parse(JSON.stringify(structureResult.inspectTwist || []));
  formData.inspectTwist.forEach((item: any) => {
    if (!item.direction) item.direction = "";
  });
};
const getDetailData = async (id: string, deviceType: string) => {
  try {
    const response = await RoutingInspectionApi.getStrandedInspectionStructureInfoById({ id });
    recordData.value = response.data;
    detailData.value = response.data.structureInfo;
    // å¦‚果记录人为空,默认设置为当前登录用户
    if (recordData.value.structureInfo && !recordData.value.structureInfo.createUserName) {
      recordData.value.structureInfo.createUserName =
        userInfo.value?.nickName || userInfo.value?.userName || "";
    }
    console.log("detailData.value", detailData.value);
    tempFiles.value = []; // æ¸…空临时文件
    initFormData(); // æ•°æ®è¿”回后初始化表单
    detailDataLoaded.value = true; // æ•°æ®åŠ è½½å®ŒæˆåŽï¼Œæ¸²æŸ“å­ç»„ä»¶
    console.log("父组件-数据就绪后打印");
  } catch (error) {
    console.error("获取详情失败:", error);
    uni.showToast({ title: "加载失败", icon: "error" });
  }
};
// é¡µé¢åŠ è½½
onLoad((options: any) => {
  try {
    paramsId.value = options.id;
    paramsType.value = options.deviceType;
    getDetailData(options.id, options.deviceType);
  } catch (error) {
    console.error("获取详情失败:", error);
    uni.showToast({ title: "加载失败", icon: "error" });
  }
});
// ç¼–辑模式切换
const editList = () => {
  isEdit.value = true;
};
// å–消编辑(重置表单)
const close = () => {
  isEdit.value = false;
  tempFiles.value = [];
  initFormData();
};
// ä¿å­˜ç¼–辑(含必填项校验)
const saveList = async () => {
  // 1. åŸºç¡€å­—段校验
  if (!formData.structureFormula) return uni.showToast({ title: "成品结构为必填项", icon: "none" });
  if (!formData.twistedOuterDiameter)
    return uni.showToast({ title: "绞制外径为必填项", icon: "none" });
  if (!formData.productAppearance.length)
    return uni.showToast({ title: "产品外观为必填项", icon: "none" });
  if (!formData.conclusion) return uni.showToast({ title: "结论为必填项", icon: "none" });
  if (!formData.isFully) return uni.showToast({ title: "样品是否齐全为必填项", icon: "none" });
  // 2. ç»“构项循环校验
  for (const item of formData.structureItems) {
    if (!item.structureValue)
      return uni.showToast({ title: `${item.structureName}标准值为必填项`, icon: "none" });
    if (!item.actualValue1)
      return uni.showToast({ title: `${item.structureName}实测根数为必填项`, icon: "none" });
    if (!item.actualValue2)
      return uni.showToast({ title: `${item.structureName}实测直径为必填项`, icon: "none" });
  }
  // 3. ç»žçº¿å·¥è‰ºé¡¹å¾ªçŽ¯æ ¡éªŒ
  for (const item of formData.inspectTwist) {
    if (!item.direction)
      return uni.showToast({ title: `${item.twistName}绞向为必填项`, icon: "none" });
    if (!item.pitch) return uni.showToast({ title: `${item.twistName}节距为必填项`, icon: "none" });
    if (!item.pitchRatio)
      return uni.showToast({ title: `${item.twistName}节径比为必填项`, icon: "none" });
  }
  // éªŒè¯æ‰«ç æ•°æ®ï¼ˆä»Žç¼“存或新扫码获取)
  console.log("保存前检查 deviceUid:", deviceUid.value);
  if (!deviceUid.value) {
    return uni.showToast({
      title: "请先扫描设备二维码",
      icon: "none",
      duration: 2000,
    });
  }
  const { newFiles } = attachmentRef.value.getSubmitFiles();
  console.log("newFiles", newFiles);
  const allFileIds = [...newFiles];
  try {
    const res = await RoutingInspectionApi.strandedPatrolCheckInspection({
      deviceUid: deviceUid.value,
      id: paramsId.value,
      inspectionResult: {
        twistedOuterDiameter: formData.twistedOuterDiameter,
        structureFormula: formData.structureFormula,
        structureItems: formData.structureItems,
        inspectTwist: formData.inspectTwist,
        productAppearance: formData.productAppearance,
        conclusion: formData.conclusion,
        isFully: formData.isFully,
      },
      result: {
        isFully: formData.isFully,
      },
      processInspectionAttachmentList: allFileIds,
    });
    if (res.code === 200) {
      // è®¾ç½®åˆ·æ–°æ ‡è®°ï¼Œå‘Šè¯‰åˆ—表页需要刷新
      uni.setStorageSync("needRefreshInspectionList", true);
      uni.showToast({
        title: "保存成功",
        icon: "success",
        duration: 1500,
      });
      // å»¶è¿Ÿè¿”回列表页,让用户看到成功提示
      setTimeout(() => {
        uni.navigateBack({
          delta: 1,
        });
      }, 1500);
    } else {
      uni.showModal({ title: res.msg || "保存失败", icon: "error" });
    }
  } catch (e) {
    console.error("保存失败:", e);
    uni.showModal({ title: e.message || "保存失败", icon: "error" });
  }
};
const handleClose = () => {
  show.value = false;
};
// çŠ¶æ€ç±»åž‹æ˜ å°„
const getStatusType = (status: number) => {
  switch (status) {
    case 0:
      return "warning"; // å¾…巡检
    case 1:
      return "danger"; // å·²é©³å›ž
    case 2:
      return "primary"; // å¾…审核
    case 3:
      return "success"; // é€šè¿‡
    default:
      return "default";
  }
};
// çŠ¶æ€æ–‡æœ¬æ˜ å°„
const getStatusText = (status: number) => {
  switch (status) {
    case 0:
      return "待巡检";
    case 1:
      return "已驳回";
    case 2:
      return "待审核";
    case 3:
      return "通过";
    default:
      return "未知";
  }
};
// æ ¼å¼åŒ–产品外观显示
const formatProductAppearance = (productAppearance: string[]) => {
  if (!productAppearance || productAppearance.length === 0) return "-";
  return productAppearance.join("、");
};
// æ ¼å¼åŒ–数值显示
const formatValue = (value: any, unit?: string) => {
  if (value === null || value === undefined || value === "") return "-";
  return unit ? `${value}${unit}` : value;
};
// æ ¼å¼åŒ–日期显示
const formatDate = (date: string) => {
  if (!date) return "-";
  return new Date(date).toLocaleDateString("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  });
};
// è®¡ç®—节径比
const calculatePitchRatio = (pitch: string, dia: string) => {
  // å¦‚æžœpitch或dia为空,则返回"-"
  if (!pitch || !dia) return "-";
  // å°†pitch和dia转换为浮点数
  const pitchNum = parseFloat(pitch);
  const diaNum = parseFloat(dia);
  // å¦‚æžœpitchNum或diaNum是NaN,或者diaNum为0,则返回"-"
  if (isNaN(pitchNum) || isNaN(diaNum) || diaNum === 0) return "-";
  // è®¡ç®—pitchNum和diaNum的比值,并保留两位小数
  return (pitchNum / diaNum).toFixed(2);
};
// æ›´æ–°èŠ‚å¾„æ¯”ï¼ˆå½“èŠ‚è·å˜åŒ–æ—¶è‡ªåŠ¨è®¡ç®—ï¼‰
const updatePitchRatio = (item: any) => {
  // ä½¿ç”¨ç»žåˆå¤–径作为直径来计算节径比
  const dia = item.dia;
  item.pitchRatio = calculatePitchRatio(item.pitch, dia);
};
// å¤„理产品外观选择的互斥逻辑
const handleAppearanceClick = (value: string) => {
  const currentValues = [...formData.productAppearance];
  const isCurrentlyChecked = currentValues.includes(value);
  let newSelection: string[] = [];
  if (value === "无外观问题") {
    if (isCurrentlyChecked) {
      // å–消选中"无外观问题"
      newSelection = [];
    } else {
      // é€‰ä¸­"无外观问题",清空其他选项
      newSelection = ["无外观问题"];
    }
  } else {
    // ç‚¹å‡»å…¶ä»–选项
    if (isCurrentlyChecked) {
      // å–消选中该选项
      newSelection = currentValues.filter((v) => v !== value);
    } else {
      // é€‰ä¸­è¯¥é€‰é¡¹ï¼Œç§»é™¤"无外观问题"
      const filteredValues = currentValues.filter((v) => v !== "无外观问题");
      newSelection = [...filteredValues, value];
    }
  }
  formData.productAppearance = newSelection;
};
const openScan = () => {
  console.log("indexJX - ç‚¹å‡»æ‰«ç æŒ‰é’®ï¼ˆå…¨å±€æ‰«ç æ¨¡å¼ï¼Œæ— éœ€æ‰‹åŠ¨è§¦å‘ï¼‰");
  // å…¨å±€æ‰«ç æ¨¡å¼ä¸‹ï¼Œç¡¬ä»¶æ‰«ç ä¼šè‡ªåŠ¨è§¦å‘ï¼Œæ— éœ€æ‰‹åŠ¨è°ƒç”¨
  uni.showToast({
    title: "请使用扫码枪扫描",
    icon: "none",
  });
};
// é¡µé¢æ˜¾ç¤ºæ—¶çš„处理
onShow(() => {
  console.log("========== indexJX - onShow è§¦å‘ ==========");
  // é‡æ–°å¯ç”¨ç›‘听器(确保监听器有效)
  enableListener();
  // åŠ è½½ç¼“å­˜ï¼ˆæ›´æ–°UI显示)
  const cachedData = loadFromCache();
  // å¦‚果没有缓存数据,提示用户需要扫码
  if (!cachedData || !cachedData.uid) {
    console.log("⚠️ æœªæ£€æµ‹åˆ°æ‰«ç ç¼“存,用户需要扫描设备二维码");
    // åœ¨ç¼–辑模式下才提示
    if (isEdit.value) {
      setTimeout(() => {
        uni.showToast({
          title: "请扫描设备二维码后再保存",
          icon: "none",
          duration: 2000,
        });
      }, 500);
    }
  }
});
</script>
<style lang="scss" scoped>
.fixed-header {
  position: fixed;
  top: 44;
  left: 0;
  right: 0;
  background: #f3f9f8;
  z-index: 999;
  padding: 12px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  min-height: 60px;
  box-sizing: border-box;
  overflow: visible;
}
.header-container {
  display: flex;
  align-items: center;
  width: 100%;
  gap: 10px;
}
.placeholder {
  flex: 1;
}
.scan-info {
  display: flex;
  align-items: center;
  margin-right: 10px;
  .scan-device-text {
    font-size: 14px;
    color: #0d867f;
    font-weight: 500;
  }
}
.scan-wrapper {
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 6px;
  flex-shrink: 0;
}
.list {
  padding: 12px;
  padding-top: 84px;
  background: #f3f9f8;
  min-height: 100vh;
  box-sizing: border-box;
  overflow-y: auto;
}
.title {
  position: relative;
  margin-left: 10px;
  font-size: 16px;
  font-weight: 500;
  color: #0d867f;
}
.title::after {
  position: absolute;
  content: "";
  top: 4px;
  left: -10px;
  width: 4px;
  height: 16px;
  background: #0d867f;
  border-radius: 2px;
}
// äº§å“å¤–观和结论选择器样式(一行两个)
.checkbox-group {
  display: flex;
  flex-wrap: wrap;
  gap: 16rpx;
  padding: 8rpx 0;
}
.checkbox-item {
  width: calc(50% - 8rpx);
  margin-bottom: 8rpx;
}
// é™„件相关样式
.attachment-section {
  width: 100%;
}
.attachment-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px 0;
}
.attachment-item {
  width: calc(25% - 10px);
  box-sizing: border-box;
  position: relative;
}
.upload-btn {
  width: 80px;
  height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}
.upload-icon {
  font-size: 32px;
  color: #0d867f;
}
// é™„件删除图标
.delete-icon {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 24px;
  height: 24px;
  background-color: rgba(255, 0, 0, 0.8);
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
}
@media (max-width: 768px) {
  .attachment-item {
    width: calc(25% - 10px);
  }
}
// ç¼–辑模式下表单组件样式优化
:deep(.wd-form-item) {
  margin-bottom: 8rpx;
}
:deep(.wd-input, .wd-select, .wd-radio-group, .wd-checkbox-group) {
  width: 100%;
  box-sizing: border-box;
}
:deep(.wd-form-item__label) {
  &::after {
    content: "*";
    color: red;
    margin-left: 4rpx;
  }
}
// ä¿®å¤é€‰æ‹©å™¨æ ·å¼
:deep(.wd-select) {
  width: 100%;
}
:deep(.wd-checkbox) {
  margin-right: 0;
}
.conclusion-radio-group {
  display: flex;
  align-items: flex-start; // åž‚直方向顶部对齐(上移关键)
  gap: 20rpx; // é€‰é¡¹ä¹‹é—´çš„间距
}
</style>
src/pages/routingInspection/detail/indexLS.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,788 @@
<template>
  <view class="fixed-header">
    <view class="header-container">
      <wd-button
        icon="file-add"
        :round="false"
        size="small"
        custom-class="add_btn"
        @click="editList"
        v-if="!isEdit"
      >
        ç¼–辑
      </wd-button>
      <wd-button
        icon="close"
        type="info"
        :round="false"
        size="small"
        custom-class="add_btn"
        @click="close"
        v-if="isEdit"
      >
        å–消
      </wd-button>
      <wd-button
        icon="check"
        type="success"
        :round="false"
        size="small"
        custom-class="add_btn"
        @click="saveList"
        v-if="isEdit"
      >
        ä¿å­˜
      </wd-button>
      <view class="placeholder"></view>
      <view class="scan-info">
        <text class="scan-device-text">当前扫码机台: {{ scannedDeviceModel || "未扫码" }}</text>
      </view>
      <view class="scan-wrapper" @click="openScan">
        <wd-icon name="scan" size="24px" color="#0D867F"></wd-icon>
      </view>
    </view>
  </view>
  <view class="list">
    <!-- åŸºæœ¬ä¿¡æ¯æ¨¡å— -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "基本信息" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="日期" prop="recordDate">
          {{ formatDate(detailData.fixedInfo?.recordDate) }}
        </wd-form-item>
        <wd-form-item label="机台" prop="deviceModel">
          {{ formatValue(detailData.fixedInfo?.deviceModel) }}
        </wd-form-item>
        <wd-form-item label="班次" prop="workShift">
          {{ formatValue(detailData.fixedInfo?.workShift) }}
        </wd-form-item>
        <wd-form-item label="班组" prop="teamName">
          {{ formatValue(detailData.fixedInfo?.teamName) }}
        </wd-form-item>
        <wd-form-item label="单丝规格" prop="model">
          {{ formatValue(detailData.fixedInfo?.model) }}
        </wd-form-item>
        <wd-form-item label="生产轴数" prop="outputNumber">
          {{ formatValue(detailData.fixedInfo?.outputNumber, "è½´") }}
        </wd-form-item>
        <wd-form-item label="型号" prop="poleModel">
          {{ formatValue(detailData.fixedInfo?.poleModel) }}
        </wd-form-item>
        <wd-form-item label="批次" prop="poleNumber">
          {{ formatValue(detailData.fixedInfo?.poleNumber) }}
        </wd-form-item>
        <wd-form-item label="记录人" prop="createUserName">
          {{ formatValue(detailData.fixedInfo?.createUserName) }}
        </wd-form-item>
        <wd-form-item label="首检盘号" prop="firstNo">
          {{ formatValue(detailData.fixedInfo?.firstNo) }}
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- å·¥è‰ºè®°å½•详情模块 -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "工艺记录详情" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="巡检员" prop="processInspectionUserName">
          {{ detailData.processInspectionUserName || "-" }}
        </wd-form-item>
        <wd-form-item label="状态" prop="status">
          <wd-tag custom-class="space" :type="getStatusType(detailData.status)">
            {{ getStatusText(detailData.status) }}
          </wd-tag>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- æ£€éªŒç»“æžœ -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "检验结果" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item label="单丝直径" prop="dia">
          {{ formatValue(detailData.inspectionResult?.dia, "mm") || "-" }}
        </wd-form-item>
        <wd-form-item label="最大直径" prop="maxDia" required>
          <template v-if="isEdit">
            <wd-input v-model="formData.maxDia" placeholder="请输入最大直径(mm)" type="number" />
          </template>
          <template v-else>
            {{ formatValue(detailData.inspectionResult?.maxDia, "mm") || "-" }}
          </template>
        </wd-form-item>
        <wd-form-item label="最小直径" prop="minDia" required>
          <template v-if="isEdit">
            <wd-input v-model="formData.minDia" placeholder="请输入最小直径(mm)" type="number" />
          </template>
          <template v-else>
            {{ formatValue(detailData.inspectionResult?.minDia, "mm") || "-" }}
          </template>
        </wd-form-item>
        <wd-form-item label="外观" prop="appearance" required>
          <template v-if="isEdit">
            <view style="display: flex; flex-wrap: wrap; gap: 10px">
              <wd-checkbox
                v-for="(opt, idx) in appearanceOptions"
                :key="idx"
                :value="opt.value"
                :modelValue="formData.appearance?.includes(opt.value) || false"
                @click="handleAppearanceClick(opt.value)"
                style="width: 100px"
              >
                {{ opt.label }}
              </wd-checkbox>
            </view>
          </template>
          <template v-else>
            {{ formatProductAppearance(formData.appearance) }}
          </template>
        </wd-form-item>
        <wd-form-item label="卷绕紧密" prop="windingTightness" required>
          <template v-if="isEdit">
            <wd-radio-group
              v-model="formData.windingTightness"
              inline
              class="conclusion-radio-group"
            >
              <wd-radio
                v-for="(opt, idx) in sampleCompleteOptions"
                :key="idx"
                :value="opt.value"
                shape="dot"
              >
                {{ opt.label }}
              </wd-radio>
            </wd-radio-group>
          </template>
          <template v-else>
            {{ formatValue(detailData.inspectionResult?.windingTightness) }}
          </template>
        </wd-form-item>
        <wd-form-item label="排列整齐" prop="arrangementNeatness" required>
          <template v-if="isEdit">
            <wd-radio-group
              v-model="formData.arrangementNeatness"
              inline
              class="conclusion-radio-group"
            >
              <wd-radio
                v-for="(opt, idx) in sampleCompleteOptions"
                :key="idx"
                :value="opt.value"
                shape="dot"
              >
                {{ opt.label }}
              </wd-radio>
            </wd-radio-group>
          </template>
          <template v-else>
            {{ formatValue(detailData.inspectionResult?.arrangementNeatness) }}
          </template>
        </wd-form-item>
        <wd-form-item
          label="外层铝线离侧板边缘距离"
          prop="aluminumWireDistance"
          label-width="360rpx"
          required
        >
          <template v-if="isEdit">
            <wd-input
              v-model="formData.aluminumWireDistance"
              placeholder="请输入距离(mm)"
              type="number"
            />
          </template>
          <template v-else>
            {{ formatValue(detailData.inspectionResult?.aluminumWireDistance, "mm") || "-" }}
          </template>
        </wd-form-item>
        <wd-form-item label="成品模后接头情况" prop="jointCondition" label-width="280rpx" required>
          <template v-if="isEdit">
            <wd-radio-group v-model="formData.jointCondition" inline class="conclusion-radio-group">
              <wd-radio
                v-for="(opt, idx) in jointConditionOptions"
                :key="idx"
                :value="opt.value"
                shape="dot"
              >
                {{ opt.label }}
              </wd-radio>
            </wd-radio-group>
          </template>
          <template v-else>
            {{ formatValue(detailData.inspectionResult?.jointCondition) || "-" }}
          </template>
        </wd-form-item>
        <wd-form-item label="结论" prop="conclusion" required>
          <template v-if="isEdit">
            <wd-radio-group v-model="formData.conclusion" inline class="conclusion-radio-group">
              <wd-radio
                v-for="(opt, idx) in conclusionOptions"
                :key="idx"
                :value="opt.value"
                shape="dot"
              >
                {{ opt.label }}
              </wd-radio>
            </wd-radio-group>
          </template>
          <template v-else>
            {{ formatValue(detailData.inspectionResult?.conclusion) || "-" }}
          </template>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- å·¡æ£€ç»“æžœ -->
    <wd-row>
      <view style="margin: 10rpx">
        <text class="title">{{ "巡检结果" }}</text>
      </view>
      <wd-col :span="24">
        <wd-form-item
          label="铝杆前、中、尾样品是否齐全"
          prop="isFully"
          required
          label-width="420rpx"
        >
          <template v-if="isEdit">
            <wd-radio-group v-model="formData.isFully" inline class="conclusion-radio-group">
              <wd-radio
                v-for="(opt, idx) in sampleCompleteOptions"
                :key="idx"
                :value="opt.value"
                shape="dot"
              >
                {{ opt.label }}
              </wd-radio>
            </wd-radio-group>
          </template>
          <template v-else>
            <wd-tag
              custom-class="space"
              :type="detailData.processInspectionResult?.isFully ? 'success' : 'danger'"
            >
              {{ detailData.processInspectionResult?.isFully ? "是" : "否" }}
            </wd-tag>
          </template>
        </wd-form-item>
      </wd-col>
    </wd-row>
    <!-- é™„件模块 -->
    <wd-row class="attachment-section">
      <view style="margin: 10rpx">
        <text class="title">{{ "附件" }}</text>
      </view>
      <wd-col :span="24">
        <AttachmentUpload
          :detailData="detailData"
          :isEdit="isEdit"
          :deviceType="paramsType"
          ref="attachmentRef"
        />
      </wd-col>
    </wd-row>
    <wd-popup v-model="show" custom-style="border-radius:32rpx;" @close="handleClose">
      <div class="image-preview">
        <img :src="previewImageUrl" alt="预览图片" style="width: 100%; height: auto" />
      </div>
    </wd-popup>
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onUnmounted } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import RoutingInspectionApi from "@/api/routingInspection/routingInspection";
import { useToast } from "wot-design-uni";
import AttachmentUpload from "../upload.vue";
import { useUserStore } from "@/store/modules/user";
import { useScanCode } from "@/composables/useScanCode";
// æ ¸å¿ƒçŠ¶æ€
const paramsId = ref("");
const paramsType = ref("");
const detailData = ref<any>({});
const show = ref(false);
const previewImageUrl = ref("");
const isEdit = ref(false);
const tempFiles = ref<any[]>([]);
const toast = useToast();
const attachmentRef = ref<any>(null);
// èŽ·å–å½“å‰ç™»å½•ç”¨æˆ·ä¿¡æ¯
const userStore = useUserStore();
const userInfo: any = computed(() => userStore.userInfo);
// ä½¿ç”¨æ‰«ç ç®¡ç† composable(全局监听器,不随页面切换关闭)
const {
  deviceUid,
  deviceModel: scannedDeviceModel,
  loadFromCache,
  enableListener,
} = useScanCode("scanLS");
// è¡¨å•数据
const formData = reactive({
  dia: "",
  maxDia: "",
  minDia: "",
  appearance: [] as string[],
  windingTightness: "",
  arrangementNeatness: "",
  aluminumWireDistance: "",
  jointCondition: "",
  conclusion: "",
  isFully: "",
});
// å¤–观选项
const appearanceOptions = [
  { label: "无外观问题", value: "无外观问题" },
  { label: "表面划伤", value: "表面划伤" },
  { label: "直径不均", value: "直径不均" },
  { label: "其他缺陷", value: "其他缺陷" },
];
const sampleCompleteOptions = [
  { label: "是", value: "是" },
  { label: "否", value: "否" },
];
const jointConditionOptions = [
  { label: "有", value: "有" },
  { label: "无", value: "无" },
];
const conclusionOptions = [
  { label: "合格", value: "合格" },
  { label: "不合格", value: "不合格" },
];
// çŠ¶æ€æ˜ å°„
const getStatusType = (status: number) => {
  switch (status) {
    case 0:
      return "warning";
    case 1:
      return "danger";
    case 2:
      return "primary";
    case 3:
      return "success";
    default:
      return "default";
  }
};
const getStatusText = (status: number) => {
  switch (status) {
    case 0:
      return "待巡检";
    case 1:
      return "已驳回";
    case 2:
      return "待审核";
    case 3:
      return "通过";
    default:
      return "未知";
  }
};
// æ ¼å¼åŒ–工具
const formatProductAppearance = (productAppearance: string[]) => {
  if (!productAppearance || !Array.isArray(productAppearance) || !productAppearance.length) {
    return "-";
  }
  return productAppearance.join("、");
};
// å¤„理外观选择的互斥逻辑
const handleAppearanceClick = (value: string) => {
  // ç¡®ä¿ appearance æ˜¯æ•°ç»„
  if (!Array.isArray(formData.appearance)) {
    formData.appearance = [];
  }
  const currentValues = [...formData.appearance];
  const isCurrentlyChecked = currentValues.includes(value);
  let newSelection: string[] = [];
  if (value === "无外观问题") {
    if (isCurrentlyChecked) {
      // å–消选中"无外观问题"
      newSelection = [];
    } else {
      // é€‰ä¸­"无外观问题",清空其他选项
      newSelection = ["无外观问题"];
    }
  } else {
    // ç‚¹å‡»å…¶ä»–选项
    if (isCurrentlyChecked) {
      // å–消选中该选项
      newSelection = currentValues.filter((v) => v !== value);
    } else {
      // é€‰ä¸­è¯¥é€‰é¡¹ï¼Œç§»é™¤"无外观问题"
      const filteredValues = currentValues.filter((v) => v !== "无外观问题");
      newSelection = [...filteredValues, value];
    }
  }
  formData.appearance = newSelection;
};
const formatValue = (value: any, unit?: string) => {
  if (value === null || value === undefined || value === "") return "-";
  return unit ? `${value}${unit}` : value;
};
const formatDate = (date: string) => {
  if (!date) return "-";
  return new Date(date).toLocaleDateString("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  });
};
// åˆå§‹åŒ–表单
const initFormData = () => {
  const inspectionResult = detailData.value.inspectionResult || {};
  const processInspectionResult = detailData.value.processInspectionResult || {};
  formData.dia = inspectionResult.dia || "";
  formData.maxDia = inspectionResult.maxDia || "";
  formData.minDia = inspectionResult.minDia || "";
  // ç¡®ä¿ appearance æ˜¯æ•°ç»„
  formData.appearance = Array.isArray(inspectionResult.appearance)
    ? inspectionResult.appearance
    : inspectionResult.appearance
      ? [inspectionResult.appearance]
      : [];
  formData.windingTightness = inspectionResult.windingTightness || "";
  formData.arrangementNeatness = inspectionResult.arrangementNeatness || "";
  formData.aluminumWireDistance = inspectionResult.aluminumWireDistance || "";
  formData.jointCondition = inspectionResult.jointCondition || "";
  formData.conclusion = inspectionResult.conclusion || "";
  formData.isFully = processInspectionResult.isFully ? "是" : "否";
};
// èŽ·å–è¯¦æƒ…
const getDetailData = async (id: string, deviceType: string) => {
  try {
    const response = await RoutingInspectionApi.getDrawInspectInfoById({ id });
    detailData.value = response.data;
    // å¦‚果巡检员为空,默认设置为当前登录用户
    if (!detailData.value.processInspectionUserName) {
      detailData.value.processInspectionUserName =
        userInfo.value?.nickName || userInfo.value?.userName || "";
    }
    tempFiles.value = [];
    initFormData();
  } catch (error) {
    console.error("获取详情失败:", error);
  }
};
// é¡µé¢åŠ è½½
onLoad((options: any) => {
  paramsId.value = options.id;
  paramsType.value = options.deviceType;
  getDetailData(options.id, options.deviceType);
});
// ç¼–辑切换
const editList = () => {
  isEdit.value = true;
};
// å–消编辑
const close = () => {
  isEdit.value = false;
  tempFiles.value = [];
  initFormData();
};
// ä¿å­˜ç¼–辑
const saveList = async () => {
  // æ ¡éªŒ
  if (!formData.maxDia) return uni.showToast({ title: "最大直径为必填项", icon: "none" });
  if (!formData.minDia) return uni.showToast({ title: "最小直径为必填项", icon: "none" });
  if (!formData.appearance.length) return uni.showToast({ title: "外观为必填项", icon: "none" });
  if (!formData.windingTightness) return uni.showToast({ title: "卷绕紧密为必填项", icon: "none" });
  if (!formData.arrangementNeatness)
    return uni.showToast({ title: "排列整齐为必填项", icon: "none" });
  if (!formData.aluminumWireDistance)
    return uni.showToast({ title: "外层铝线离侧板边缘距离为必填项", icon: "none" });
  if (!formData.jointCondition)
    return uni.showToast({ title: "成品模后接头情况为必填项", icon: "none" });
  if (!formData.conclusion) return uni.showToast({ title: "结论为必填项", icon: "none" });
  if (!formData.isFully) return uni.showToast({ title: "铝杆样品是否齐全为必填项", icon: "none" });
  // éªŒè¯æ‰«ç æ•°æ®ï¼ˆä»Žç¼“存或新扫码获取)
  console.log("保存前检查 deviceUid:", deviceUid.value);
  if (!deviceUid.value) {
    return uni.showToast({
      title: "请先扫描设备二维码",
      icon: "none",
      duration: 2000,
    });
  }
  const { newFiles } = attachmentRef.value.getSubmitFiles();
  console.log("newFiles", newFiles);
  const allFileIds = [...newFiles];
  // æäº¤
  try {
    const res = await RoutingInspectionApi.drawPatrolCheckInspection({
      deviceUid: deviceUid.value,
      id: paramsId.value,
      inspectionResult: {
        dia: formData.dia,
        maxDia: formData.maxDia,
        minDia: formData.minDia,
        appearance: formData.appearance,
        windingTightness: formData.windingTightness,
        arrangementNeatness: formData.arrangementNeatness,
        aluminumWireDistance: formData.aluminumWireDistance,
        jointCondition: formData.jointCondition,
        conclusion: formData.conclusion,
      },
      result: { isFully: formData.isFully },
      processInspectionAttachmentList: allFileIds,
    });
    if (res.code === 200) {
      // è®¾ç½®åˆ·æ–°æ ‡è®°ï¼Œå‘Šè¯‰åˆ—表页需要刷新
      uni.setStorageSync("needRefreshInspectionList", true);
      uni.showToast({
        title: "保存成功",
        icon: "success",
        duration: 1500,
      });
      // å»¶è¿Ÿè¿”回列表页,让用户看到成功提示
      setTimeout(() => {
        uni.navigateBack({
          delta: 1,
        });
      }, 1500);
    } else {
      uni.showModal({ title: res.msg || "保存失败", icon: "error" });
    }
  } catch (e) {
    console.error("保存失败:", e);
    uni.showModal({ title: e.message || "保存失败", icon: "error" });
  }
};
const handleClose = () => {
  show.value = false;
};
const openScan = () => {
  console.log("indexLS - ç‚¹å‡»æ‰«ç æŒ‰é’®ï¼ˆå…¨å±€æ‰«ç æ¨¡å¼ï¼Œæ— éœ€æ‰‹åŠ¨è§¦å‘ï¼‰");
  // å…¨å±€æ‰«ç æ¨¡å¼ä¸‹ï¼Œç¡¬ä»¶æ‰«ç ä¼šè‡ªåŠ¨è§¦å‘ï¼Œæ— éœ€æ‰‹åŠ¨è°ƒç”¨
  uni.showToast({
    title: "请使用扫码枪扫描",
    icon: "none",
  });
};
// é¡µé¢æ˜¾ç¤ºæ—¶çš„处理
onShow(() => {
  console.log("========== indexLS - onShow è§¦å‘ ==========");
  // é‡æ–°å¯ç”¨ç›‘听器(确保监听器有效)
  enableListener();
  // åŠ è½½ç¼“å­˜ï¼ˆæ›´æ–°UI显示)
  const cachedData = loadFromCache();
  // å¦‚果没有缓存数据,提示用户需要扫码
  if (!cachedData || !cachedData.uid) {
    console.log("⚠️ æœªæ£€æµ‹åˆ°æ‰«ç ç¼“存,用户需要扫描设备二维码");
    // åœ¨ç¼–辑模式下才提示
    if (isEdit.value) {
      setTimeout(() => {
        uni.showToast({
          title: "请扫描设备二维码后再保存",
          icon: "none",
          duration: 2000,
        });
      }, 500);
    }
  }
});
</script>
<style lang="scss" scoped>
.fixed-header {
  position: fixed;
  top: 44;
  left: 0;
  right: 0;
  background: #f3f9f8;
  z-index: 999;
  padding: 12px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  min-height: 60px;
  box-sizing: border-box;
  overflow: visible;
}
.header-container {
  display: flex;
  align-items: center;
  width: 100%;
  gap: 10px;
}
.placeholder {
  flex: 1;
}
.scan-info {
  display: flex;
  align-items: center;
  margin-right: 10px;
  .scan-device-text {
    font-size: 14px;
    color: #0d867f;
    font-weight: 500;
  }
}
.scan-wrapper {
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 6px;
  flex-shrink: 0;
}
.list {
  padding: 12px;
  padding-top: 84px;
  background: #f3f9f8;
  min-height: 100vh;
  box-sizing: border-box;
  overflow-y: auto;
}
.title {
  position: relative;
  margin-left: 10px;
  font-size: 16px;
  font-weight: 500;
  color: #0d867f;
}
.title::after {
  position: absolute;
  content: "";
  top: 4px;
  left: -10px;
  width: 4px;
  height: 16px;
  background: #0d867f;
  border-radius: 2px;
}
.attachment-section {
  width: 100%;
}
.attachment-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 10px 0;
}
.attachment-item {
  width: calc(25% - 10px);
  box-sizing: border-box;
  position: relative;
}
.upload-btn {
  width: 80px;
  height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}
.upload-icon {
  font-size: 32px;
  color: #0d867f;
}
.delete-icon {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 24px;
  height: 24px;
  background-color: rgba(255, 0, 0, 0.8);
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
}
@media (max-width: 768px) {
  .attachment-item {
    width: calc(25% - 10px);
    margin: 10;
  }
}
:deep(.wd-form-item) {
  margin-bottom: 8rpx;
}
:deep(.wd-input, .wd-select, .wd-radio-group, .wd-checkbox-group) {
  width: 100%;
  box-sizing: border-box;
}
:deep(.wd-form-item__label)::after {
  content: "*";
  color: red;
  margin-left: 4rpx;
}
:deep(.wd-select) {
  width: 100%;
}
:deep(.wd-checkbox) {
  margin-right: 0;
}
.conclusion-radio-group {
  display: flex;
  align-items: flex-start; // åž‚直方向顶部对齐(上移关键)
  gap: 20rpx; // é€‰é¡¹ä¹‹é—´çš„间距
}
</style>
src/pages/routingInspection/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
<template>
  <view>
    <wd-row>
      <wd-col :span="21">
        <wd-search
          v-model="searchKeyword"
          placeholder="请输入班组名称"
          placeholder-left
          hide-cancel
          @search="handleSearch"
          @clear="handleClear"
        ></wd-search>
      </wd-col>
      <wd-col :span="3">
        <view class="scan_box" @click="openScan">
          <wd-icon name="scan" size="24px" color="#0D867F"></wd-icon>
        </view>
      </wd-col>
    </wd-row>
    <wd-tabs v-model="tab" auto-line-width slidable="always" :map-num="patrolList.length">
      <wd-tab
        v-for="(item, index) in patrolList"
        :key="index"
        :title="`${item.deviceModel}(待检查${item.pendingNum}条)`"
        class="tab_bg"
      >
        <ProductList
          :key="searchKey"
          :api="RoutingInspectionApi.getInspectListByPatrol"
          :ProList="{ ...item, teamName: searchKeyword }"
        />
      </wd-tab>
    </wd-tabs>
    <wd-toast />
  </view>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
import { onShow, onHide } from "@dcloudio/uni-app";
import ProductList from "./list/index.vue";
import { useUserStore } from "@/store/modules/user";
import reportApi from "@/api/work/report";
import { useToast } from "wot-design-uni";
import RoutingInspectionApi from "@/api/routingInspection/routingInspection";
import { useScanCode } from "@/composables/useScanCode";
const userStore = useUserStore();
const userInfo: any = computed(() => userStore.userInfo);
const toast = useToast();
const tab = ref<number>(0);
const patrolList = ref<any[]>([]); // å·¡æ£€è®¾å¤‡åˆ—表数据
const searchKeyword = ref<string>(""); // æœç´¢å…³é”®è¯ï¼ˆç­ç»„名称)
const searchKey = ref<number>(0); // ç”¨äºŽå¼ºåˆ¶åˆ·æ–°åˆ—表
// ä½¿ç”¨æ‰«ç ç®¡ç† composable(全局监听器,不随页面切换关闭)
const { deviceUid, deviceModel, hasScanned, displayText, loadFromCache, enableListener } =
  useScanCode("scanIndex");
const handlePatrolData = (index: number, count: number) => {
  // å¯ä»¥åœ¨è¿™é‡Œæ›´æ–°ç‰¹å®šå·¡æ£€è®¾å¤‡çš„待检查数量
  // ä¾‹å¦‚:patrolList.value[index].pendingNum = count;
};
// å¤„理搜索
const handleSearch = (value: string) => {
  console.log("搜索班组:", value);
  searchKey.value++; // æ›´æ–° key å¼ºåˆ¶åˆ·æ–°åˆ—表
};
// å¤„理清空搜索
const handleClear = () => {
  console.log("清空搜索");
  searchKeyword.value = "";
  searchKey.value++; // æ›´æ–° key å¼ºåˆ¶åˆ·æ–°åˆ—表
};
const openScan = () => {
  console.log("index.vue - ç‚¹å‡»æ‰«ç æŒ‰é’®ï¼ˆå…¨å±€æ‰«ç æ¨¡å¼ï¼Œæ— éœ€æ‰‹åŠ¨è§¦å‘ï¼‰");
  // å…¨å±€æ‰«ç æ¨¡å¼ä¸‹ï¼Œç¡¬ä»¶æ‰«ç ä¼šè‡ªåŠ¨è§¦å‘ï¼Œæ— éœ€æ‰‹åŠ¨è°ƒç”¨
  uni.showToast({
    title: "请使用扫码枪扫描",
    icon: "none",
  });
};
// èŽ·å–å·¡æ£€è®¾å¤‡åˆ—è¡¨
const loadPatrolList = async () => {
  try {
    const { data } = await RoutingInspectionApi.getDeviceInspectListByPatrol({});
    if (data) {
      patrolList.value = data;
    }
  } catch (error) {
    toast.error("获取巡检设备列表失败");
  }
};
onMounted(() => {
  // é¡µé¢åŠ è½½æ—¶èŽ·å–å·¡æ£€è®¾å¤‡åˆ—è¡¨
  loadPatrolList();
  // å¯ç”¨å…¨å±€ç›‘听器
  enableListener();
  console.log("index.vue - onMounted");
});
onShow(() => {
  console.log("========== index.vue - onShow è§¦å‘ ==========");
  // é¡µé¢æ˜¾ç¤ºæ—¶é‡æ–°å¯ç”¨ç›‘听器(确保监听器有效)
  enableListener();
  // åŠ è½½ç¼“å­˜ï¼ˆæ›´æ–°UI显示)
  loadFromCache();
  // æ£€æŸ¥æ˜¯å¦éœ€è¦åˆ·æ–°åˆ—表(只有提交成功后才刷新)
  const needRefresh = uni.getStorageSync("needRefreshInspectionList");
  if (needRefresh) {
    console.log("检测到需要刷新列表,开始刷新...");
    // é‡æ–°åŠ è½½å·¡æ£€è®¾å¤‡åˆ—è¡¨ï¼ˆåˆ·æ–°å¾…æ£€æŸ¥æ•°é‡ï¼‰
    loadPatrolList();
    // å¼ºåˆ¶åˆ·æ–° ProductList ç»„ä»¶
    searchKey.value++;
    // æ¸…除刷新标记
    uni.removeStorageSync("needRefreshInspectionList");
  }
});
</script>
<style lang="scss" scoped>
::v-deep .wd-search__block {
  border-radius: unset;
}
.scan_box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 6px;
  background: #fff;
}
::v-deep .wd-tabs__line {
  background: #0d867f;
}
::v-deep .wd-tabs__nav {
  border-bottom: 1px #dddddd solid;
}
.tab_bg {
  background: #f3f9f8;
}
.icon_box {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  background: #e7f4ec99;
  border-radius: 50%;
}
.statistics_box {
  margin: 15px;
}
</style>
src/pages/routingInspection/list/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
<template>
  <view class="card_box">
    <z-paging
      ref="pagingRef"
      v-model="list"
      :fixed="false"
      :auto-show-back-to-top="true"
      @query="getList"
    >
      <ProductCard
        v-for="(item, index) in list"
        :key="index"
        :data="item"
        :map="map"
        @click="toDetail(item.id, item.deviceType)"
      />
    </z-paging>
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import ProductCard from "../product_card/index.vue";
import { useUserStore } from "@/store/modules/user";
import zPaging from "@/components/z-paging/z-paging.vue";
import { useToast } from "wot-design-uni";
const toast = useToast();
const userStore = useUserStore();
const userInfo: any = computed(() => userStore.userInfo);
const pagingRef = ref();
const map = reactive({
  deviceModel: "deviceModel",
  model: "model",
  firstNo: "firstNo",
  recordDate: "recordDate",
  workShift: "workShift",
  teamName: "teamName",
  poleModel: "poleModel",
  poleNumber: "poleNumber",
  outputNumber: "outputNumber",
  inspectPerson: "inspectPerson",
  status: "status",
  productType: "productType",
  recordPosition: "recordPosition",
  rejectList: [
    {
      rejectPerson: "rejectPerson",
      rejectTime: "rejectTime",
      rejectReason: {
        reason: "reason",
      },
    },
  ], // æ”¹ä¸ºå¯¹è±¡ï¼ŒåŒ…含所需的嵌套属性
});
const props = defineProps({
  api: {
    type: Function,
    default: () => {},
  },
  ProList: {
    type: Object,
    default: () => {},
  },
});
const list = ref<any[]>([]);
const toDetail = (id: number, deviceType: number) => {
  console.log("点击卡片", id, deviceType);
  if (deviceType == 1) {
    // ç»žçº¿
    uni.navigateTo({
      url: `/pages/routingInspection/detail/indexJX?id=${id}&deviceType=${deviceType}`,
    });
  } else if (deviceType == 0) {
    // æ‹‰ä¸
    uni.navigateTo({
      url: `/pages/routingInspection/detail/indexLS?id=${id}&deviceType=${deviceType}`,
    });
  }
};
const getList = async (pageNo = 1, pageSize = 10) => {
  const { code, data } = await props.api({
    deviceModel: props.ProList.deviceModel,
    status: "0",
    deviceType: props.ProList.deviceType,
    teamName: props.ProList.teamName || "", // ç­ç»„名称搜索
    current: pageNo,
    size: pageSize,
  });
  if (code == 200) {
    map.deviceModel = "deviceModel";
    map.model = "model";
    map.firstNo = "firstNo";
    map.recordDate = "recordDate";
    map.workShift = "workShift";
    map.teamName = "teamName";
    map.poleModel = "poleModel";
    map.poleNumber = "poleNumber";
    map.outputNumber = "outputNumber";
    map.inspectPerson = "inspectPerson";
    map.productType = "productType";
    map.recordPosition = "recordPosition";
    map.rejectList = [
      {
        rejectPerson: "rejectPerson",
        rejectTime: "rejectTime",
        rejectReason: {
          reason: "reason",
        },
      },
    ];
    map.status = "status";
    if (data.total == 0) {
      pagingRef.value.complete(true);
    } else {
      console.log("data.records", data.records);
      pagingRef.value.complete(data.records);
    }
  }
};
</script>
<style lang="scss" scoped>
.card_box {
  height: calc(100vh - 120px);
}
</style>
src/pages/routingInspection/product_card/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
<template>
  <wd-card class="card_bg" @click="handleCardClick">
    <template #title>
      <view class="flex justify-between w-full">
        <text class="font-medium text-[#252525]">记录位置: {{ data[map.recordPosition] }}</text>
        <wd-tag color="#0D867F" bg-color="#E7F4EC">
          <text class="text-xs">{{ data[map.model] }}</text>
        </wd-tag>
      </view>
    </template>
    <wd-row class="my-2">
      <wd-col :span="24">
        <view class="flex">
          <view class="icon_box">
            <wd-icon name="folder" color="#0D867F"></wd-icon>
          </view>
          <text class="text-[#646874] mx-2">
            ç­æ¬¡:
            <text class="text-[#252525]">{{ data[map.workShift] }}</text>
          </text>
        </view>
      </wd-col>
    </wd-row>
    <wd-row class="my-2">
      <wd-col :span="24">
        <view class="flex">
          <view class="icon_box">
            <wd-icon name="folder" color="#0D867F"></wd-icon>
          </view>
          <text class="text-[#646874] mx-2">
            ç­ç»„:
            <text class="text-[#252525]">
              {{ data[map.teamName]?.slice(-2) || data[map.teamName] }}
            </text>
          </text>
        </view>
      </wd-col>
    </wd-row>
    <wd-row class="my-2" v-if="data[map.productType]">
      <wd-col :span="24">
        <view class="flex">
          <view class="icon_box">
            <wd-icon name="folder" color="#0D867F"></wd-icon>
          </view>
          <text class="text-[#646874] mx-2">
            äº§å“ç±»åˆ«:
            <text class="text-[#252525]">{{ data[map.productType] }}</text>
          </text>
        </view>
      </wd-col>
    </wd-row>
    <wd-row class="my-2">
      <wd-col :span="12">
        <view class="flex">
          <view class="icon_box">
            <wd-icon name="folder" color="#0D867F"></wd-icon>
          </view>
          <text class="text-[#646874] mx-2">
            è‡ªæ£€äºº:
            <text class="text-[#252525]">{{ data[map.inspectPerson] }}</text>
          </text>
        </view>
      </wd-col>
      <wd-col :span="12">
        <view class="flex">
          <view class="icon_box">
            <wd-icon name="folder" color="#0D867F"></wd-icon>
          </view>
          <text class="text-[#646874] mx-2">
            çŠ¶æ€:
            <text class="text-[#252525]">{{ data[map.status] == 1 ? "被驳回" : "巡检" }}</text>
          </text>
        </view>
      </wd-col>
    </wd-row>
    <wd-row class="my-2">
      <wd-col :span="16">
        <view class="flex">
          <view class="icon_box">
            <wd-icon name="folder" color="#0D867F"></wd-icon>
          </view>
          <text class="text-[#646874] mx-2">
            è®°å½•æ—¶é—´:
            <text class="text-[#252525]">{{ data[map.recordDate] }}</text>
          </text>
        </view>
      </wd-col>
      <wd-col :span="8">
        <view class="flex" @click.stop>
          <wd-button
            v-if="data[map.status] == 1"
            size="small"
            type="primary"
            @click="showRejectPopup = true"
            style="margin-left: auto"
          >
            æŸ¥çœ‹é©³å›žä¿¡æ¯
          </wd-button>
        </view>
      </wd-col>
    </wd-row>
  </wd-card>
  <wd-popup
    v-model="showRejectPopup"
    title="驳回信息"
    custom-style="border-radius:32rpx;height: 800rpx;width: 600rpx;"
  >
    <wd-card
      v-for="(item, index) in data.rejectList"
      :key="index"
      :class="index % 2 === 0 ? 'reject-card-bg-1' : 'reject-card-bg-2'"
      style="margin-bottom: 8px; padding: 10px; border-radius: 8px"
    >
      <view class="content">
        <view>
          <view
            style="
              display: flex;
              justify-content: space-between;
              align-items: center;
              color: rgba(0, 0, 0, 0.85);
              font-size: 14px;
              margin-bottom: 8px;
            "
          >
            <view>{{ item.rejectPerson }}</view>
            <view>{{ item.rejectTime }}</view>
          </view>
          <view
            style="
              color: rgba(0, 0, 0, 0.85);
              font-size: 14px;
              word-break: break-word;
              overflow-wrap: break-word;
              max-width: 100%;
              padding: 5px 0;
            "
          >
            {{ item.rejectReason.reason }}
          </view>
        </view>
      </view>
    </wd-card>
  </wd-popup>
</template>
<script setup lang="ts">
import { ref } from "vue";
const emit = defineEmits(["click"]);
defineProps({
  data: {
    type: Object,
    default: () => {},
  },
  map: {
    type: Object,
    default: () => {},
  },
});
const showRejectPopup = ref<boolean>(false);
const handleCardClick = () => {
  emit("click");
};
</script>
<style lang="scss" scoped>
.card_bg {
  box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
  padding-bottom: 10px;
}
// æ·»åŠ ï¼šä¸¤ç§ä¸åŒçš„èƒŒæ™¯è‰²æ ·å¼
.reject-card-bg-1 {
  background-color: #f5f7fa;
}
.reject-card-bg-2 {
  background-color: #eef2f7;
}
.page-class {
  :deep() {
    .custom-shadow {
      box-shadow:
        0 3px 1px -2px rgb(0 0 0 / 20%),
        0 2px 2px 0 rgb(0 0 0 / 14%),
        0 1px 5px 0 rgb(0 0 0 / 12%);
    }
  }
}
.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
// ä¿®æ”¹ï¼šè°ƒæ•´å†…容区域的样式
.content {
  padding: 5px;
}
</style>
src/pages/routingInspection/upload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,587 @@
<template>
  <view class="attachment-container">
    <!-- å¤´éƒ¨æ“ä½œåŒº -->
    <view class="header-actions">
      <wd-button
        icon="file-add"
        :round="false"
        size="small"
        custom-class="add_btn"
        @click="addAttachment"
        v-if="isEdit"
      >
        æ–°å¢ž
      </wd-button>
    </view>
    <!-- é™„件列表 -->
    <view class="attachment-list">
      <wd-status-tip
        v-if="attachmentList.length === 0"
        image="content"
        tip="暂无附件"
        custom-class="status-tip-full"
      />
      <view v-for="(item, index) in attachmentList" :key="item.id || index" class="attachment-card">
        <view class="media-wrapper" @click="previewAttachment(item)">
          <!-- å›¾ç‰‡é¢„览 -->
          <template v-if="isImageType(item.url)">
            <image
              :src="getFullUrl(item.url)"
              mode="aspectFill"
              class="media-preview"
              style="width: 100%; height: 100%"
              @error="onImageError(item, index)"
              @load="onImageLoad(item, index)"
              :show-menu-by-longpress="true"
            />
            <!-- åŠ è½½ä¸­é®ç½© -->
            <view v-if="item.loading" class="loading-mask">
              <text class="loading-text">加载中...</text>
            </view>
            <!-- å›¾ç‰‡åŠ è½½å¤±è´¥æ˜¾ç¤ºé»˜è®¤å›¾æ ‡ -->
            <view v-if="item.loadError" class="file-icon-wrapper error-overlay">
              <wd-icon name="picture" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- è§†é¢‘预览 -->
          <template v-else-if="isVideoType(item.url)">
            <video
              :src="getFullUrl(item.url)"
              class="media-preview"
              :controls="false"
              :show-center-play-btn="true"
              @error="onVideoError(item, index)"
              object-fit="cover"
            />
            <!-- è§†é¢‘加载失败显示默认图标 -->
            <view v-if="item.loadError" class="file-icon-wrapper error-overlay">
              <wd-icon name="video" size="48px" color="#ccc" />
              <text class="file-name error-text">加载失败</text>
            </view>
          </template>
          <!-- å…¶ä»–文件类型显示图标 -->
          <view v-else class="file-icon-wrapper">
            <wd-icon name="file-outline" size="48px" color="#999" />
            <text class="file-name">文件</text>
          </view>
          <!-- åˆ é™¤æŒ‰é’® -->
          <view class="delete-btn" @click.stop="deleteAttachment(item.id)" v-if="isEdit">
            <wd-icon name="delete" color="#fff" size="20px" />
          </view>
        </view>
      </view>
    </view>
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { useToast } from "wot-design-uni";
import AttachmentAPI from "@/api/product/attachment";
// H5 ä½¿ç”¨ VITE_APP_BASE_API ä½œä¸ºä»£ç†è·¯å¾„,其他平台使用 VITE_APP_API_URL ä½œä¸ºè¯·æ±‚路径
let baseUrlValue = import.meta.env.VITE_APP_API_URL || "";
// #ifdef H5
baseUrlValue = import.meta.env.VITE_APP_BASE_API || "";
// #endif
const baseUrl = ref(baseUrlValue); // ä½¿ç”¨ref使其在模板中可访问
// å¤–部参数
const props = defineProps({
  detailData: { type: Object, default: () => ({}) },
  isEdit: { type: Boolean, default: false },
  deviceType: { type: String, default: "" },
});
const toast = useToast();
// èŽ·å–åˆå§‹æ•°æ®
const getInitialData = () => {
  // å¤„理不同的数据结构
  let data = props.detailData;
  // å¦‚果是 ref å¯¹è±¡ï¼ŒèŽ·å–å…¶ value
  if (data && typeof data === "object" && "value" in data) {
    data = data.value;
  }
  // å¦‚果是数组,直接返回
  if (Array.isArray(data)) {
    return data.map((item) => ({
      ...item,
      loading: false,
      loadError: false,
    }));
  }
  // å¦‚果有 files å±žæ€§
  if (data && data.files) {
    const files = Array.isArray(data.files) ? data.files : [];
    return files.map((item) => ({
      ...item,
      loading: false,
      loadError: false,
    }));
  }
  return [];
};
const attachmentList = ref<any[]>(getInitialData());
const attachmentIds = ref<string[]>(attachmentList.value.map((item: any) => item.id) || []);
// ç›‘听 props.detailData å˜åŒ–
watch(
  () => props.detailData,
  (newVal) => {
    const newData = getInitialData();
    if (newData.length > 0) {
      attachmentList.value = newData.map((item) => ({
        ...item,
        loading: false,
        loadError: false,
      }));
      attachmentIds.value = newData.map((item: any) => item.id);
    }
  },
  { deep: true, immediate: false }
);
// èŽ·å–å®Œæ•´çš„å›¾ç‰‡/视频 URL
const getFullUrl = (url: string) => {
  if (!url) return "";
  // å¦‚果已经是完整的 URL(http æˆ– https å¼€å¤´ï¼‰ï¼Œç›´æŽ¥è¿”回
  if (url.startsWith("http://") || url.startsWith("https://")) {
    return url;
  }
  // æ£€æŸ¥ baseUrl æ˜¯å¦æœ‰æ•ˆ
  if (!baseUrl.value) {
    console.error("❌ baseUrl未配置,url:", url);
    return url;
  }
  // å¦‚果是相对路径,拼接基础 URL
  const separator = url.startsWith("/") || baseUrl.value.endsWith("/") ? "" : "/";
  return `${baseUrl.value}${separator}${url}`;
};
// å›¾ç‰‡åŠ è½½æˆåŠŸ
const onImageLoad = (item: any, index: number) => {
  item.loading = false;
  item.loadError = false;
  attachmentList.value = [...attachmentList.value];
};
// å›¾ç‰‡åŠ è½½å¤±è´¥
const onImageError = (item: any, index: number) => {
  console.error(`图片加载失败 [${index}]:`, item.url);
  item.loading = false;
  item.loadError = true;
  attachmentList.value = [...attachmentList.value];
};
// è§†é¢‘加载失败
const onVideoError = (item: any, index: number) => {
  console.error(`视频加载失败 [${index}]:`, item.url);
  item.loading = false;
  item.loadError = true;
  attachmentList.value = [...attachmentList.value];
};
// æ–°å¢žé™„ä»¶
const addAttachment = () => {
  // æ˜¾ç¤ºé€‰æ‹©æ–‡ä»¶ç±»åž‹çš„弹窗
  uni.showActionSheet({
    itemList: ["选择图片", /* "选择视频", */ "拍照" /* , "录像" */],
    success: (res) => {
      switch (res.tapIndex) {
        case 0: // é€‰æ‹©å›¾ç‰‡
          chooseImages();
          break;
        // case 1: // é€‰æ‹©è§†é¢‘
        //   chooseVideos();
        //   break;
        case 1: // æ‹ç…§
          takePhoto();
          break;
        // case 3: // å½•像
        //   recordVideo();
        //   break;
      }
    },
    fail: (error) => {
      console.error("选择文件类型失败:", error);
      toast.show("选择文件类型失败");
    },
  });
};
// é€‰æ‹©å›¾ç‰‡
const chooseImages = () => {
  uni.chooseImage({
    count: 9,
    sizeType: ["original", "compressed"],
    sourceType: ["album"],
    success: async (res) => {
      const filePaths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : [res.tempFilePaths];
      await handleFileUpload(filePaths);
    },
    fail: (error) => {
      console.error("选择图片失败:", error);
      toast.show("选择图片失败");
    },
  });
};
// é€‰æ‹©è§†é¢‘
const chooseVideos = () => {
  uni.chooseVideo({
    sourceType: ["album"],
    maxDuration: 60,
    camera: "back",
    success: async (res) => {
      await handleFileUpload([res.tempFilePath]);
    },
    fail: (error) => {
      console.error("选择视频失败:", error);
      toast.show("选择视频失败");
    },
  });
};
// æ‹ç…§
const takePhoto = () => {
  uni.chooseImage({
    count: 1,
    sizeType: ["original", "compressed"],
    sourceType: ["camera"],
    success: async (res) => {
      const filePaths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : [res.tempFilePaths];
      await handleFileUpload(filePaths);
    },
    fail: (error) => {
      console.error("拍照失败:", error);
      toast.show("拍照失败");
    },
  });
};
// å½•像
const recordVideo = () => {
  uni.chooseVideo({
    sourceType: ["camera"],
    maxDuration: 60,
    camera: "back",
    success: async (res) => {
      await handleFileUpload([res.tempFilePath]);
    },
    fail: (error) => {
      console.error("录像失败:", error);
      toast.show("录像失败");
    },
  });
};
// å¤„理文件上传
const handleFileUpload = async (filePaths: string[]) => {
  try {
    toast.show("正在上传...");
    // ä¸Šä¼ æ–‡ä»¶
    const uploadResults: any = await AttachmentAPI.uploadAttachmentFiles(filePaths);
    const result = uploadResults.map((it: any) => {
      return it.data;
    });
    // æ›´æ–°é™„件列表
    const flattenedResult = result.flat();
    attachmentList.value.push(...flattenedResult);
    // æå–附件ID
    attachmentIds.value = attachmentList.value.map((item: any) => item.id);
    toast.show("上传成功");
  } catch (error) {
    console.error("上传失败:", error);
    toast.show("上传失败");
  }
};
// åˆ é™¤é™„ä»¶
const deleteAttachment = async (aid: number) => {
  try {
    uni.showModal({
      title: "确认删除",
      content: "确定要删除这个附件吗?",
      success: async (res) => {
        if (res.confirm) {
          // å‰ç«¯æ‰‹åŠ¨åˆ é™¤ï¼šç›´æŽ¥ä»Žåˆ—è¡¨ä¸­ç§»é™¤è¿™æ¡æ•°æ®
          attachmentList.value = attachmentList.value.filter((item) => item.id !== aid);
          // èŽ·å–å‰©ä½™çš„é™„ä»¶ID
          attachmentIds.value = attachmentList.value.map((item) => item.id);
          toast.show("删除成功");
        }
      },
    });
  } catch (error) {
    console.error("删除失败:", error);
    toast.show("删除失败");
  }
};
// é¢„览附件
const previewAttachment = (item: any) => {
  // æ ¹æ®æ–‡ä»¶ç±»åž‹è¿›è¡Œé¢„览
  const fileType = getFileType(item.url);
  const fullUrl = getFullUrl(item.url);
  if (fileType.startsWith("image")) {
    // å›¾ç‰‡é¢„览
    uni.previewImage({
      urls: [fullUrl],
      current: fullUrl,
    });
  } else {
    // å…¶ä»–文件类型,可以下载或打开
    uni.downloadFile({
      url: fullUrl,
      success: (res) => {
        uni.openDocument({
          filePath: res.tempFilePath,
          success: () => {
            // æ‰“开文档成功
          },
          fail: (error) => {
            console.error("打开文档失败:", error);
            toast.show("无法预览此文件类型");
          },
        });
      },
      fail: (error) => {
        console.error("下载文件失败:", error);
        toast.show("下载文件失败");
      },
    });
  }
};
// ä»Ž URL æˆ–文件名中提取扩展名
const getExtension = (urlOrFileName: string) => {
  if (!urlOrFileName) return "";
  // ç§»é™¤æŸ¥è¯¢å‚数和哈希
  const cleanUrl = urlOrFileName.split("?")[0].split("#")[0];
  // èŽ·å–æœ€åŽä¸€ä¸ªç‚¹åŽé¢çš„å†…å®¹
  const extension = cleanUrl.split(".").pop()?.toLowerCase();
  return extension || "";
};
// åˆ¤æ–­æ˜¯å¦ä¸ºå›¾ç‰‡ç±»åž‹
const isImageType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(extension);
};
// åˆ¤æ–­æ˜¯å¦ä¸ºè§†é¢‘类型
const isVideoType = (urlOrFileName: string) => {
  const extension = getExtension(urlOrFileName);
  return ["mp4", "mov", "avi", "wmv", "flv", "mkv", "webm"].includes(extension);
};
// èŽ·å–æ–‡ä»¶ç±»åž‹
const getFileType = (urlOrFileName: string) => {
  if (!urlOrFileName) return "unknown";
  const extension = getExtension(urlOrFileName);
  switch (extension) {
    case "jpg":
    case "jpeg":
    case "png":
    case "gif":
    case "bmp":
    case "webp":
      return "image";
    case "mp4":
    case "mov":
    case "avi":
    case "wmv":
    case "flv":
    case "mkv":
    case "webm":
      return "video";
    case "pdf":
      return "pdf";
    case "doc":
    case "docx":
      return "word";
    case "xls":
    case "xlsx":
      return "excel";
    case "ppt":
    case "pptx":
      return "powerpoint";
    case "txt":
      return "text";
    case "zip":
    case "rar":
      return "archive";
    default:
      return "file";
  }
};
// æ ¼å¼åŒ–文件大小
const formatFileSize = (size: number) => {
  if (size < 1024) return size + " B";
  if (size < 1024 * 1024) return (size / 1024).toFixed(1) + " KB";
  return (size / (1024 * 1024)).toFixed(1) + " MB";
};
// æ ¼å¼åŒ–æ—¶é—´
const formatTime = (time: string) => {
  const date = new Date(time);
  return date.toLocaleString();
};
// å¯¹å¤–暴露方法:获取所有需提交的文件
const getSubmitFiles = () => ({
  newFiles: attachmentIds.value || [],
});
defineExpose({ getSubmitFiles });
</script>
<style lang="scss" scoped>
.attachment-container {
  padding: 12px;
  background: #f3f9f8;
  min-height: 100vh;
}
.header-actions {
  margin-bottom: 12px;
  :deep(.add_btn) {
    background: #0d867f;
    color: white;
    border: none;
  }
}
.attachment-list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  :deep(.status-tip-full) {
    grid-column: 1 / -1;
    width: 100%;
  }
  .attachment-card {
    width: 100%;
    position: relative;
    // ä½¿ç”¨ padding-top å®žçŽ°æ­£æ–¹å½¢ï¼ˆå…¼å®¹æ€§æ›´å¥½ï¼‰
    &::before {
      content: "";
      display: block;
      padding-top: 100%; // é«˜åº¦ç­‰äºŽå®½åº¦
    }
  }
}
.media-wrapper {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 8px;
  overflow: hidden;
  background: #f5f5f5;
  .media-preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }
  .loading-mask {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.3);
    z-index: 5;
    .loading-text {
      font-size: 12px;
      color: #fff;
    }
  }
  .file-icon-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding: 8px;
    text-align: center;
    .file-name {
      margin-top: 8px;
      font-size: 12px;
      color: #666;
      word-break: break-all;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
      &.error-text {
        color: #ff4757;
      }
    }
    &.error-overlay {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(255, 255, 255, 0.9);
      z-index: 5;
    }
  }
  .delete-btn {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10;
  }
}
</style>
src/static/icons/routingInspection.png
src/utils/cache.ts
@@ -1,6 +1,7 @@
const TOKEN_KEY = "app-token";
const USER_INFO_KEY = "user-info";
const DICT_KEY = "dict";
const TEAM_ID_KEY = "team-id";
import { type DictData } from "@/api/system/dict";
// è®¾ç½® token
@@ -48,9 +49,25 @@
  uni.removeStorageSync(DICT_KEY);
}
// è®¾ç½®ç­ç»„ID
export function setTeamId(teamId: string | number) {
  uni.setStorageSync(TEAM_ID_KEY, teamId);
}
// èŽ·å–ç­ç»„ID
export function getTeamId(): string | number | null {
  return uni.getStorageSync(TEAM_ID_KEY) || null;
}
// æ¸…除班组ID
export function clearTeamId() {
  uni.removeStorageSync(TEAM_ID_KEY);
}
// æ¸…除所有缓存信息
export function clearAll() {
  clearToken();
  clearUserInfo();
  clearDictCache();
  clearTeamId();
}
src/utils/request.ts
@@ -36,10 +36,12 @@
          uni.showToast({
            title: resData.msg || "业务处理失败",
            icon: "none",
            duration: 2000,
          });
          reject({
            message: resData.msg || "业务处理失败",
            code: resData.code,
            duration: 2000,
          });
        }
      },
@@ -53,6 +55,7 @@
        reject({
          message: "网络请求失败",
          error,
          duration: 2000,
        });
      },
    });