spring
2026-09-04 166ff56efefe6682a9d840fe0f1fa6e456833556
fix: 时效模块原型
已添加3个文件
已修改9个文件
3247 ■■■■■ 文件已修改
.env.development 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/product/twist.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/timeSensitiveTask.ts 709 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/components/SteelCoreCard.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/steelCore/form.vue 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/production/twist/receive/steelCore/index.vue 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/timely/detail.vue 727 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/timely/index.vue 1008 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/timely/inspection.vue 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -7,13 +7,13 @@
VITE_APP_PORT = 4096
# API åŸºç¡€è·¯å¾„,开发环境下的请求前缀
# VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
# VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
# VITE_APP_BASE_API = 'http://192.168.10.119:7002/mes' # é‚¹è£•
VITE_APP_BASE_API = 'http://192.168.100.131:7002/mes' #
# VITE_APP_BASE_API = 'http://192.168.100.131:7002/mes' #
# API æœåŠ¡å™¨çš„ URL
# VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
# VITE_APP_API_URL = 'http://192.168.10.119:7002/mes'
VITE_APP_API_URL = 'http://192.168.100.131:7002/mes' #
# VITE_APP_API_URL = 'http://192.168.100.131:7002/mes' #
.env.production
@@ -4,7 +4,7 @@
VITE_APP_BASE_API = '/prod-api'
# VITE_APP_BASE_API = 'http://114.132.189.42:7002/mes'
# API æœåŠ¡å™¨çš„ URL
# VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
VITE_APP_API_URL = 'http://114.132.189.42:7002/mes'
# å±±ä¸œ
VITE_APP_API_URL = 'http://192.168.100.131:7002/mes'
# VITE_APP_API_URL = 'http://192.168.100.131:7002/mes'
src/api/product/twist.ts
@@ -81,6 +81,20 @@
    });
  },
  // æŸ¥è¯¢å¯é¢†ç”¨çš„原材料批次(与 PC ç«¯é’¢èŠ¯é¢†ç”¨ä½¿ç”¨åŒä¸€æŽ¥å£ï¼‰
  getMaterialBatchStockPage(params: {
    current?: number;
    size?: number;
    batchCode?: string;
    materialType?: string;
  }) {
    return request<BaseResult<any>>({
      url: "/material/batch/list",
      method: "POST",
      data: params,
    });
  },
  // æ ¹æ®å•丝编号查询
  selectByMonofilamentNumber(params: { monofilamentNumber: string }) {
    return request<BaseResult<any>>({
src/api/timeSensitiveTask.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,709 @@
import request from "@/utils/request";
import type { BaseResult, PageResult } from "@/models/base";
export type TimelinessMaterialType = "单丝" | "绞线";
export interface TimeSensitiveTaskQuery {
  current: number;
  size?: number;
  initiatorName?: string;
  machineName?: string;
  materialType?: TimelinessMaterialType | string;
  taskStatus?: number | string;
  startTime?: string;
  endTime?: string;
}
export interface TimeSensitiveTaskForm {
  materialType: TimelinessMaterialType;
  initiatorName: string;
  initiateTime: string;
  setTemperature: number | string;
  agingDuration: number | string;
  machineName: string;
  inspectorName: string;
}
export interface TimeSensitiveTaskStartForm extends TimeSensitiveTaskForm {
  id: number | string;
  furnaceInTime: string;
}
export interface TimeSensitiveCandidateQuery {
  current: number;
  size?: number;
  taskId: number | string;
  materialType: TimelinessMaterialType;
  model?: string;
  materialCode?: string;
}
export interface TimelinessInspectionDetailForm {
  id?: number | string;
  routingInspectionUser?: string;
  zoneOne?: number | string;
  zoneTwo?: number | string;
  zoneThree?: number | string;
  zoneFour?: number | string;
  zoneFive?: number | string;
  zoneSix?: number | string;
  timelinessInspectionId?: number | string;
}
/** è”调完成后改为 false å³å¯æ¢å¤ä¸‹æ–¹çœŸå®žæŽ¥å£ã€‚ */
export const USE_TIME_SENSITIVE_MOCK = true;
const mockTasks: any[] = [
  {
    id: 9001,
    taskCode: "SX-20260904-001",
    materialType: "单丝",
    initiatorName: "å¼ å·¥",
    initiateTime: "2026-09-04 09:10:00",
    setTemperature: 180,
    agingDuration: "2+4+1",
    machineName: "时效炉-01",
    itemCount: 2,
    furnaceInTime: "",
    furnaceOutTime: "",
    inspectorName: "李师傅",
    taskStatus: 0,
  },
  {
    id: 9002,
    taskCode: "JX-20260904-001",
    materialType: "绞线",
    initiatorName: "王工",
    initiateTime: "2026-09-04 08:30:00",
    setTemperature: 175,
    agingDuration: "2+3+1",
    machineName: "时效炉-02",
    itemCount: 2,
    furnaceInTime: "2026-09-04 10:00:00",
    furnaceOutTime: "",
    inspectorName: "赵师傅",
    taskStatus: 1,
  },
  {
    id: 9003,
    taskCode: "SX-20260903-001",
    materialType: "单丝",
    initiatorName: "刘工",
    initiateTime: "2026-09-03 08:00:00",
    setTemperature: 180,
    agingDuration: "2+4+1",
    machineName: "时效炉-01",
    itemCount: 1,
    furnaceInTime: "2026-09-03 08:30:00",
    furnaceOutTime: "2026-09-03 15:30:00",
    inspectorName: "孙师傅",
    taskStatus: 2,
  },
];
const mockMaterialPool: Record<TimelinessMaterialType, any[]> = {
  å•丝: [
    {
      id: 9101,
      materialType: "单丝",
      model: "Φ5.6",
      poleModel: "1A60 H12 Î¦9.5 A2",
      rodNo: "720526083102-014",
      batchCode: "750826090101-005",
      monofilamentNumber: "750826090101-005",
      weight: 2875,
      supplier: "山东元旺电工科技有限公司",
    },
    {
      id: 9102,
      materialType: "单丝",
      model: "L-3.45",
      poleModel: "1A60-H14 Î¦9.5 A6",
      rodNo: "710726081602-003",
      batchCode: "750426083002-037",
      monofilamentNumber: "750426083002-037",
      weight: 2650,
      supplier: "山东元旺电工科技有限公司",
    },
    {
      id: 9301,
      outputId: 9301,
      materialType: "单丝",
      model: "8030-95L",
      poleModel: "8030",
      rodNo: "720626082404-001",
      batchCode: "750626083101-001",
      monofilamentNumber: "750626083101-001",
      weight: 2400,
      supplier: "山东元旺电工科技有限公司",
    },
    {
      id: 9302,
      outputId: 9302,
      materialType: "单丝",
      model: "Φ3.20",
      poleModel: "1A60 H14 Î¦9.5 A6",
      rodNo: "710726090401-002",
      batchCode: "750726090401-002",
      monofilamentNumber: "750726090401-002",
      weight: 2520,
      supplier: "山东元旺电工科技有限公司",
    },
    {
      id: 9501,
      materialType: "单丝",
      model: "Φ4.50",
      poleModel: "1A60 H12 Î¦9.5 A2",
      rodNo: "720526083102-018",
      batchCode: "750826090201-001",
      monofilamentNumber: "750826090201-001",
      weight: 2780,
      supplier: "山东元旺电工科技有限公司",
    },
  ],
  ç»žçº¿: [
    {
      id: 9201,
      outputId: 9201,
      materialType: "绞线",
      model: "JLHA1/G1A-16/3-6/1",
      batchCode: "JX260904001-01",
      weight: 1180,
    },
    {
      id: 9202,
      outputId: 9202,
      materialType: "绞线",
      model: "JLHA1/G1A-25/4-6/1",
      batchCode: "JX260904001-02",
      weight: 1260,
    },
    {
      id: 9401,
      outputId: 9401,
      materialType: "绞线",
      model: "JLHA1/G1A-35/6-6/1",
      batchCode: "JX260904002-01",
      weight: 1380,
    },
    {
      id: 9402,
      outputId: 9402,
      materialType: "绞线",
      model: "JLHA1/G1A-50/8-6/1",
      batchCode: "JX260904002-02",
      weight: 1460,
    },
  ],
};
const findMockMaterial = (id: number | string) =>
  [...mockMaterialPool.单丝, ...mockMaterialPool.绞线].find(
    (item) => String(item.id) === String(id)
  );
const cloneMockMaterial = (id: number | string) => {
  const item = findMockMaterial(id);
  return item ? { ...item } : undefined;
};
const mockItems: Record<string, any[]> = {
  "9001": [cloneMockMaterial(9101), cloneMockMaterial(9102)].filter(Boolean),
  "9002": [cloneMockMaterial(9201), cloneMockMaterial(9202)].filter(Boolean),
  "9003": [cloneMockMaterial(9501)].filter(Boolean),
};
const mockInspections: Record<string, any[]> = {
  "9002": [
    {
      id: 9601,
      routingInspectionUser: "赵师傅",
      zoneOne: 175,
      zoneTwo: 175.2,
      zoneThree: 174.8,
      zoneFour: 175.1,
      zoneFive: 175,
      zoneSix: 174.9,
      createTime: "2026-09-04 11:00:00",
      timelinessInspectionId: 9002,
    },
  ],
  "9003": [
    {
      id: 9602,
      routingInspectionUser: "孙师傅",
      zoneOne: 180,
      zoneTwo: 180,
      zoneThree: 179.8,
      zoneFour: 180.1,
      zoneFive: 180,
      zoneSix: 179.9,
      createTime: "2026-09-03 10:30:00",
      timelinessInspectionId: 9003,
    },
  ],
};
const mockOperations: Record<string, any[]> = {
  "9001": [
    {
      id: 9701,
      operationType: 1,
      operationTypeDesc: "新增",
      beforeStatus: "",
      afterStatus: "待开始",
      operationDetail: "张工创建时效任务",
      operatorName: "å¼ å·¥",
      operateTime: "2026-09-04 09:10:00",
    },
  ],
  "9002": [
    {
      id: 9702,
      operationType: 2,
      operationTypeDesc: "开始时效",
      beforeStatus: "待开始",
      afterStatus: "时效中",
      operationDetail: "赵师傅确认任务信息并开始时效",
      operatorName: "赵师傅",
      operateTime: "2026-09-04 10:00:00",
    },
  ],
  "9003": [
    {
      id: 9703,
      operationType: 3,
      operationTypeDesc: "结束时效",
      beforeStatus: "时效中",
      afterStatus: "时效结束",
      operationDetail: "孙师傅结束时效任务",
      operatorName: "孙师傅",
      operateTime: "2026-09-03 15:30:00",
    },
  ],
};
const mockUsers = [
  { userId: 1, nickName: "å¼ å·¥" },
  { userId: 2, nickName: "王工" },
  { userId: 3, nickName: "刘工" },
  { userId: 4, nickName: "李师傅" },
  { userId: 5, nickName: "赵师傅" },
  { userId: 6, nickName: "孙师傅" },
];
const mockMachines = [
  { deviceId: 1, deviceModel: "时效炉-01" },
  { deviceId: 2, deviceModel: "时效炉-02" },
  { deviceId: 3, deviceModel: "时效炉-03" },
];
const pad = (value: number) => String(value).padStart(2, "0");
const mockDateTime = () => {
  const date = new Date();
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
    date.getHours()
  )}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const mockResult = <T>(data: T, msg = "success") =>
  new Promise<BaseResult<T>>((resolve) => {
    setTimeout(() => resolve({ code: 200, msg, data }), 120);
  });
const mockMaterialCode = (item: any) =>
  String(item?.flowCode || item?.batchCode || item?.monofilamentNumber || "").trim();
const updateMockItemCount = (taskId: number | string) => {
  const task = mockTasks.find((item) => String(item.id) === String(taskId));
  if (task) task.itemCount = (mockItems[String(taskId)] || []).length;
};
const addMockOperation = (
  taskId: number | string,
  operationType: number,
  operationTypeDesc: string,
  beforeStatus: string,
  afterStatus: string,
  operationDetail: string,
  operatorName: string
) => {
  const key = String(taskId);
  mockOperations[key] ||= [];
  mockOperations[key].unshift({
    id: Date.now(),
    operationType,
    operationTypeDesc,
    beforeStatus,
    afterStatus,
    operationDetail,
    operatorName,
    operateTime: mockDateTime(),
  });
};
const TimeSensitiveTaskApi = {
  queryPage(params: TimeSensitiveTaskQuery) {
    if (USE_TIME_SENSITIVE_MOCK) {
      let rows = [...mockTasks];
      if (params.initiatorName) {
        rows = rows.filter((item) => item.initiatorName.includes(params.initiatorName!));
      }
      if (params.machineName) {
        rows = rows.filter((item) => item.machineName.includes(params.machineName!));
      }
      if (params.materialType) {
        rows = rows.filter((item) => item.materialType === params.materialType);
      }
      if (params.taskStatus !== undefined && params.taskStatus !== "") {
        rows = rows.filter((item) => Number(item.taskStatus) === Number(params.taskStatus));
      }
      if (params.startTime) {
        rows = rows.filter((item) => item.initiateTime >= params.startTime!);
      }
      if (params.endTime) {
        rows = rows.filter((item) => item.initiateTime <= params.endTime!);
      }
      const current = Number(params.current || 1);
      const size = Number(params.size || 10);
      return mockResult<PageResult>({
        records: rows.slice((current - 1) * size, current * size),
        total: rows.length,
        current,
        size,
        pages: current * size < rows.length,
      });
    }
    return request<BaseResult<PageResult>>({
      url: "/timeSensitiveTask/queryPage",
      method: "GET",
      data: params,
    });
  },
  getDetail(id: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      return mockResult(mockTasks.find((item) => String(item.id) === String(id)) || null);
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/getDetailById",
      method: "GET",
      data: { id },
    });
  },
  getItems(taskId: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      return mockResult<any[]>((mockItems[String(taskId)] || []).map((item) => ({ ...item })));
    }
    return request<BaseResult<any[]>>({
      url: "/timeSensitiveTask/getItems",
      method: "GET",
      data: { taskId },
    });
  },
  create(data: TimeSensitiveTaskForm) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const id = Date.now();
      const taskCode = `${data.materialType === "单丝" ? "SX" : "JX"}-${String(id).slice(-10)}`;
      mockTasks.unshift({
        id,
        taskCode,
        ...data,
        itemCount: 0,
        furnaceInTime: "",
        furnaceOutTime: "",
        taskStatus: 0,
      });
      mockItems[String(id)] = [];
      addMockOperation(
        id,
        1,
        "新增",
        "",
        "待开始",
        `${data.initiatorName}创建时效任务`,
        data.initiatorName
      );
      return mockResult({ id, taskCode }, "时效任务新增成功");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/add",
      method: "POST",
      data,
    });
  },
  update(data: TimeSensitiveTaskForm & { id: number | string }) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const task = mockTasks.find((item) => String(item.id) === String(data.id));
      if (task) Object.assign(task, data);
      addMockOperation(
        data.id,
        2,
        "修改",
        "待开始",
        "待开始",
        "修改时效任务基础信息",
        data.initiatorName
      );
      return mockResult(task, "时效任务修改成功");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/update",
      method: "POST",
      data,
    });
  },
  delete(id: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const index = mockTasks.findIndex((item) => String(item.id) === String(id));
      if (index >= 0) mockTasks.splice(index, 1);
      delete mockItems[String(id)];
      delete mockOperations[String(id)];
      delete mockInspections[String(id)];
      return mockResult(true, "时效任务删除成功");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/delete",
      method: "DELETE",
      data: { id },
    });
  },
  start(data: TimeSensitiveTaskStartForm) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const task = mockTasks.find((item) => String(item.id) === String(data.id));
      if (task) Object.assign(task, data, { taskStatus: 1 });
      addMockOperation(
        data.id,
        2,
        "开始时效",
        "待开始",
        "时效中",
        `${data.inspectorName}确认任务信息并开始时效`,
        data.inspectorName
      );
      return mockResult(task, "时效已开始");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/start",
      method: "POST",
      data,
    });
  },
  checkEnd(id: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      return mockResult({
        canEnd: false,
        reachedDuration: false,
        elapsedMinutes: 120,
        requiredMinutes: 360,
        message: "Mock:尚未达到设定时效时长。",
      });
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/checkEnd",
      method: "GET",
      data: { id },
    });
  },
  end(data: { id: number | string; forceEnd: boolean }) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const task = mockTasks.find((item) => String(item.id) === String(data.id));
      if (task) Object.assign(task, { taskStatus: 2, furnaceOutTime: mockDateTime() });
      addMockOperation(
        data.id,
        3,
        "结束时效",
        "时效中",
        "时效结束",
        data.forceEnd ? "未达到时长,确认强制结束时效" : "结束时效任务",
        task?.inspectorName || "操作人"
      );
      return mockResult(task, "时效已结束");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/end",
      method: "POST",
      data,
    });
  },
  queryCandidates(params: TimeSensitiveCandidateQuery) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const existingIds = new Set(
        (mockItems[String(params.taskId)] || []).map((item) => String(item.id))
      );
      let rows = mockMaterialPool[params.materialType]
        .filter((item) => !existingIds.has(String(item.id)))
        .map((item) => ({ ...item }));
      if (params.model) {
        const model = params.model.trim().toUpperCase();
        rows = rows.filter((item) =>
          String(item.model || "")
            .toUpperCase()
            .includes(model)
        );
      }
      if (params.materialCode) {
        const code = params.materialCode.trim().toUpperCase();
        rows = rows.filter(
          (item) =>
            mockMaterialCode(item).toUpperCase().includes(code) ||
            String(item.id).toUpperCase() === code ||
            String(item.outputId || "").toUpperCase() === code
        );
      }
      const current = Number(params.current || 1);
      const size = Number(params.size || 10);
      return mockResult<PageResult>({
        records: rows.slice((current - 1) * size, current * size),
        total: rows.length,
        current,
        size,
        pages: current * size < rows.length,
      });
    }
    return request<BaseResult<PageResult>>({
      url: "/timeSensitiveTask/queryCandidates",
      method: "GET",
      data: params,
    });
  },
  addItems(data: { taskId: number | string; itemIds: Array<number | string> }) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const key = String(data.taskId);
      mockItems[key] ||= [];
      const existingIds = new Set(mockItems[key].map((item) => String(item.id)));
      data.itemIds.forEach((id) => {
        const material = cloneMockMaterial(id);
        if (material && !existingIds.has(String(id))) mockItems[key].push(material);
      });
      updateMockItemCount(data.taskId);
      const task = mockTasks.find((item) => String(item.id) === key);
      addMockOperation(
        data.taskId,
        1,
        "新增明细",
        "待开始",
        "待开始",
        `新增 ${data.itemIds.length} æ¡æ—¶æ•ˆæ˜Žç»†`,
        task?.initiatorName || "操作人"
      );
      return mockResult(true, "时效明细新增成功");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/addItems",
      method: "POST",
      data,
    });
  },
  deleteItems(data: { taskId: number | string; itemIds: Array<number | string> }) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const key = String(data.taskId);
      const idSet = new Set(data.itemIds.map(String));
      mockItems[key] = (mockItems[key] || []).filter((item) => !idSet.has(String(item.id)));
      updateMockItemCount(data.taskId);
      const task = mockTasks.find((item) => String(item.id) === key);
      addMockOperation(
        data.taskId,
        4,
        "删除明细",
        "待开始",
        "待开始",
        `删除 ${data.itemIds.length} æ¡æ—¶æ•ˆæ˜Žç»†`,
        task?.initiatorName || "操作人"
      );
      return mockResult(true, "时效明细删除成功");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/deleteItems",
      method: "POST",
      data,
    });
  },
  getMachines() {
    if (USE_TIME_SENSITIVE_MOCK) return mockResult(mockMachines.map((item) => ({ ...item })));
    return request<BaseResult<any[]>>({
      url: "/device/querydeviceSx",
      method: "GET",
    });
  },
  getUsers() {
    if (USE_TIME_SENSITIVE_MOCK) return mockResult(mockUsers.map((item) => ({ ...item })));
    return request<BaseResult<any[]>>({
      url: "/team/getUser",
      method: "GET",
    });
  },
  getOperationRecords(recordId: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      return mockResult<any[]>(
        (mockOperations[String(recordId)] || []).map((item) => ({ ...item }))
      );
    }
    return request<BaseResult<any[]>>({
      url: "/operationRecord/list",
      method: "POST",
      data: { tableName: "time_sensitive_task", recordId },
    });
  },
  getInspectionDetails(id: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      return mockResult<any[]>((mockInspections[String(id)] || []).map((item) => ({ ...item })));
    }
    return request<BaseResult<any>>({
      url: "/timelinessInspection/getDetailById",
      method: "GET",
      data: { id },
    });
  },
  saveInspectionDetails(data: TimelinessInspectionDetailForm[]) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const inspectionId = data[0]?.timelinessInspectionId;
      if (inspectionId !== undefined) {
        mockInspections[String(inspectionId)] = data.map((item, index) => ({
          ...item,
          id: item.id || Date.now() + index,
          createTime: (item as any).createTime || mockDateTime(),
        }));
      }
      return mockResult(true, "巡检记录保存成功");
    }
    return request<BaseResult<any>>({
      url: "/timelinessInspectionDetail/add",
      method: "POST",
      data,
    });
  },
  deleteInspectionDetail(id: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      Object.keys(mockInspections).forEach((key) => {
        mockInspections[key] = mockInspections[key].filter(
          (item) => String(item.id) !== String(id)
        );
      });
      return mockResult(true, "巡检记录删除成功");
    }
    return request<BaseResult<any>>({
      url: "/timelinessInspectionDetail/delete",
      method: "DELETE",
      data: { id },
    });
  },
};
export default TimeSensitiveTaskApi;
src/pages.json
@@ -268,7 +268,19 @@
    {
      "path": "pages/timely/index",
      "style": {
        "navigationBarTitleText": "时效报工"
        "navigationBarTitleText": "时效任务"
      }
    },
    {
      "path": "pages/timely/detail",
      "style": {
        "navigationBarTitleText": "时效任务明细"
      }
    },
    {
      "path": "pages/timely/inspection",
      "style": {
        "navigationBarTitleText": "时效巡检记录"
      }
    },
    {
src/pages/index/index.vue
@@ -230,6 +230,12 @@
    url: "/pages/outbound/index",
    show: false,
  },
  {
    icon: "/static/icons/log.png",
    title: "时效",
    url: "/pages/timely/index",
    show: true,
  },
]);
// åŠ è½½è®¿é—®ç»Ÿè®¡æ•°æ®
src/pages/production/twist/components/SteelCoreCard.vue
@@ -10,6 +10,7 @@
      <wd-card>
        <wd-cell-group :border="true">
          <wd-cell title="芯线类型" :value="data.diskMaterial || '-'" />
          <wd-cell title="批次号" :value="data.batchNo || data.batchCode || '-'" />
          <wd-cell title="规格型号" :value="data.model || '-'" />
          <wd-cell title="盘号" :value="data.monofilamentNumber || '-'" />
          <wd-cell title="数量" :value="formatAmount" />
src/pages/production/twist/receive/steelCore/form.vue
@@ -11,13 +11,23 @@
        clearable
        @confirm="handleDiskMaterialChange"
      />
      <wd-cell
        title="批次号"
        title-width="100px"
        :value="model.batchNo || '请选择批次号'"
        :value-class="model.batchNo ? '' : 'batch-placeholder'"
        is-link
        required
        clickable
        @click="openBatchSelector"
      />
      <wd-input
        v-model="model.model"
        label="规格型号"
        label-width="100px"
        prop="model"
        clearable
        placeholder="请输入规格型号"
        readonly
        placeholder="选择批次后自动带出"
      />
      <wd-input
        v-model="model.monofilamentNumber"
@@ -37,11 +47,11 @@
      />
      <wd-input
        v-model="model.weight"
        label="重量"
        label="领用重量"
        label-width="100px"
        prop="weight"
        clearable
        placeholder="请输入重量"
        placeholder="请输入领用重量(kg)"
      />
      <wd-picker
        v-model="model.supplier"
@@ -55,12 +65,57 @@
      />
    </wd-cell-group>
  </wd-form>
  <wd-popup
    v-model="showBatchSelector"
    position="bottom"
    :z-index="30"
    :close-on-click-modal="false"
    custom-style="height: 72vh; border-radius: 24rpx 24rpx 0 0;"
  >
    <view class="batch-selector">
      <view class="batch-header">
        <text class="batch-title">选择批次号</text>
        <wd-icon name="close" size="20px" @click="showBatchSelector = false" />
      </view>
      <wd-search
        v-model="batchKeyword"
        placeholder="请输入批次号搜索"
        hide-cancel
        @search="searchBatchRemote"
        @clear="searchBatchRemote('')"
      />
      <scroll-view class="batch-list" scroll-y>
        <view v-if="batchSearching" class="batch-state">正在加载批次...</view>
        <template v-else-if="filteredBatchOptions.length">
          <view
            v-for="batch in filteredBatchOptions"
            :key="batch.batchCode"
            class="batch-item"
            :class="{ active: batch.batchCode === model.batchNo }"
            @click="selectBatch(batch)"
          >
            <view class="batch-item-header">
              <text class="batch-code">{{ batch.batchCode }}</text>
              <text class="batch-weight">当前 {{ stockWeightKg(batch) ?? "-" }} kg</text>
            </view>
            <view class="batch-meta">
              <text>{{ batch.materialName || "-" }}</text>
              <text>{{ getBatchModel(batch) }}</text>
            </view>
          </view>
        </template>
        <view v-else class="batch-state">暂无可领用的合格批次</view>
      </scroll-view>
    </view>
  </wd-popup>
</template>
<script lang="ts" setup>
import { onMounted, watch } from "vue";
import useFormData from "@/hooks/useFormData";
import ManageApi from "@/api/product/manage";
import TwistApi from "@/api/product/twist";
import { useToast } from "wot-design-uni";
const props = defineProps({
@@ -82,6 +137,8 @@
const toast = useToast();
const { form: model } = useFormData({
  diskMaterial: undefined, // èŠ¯çº¿ç±»åž‹
  batchNo: undefined, // æ‰¹æ¬¡å·ï¼ˆå±•示及钢芯领用字段)
  batchCode: undefined, // å…¼å®¹åº“存领用字段
  model: undefined, // è§„格型号
  monofilamentNumber: undefined, // ç›˜å·
  amount: undefined, // æ•°é‡
@@ -89,6 +146,75 @@
  supplier: undefined, // åނ家
  type: "钢芯",
});
interface MaterialBatch {
  batchCode: string;
  materialName?: string;
  resolvedSpecModel?: string;
  mesSpec?: string;
  ncSpec?: string;
  currentWeight?: number;
  mainUnit?: string;
  qualityStatus?: string;
  ncReturnFlag?: string;
}
const showBatchSelector = ref(false);
const batchKeyword = ref("");
const batchOptions = ref<MaterialBatch[]>([]);
const batchSearching = ref(false);
const filteredBatchOptions = computed(() =>
  batchOptions.value.filter((batch) => {
    const qualified = batch.qualityStatus === "合格" || batch.qualityStatus === "OK";
    return qualified && batch.ncReturnFlag !== "Y";
  })
);
const getBatchModel = (batch: MaterialBatch) =>
  batch.resolvedSpecModel || batch.mesSpec || batch.ncSpec || "-";
const stockWeightKg = (batch: MaterialBatch) => {
  if (batch.currentWeight === undefined || batch.currentWeight === null) return undefined;
  const weight = Number(batch.currentWeight);
  if (!Number.isFinite(weight)) return undefined;
  return batch.mainUnit === "吨" ? weight * 1000 : weight;
};
const searchBatchRemote = async (keyword: string | { value?: string } = batchKeyword.value) => {
  const searchValue =
    typeof keyword === "string" ? keyword : String(keyword?.value || batchKeyword.value || "");
  batchKeyword.value = searchValue;
  batchSearching.value = true;
  try {
    const { code, data } = await TwistApi.getMaterialBatchStockPage({
      batchCode: searchValue.trim() || undefined,
      materialType: "原材料",
      current: 1,
      size: 20,
    });
    batchOptions.value = code === 200 && Array.isArray(data?.records) ? data.records : [];
  } catch (error: any) {
    batchOptions.value = [];
    toast.error(error?.message || "批次查询失败");
  } finally {
    batchSearching.value = false;
  }
};
const openBatchSelector = () => {
  showBatchSelector.value = true;
  batchKeyword.value = model.batchNo || "";
  searchBatchRemote(batchKeyword.value);
};
const selectBatch = (batch: MaterialBatch) => {
  const batchModel = getBatchModel(batch);
  model.batchNo = batch.batchCode;
  model.batchCode = batch.batchCode;
  model.model = batchModel === "-" ? "" : batchModel;
  showBatchSelector.value = false;
};
// èŠ¯çº¿ç±»åž‹å­—å…¸æ•°æ®
const diskMaterialOptions = ref<Array<{ label: string; value: string }>>([]);
@@ -148,13 +274,37 @@
// æ”¶é›†è¡¨å•数据(不调 API,返回给父组件处理)
const collectData = () => {
  if (!model.batchNo) {
    toast.error("请选择批次号");
    return null;
  }
  if (!model.diskMaterial) {
    toast.error("请选择芯线类型");
    return null;
  }
  if (!model.monofilamentNumber) {
    toast.error("请输入盘号");
    return null;
  }
  if (!Number.isFinite(Number(model.weight)) || Number(model.weight) <= 0) {
    toast.error("请输入大于0的领用重量");
    return null;
  }
  if (!model.supplier) {
    toast.error("请选择厂家");
    return null;
  }
  return {
    diskMaterial: model.diskMaterial,
    batchNo: model.batchNo,
    batchCode: model.batchNo,
    model: model.model,
    monofilamentNumber: model.monofilamentNumber,
    amount: model.amount,
    weight: model.weight,
    supplier: model.supplier,
    unit: "kg",
    type: model.type,
  };
};
@@ -172,6 +322,8 @@
// é‡ç½®è¡¨å•数据
const resetFormData = () => {
  model.diskMaterial = undefined;
  model.batchNo = undefined;
  model.batchCode = undefined;
  model.model = undefined;
  model.monofilamentNumber = undefined;
  model.amount = undefined;
@@ -179,12 +331,16 @@
  model.supplier = undefined;
  model.type = "钢芯";
  diskMaterialValue.value = "";
  batchKeyword.value = "";
  batchOptions.value = [];
};
// å¡«å……表单数据(用于扫码后回显)
const fillFormData = (data: any) => {
  if (data) {
    model.diskMaterial = data.diskMaterial || "";
    model.batchNo = data.batchNo || data.batchCode || "";
    model.batchCode = data.batchCode || data.batchNo || "";
    model.model = data.model || "";
    model.monofilamentNumber = data.monofilamentNumber || "";
    model.amount = data.amount || data.oneLength || "";
@@ -199,6 +355,8 @@
const setFormData = (item: any) => {
  if (item) {
    model.diskMaterial = item.diskMaterial || "";
    model.batchNo = item.batchNo || item.batchCode || "";
    model.batchCode = item.batchCode || item.batchNo || "";
    model.model = item.model || "";
    model.monofilamentNumber = item.monofilamentNumber || "";
    model.amount = item.amount || "";
@@ -241,4 +399,78 @@
  bottom: 0;
  width: 100%;
}
:deep(.batch-placeholder) {
  color: #bfbfbf;
}
.batch-selector {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.batch-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
  border-bottom: 1px solid #eeeeee;
}
.batch-title {
  font-size: 17px;
  font-weight: 600;
  color: #333333;
}
.batch-list {
  flex: 1;
  height: 0;
  padding: 0 12px 16px;
  box-sizing: border-box;
}
.batch-item {
  padding: 14px 12px;
  margin-bottom: 8px;
  background: #ffffff;
  border: 1px solid #e5e5e5;
  border-radius: 8px;
}
.batch-item.active {
  background: #f0fbfa;
  border-color: #0d867f;
}
.batch-item-header,
.batch-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.batch-code {
  font-weight: 600;
  color: #222222;
}
.batch-weight {
  font-size: 12px;
  color: #0d867f;
}
.batch-meta {
  margin-top: 8px;
  font-size: 12px;
  color: #888888;
}
.batch-state {
  padding: 48px 16px;
  color: #999999;
  text-align: center;
}
</style>
src/pages/production/twist/receive/steelCore/index.vue
@@ -2,6 +2,7 @@
  <view class="list_box">
    <z-paging
      ref="pagingRef"
      v-model="steelData"
      :fixed="false"
      :auto-show-back-to-top="true"
      :loading-more-enabled="false"
@@ -11,38 +12,34 @@
        <CardTitle title="芯线领用" :hideAction="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="openAddForm"></wd-button>
            <wd-button
              type="icon"
              icon="add-circle"
              color="#0D867F"
              @click="openAddForm"
            ></wd-button>
          </template>
        </CardTitle>
      </template>
      <!-- å±‚级 tabs -->
      <wd-tabs v-model="tab" slidable="always" class="tabs-container">
        <block v-for="item in nodeList" :key="item.twistedLayer">
          <wd-tab :title="item.twistedLayer" :name="item.twistedLayer">
            <scroll-view class="content" scroll-y>
              <SteelCoreCard
                v-for="(m, i) in item.strandedWireDish"
                :key="i"
                :data="m"
                @delete="handleDeleteCard(item, m)"
              />
            </scroll-view>
          </wd-tab>
        </block>
      </wd-tabs>
      <template #bottom>
        <view class="flex justify-center items-center">
          <wd-button block @click="save">
            <text class="text-[#fff]">保存</text>
          </wd-button>
        </view>
      </template>
      <view class="content">
        <SteelCoreCard
          v-for="item in steelData"
          :key="item.id || item.monofilamentNumber"
          :data="item"
          @delete="handleDeleteCard(item)"
        />
      </view>
    </z-paging>
    <view class="save-footer">
      <wd-button block @click="save">
        <text class="text-[#fff]">保存</text>
      </wd-button>
    </view>
    <!-- æ–°å¢žé’¢èŠ¯å¼¹çª— -->
    <wd-popup v-model="showAddForm" position="bottom" custom-class="yl-popup">
    <wd-popup v-model="showAddForm" position="bottom" :z-index="30" custom-class="yl-popup">
      <view class="action px-3">
        <wd-button type="text" @click="cancelAdd">取消</wd-button>
        <wd-button type="text" @click="submitAdd">确定</wd-button>
@@ -70,59 +67,37 @@
const addFormRef = ref();
const scanRef = ref();
const toast = useToast();
const tab = ref("");
const nodeList = ref<any[]>([]);
const steelData = ref<any[]>([]);
const showAddForm = ref(false);
const isPageVisible = ref(false);
// ç›‘听标签切换
watch(tab, () => {
  if (tab.value) {
    getList();
  }
});
// èŽ·å–é’¢èŠ¯å±‚çº§æ•°æ®ï¼ˆå‚ç…§PC端 getSteelCoreDishTypeByWire)
// é’¢èŠ¯ä¸Ž PC ç«¯ä¿æŒä¸€è‡´ï¼Œç›´æŽ¥ä½¿ç”¨â€œå…¶ä»–”数据,不再按绞线层级领用。
const fetchSteelCoreData = async (wireId: number) => {
  const { code, data, msg } = await TwistApi.getSteelCoreDishTypeByWire(wireId);
  if (code === 200 && data) {
    nodeList.value = data.nodeList || [];
    // è¿½åŠ "其他"层
    if (data.otherStrandedWireDish && data.otherStrandedWireDish.length > 0) {
      nodeList.value.push({
        strandedWireDish: data.otherStrandedWireDish,
        twistedLayer: "其他",
        twistId: null,
      });
    }
    // è®¾ç½®é»˜è®¤ç¬¬ä¸€å±‚
    if (nodeList.value.length > 0 && !tab.value) {
      tab.value = nodeList.value[0].twistedLayer;
      getList();
    }
    const list = Array.isArray(data.otherStrandedWireDish)
      ? data.otherStrandedWireDish.map((item: any) => ({
          ...item,
          batchNo: item.batchNo || item.batchCode || "",
          batchCode: item.batchCode || item.batchNo || "",
          type: item.type || "钢芯",
          saleTwistId: null,
        }))
      : [];
    pagingRef.value?.complete(list);
  } else {
    toast.error(msg || "获取钢芯层级数据失败");
    pagingRef.value?.complete([]);
    toast.error(msg || "获取钢芯领用数据失败");
  }
};
// ä»Žå½“前选中层获取列表数据
const getList = async () => {
  const currentLayer = nodeList.value.find((node) => node.twistedLayer === tab.value);
  if (currentLayer && currentLayer.strandedWireDish) {
    pagingRef.value.complete(currentLayer.strandedWireDish);
  } else {
    pagingRef.value.complete([]);
  }
  if (!paramsId.value) return;
  await fetchSteelCoreData(Number(paramsId.value));
};
// æ‰“开新增表单
const openAddForm = () => {
  if (!tab.value) {
    toast.error("请先选择一个层");
    return;
  }
  if (addFormRef.value) {
    addFormRef.value.resetFormData();
  }
@@ -134,16 +109,8 @@
  const formData = await addFormRef.value.submit();
  if (!formData) return;
  // æŸ¥æ‰¾å½“前层
  const currentLayer = nodeList.value.find((node) => node.twistedLayer === tab.value);
  if (!currentLayer) {
    toast.error("未找到当前选中的层");
    return;
  }
  // åŒå±‚重复检查
  if (formData.monofilamentNumber) {
    const exists = currentLayer.strandedWireDish?.some(
    const exists = steelData.value.some(
      (item: any) => item.monofilamentNumber === formData.monofilamentNumber
    );
    if (exists) {
@@ -152,16 +119,12 @@
    }
  }
  // æ·»åŠ åˆ°å½“å‰å±‚
  if (!currentLayer.strandedWireDish) {
    currentLayer.strandedWireDish = [];
  }
  currentLayer.strandedWireDish.push({
  steelData.value.push({
    ...formData,
    wireId: paramsId.value,
    saleTwistId: null,
  });
  getList();
  showAddForm.value = false;
  toast.success("添加成功");
};
@@ -177,18 +140,13 @@
  }
  try {
    if (!tab.value) {
      toast.error("请先选择一个层");
      return;
    }
    const currentLayer = nodeList.value.find((node) => node.twistedLayer === tab.value);
    if (!currentLayer) {
      toast.error("未找到当前选中的层");
      return;
    }
    const parseData = JSON.parse(code.code);
    const batchNo = parseData.batchNo || parseData.batchCode || "";
    if (!batchNo) {
      toast.error("二维码错误,缺少批次号!");
      return;
    }
    // æ£€æŸ¥å¿…需字段
    const requiredFields = ["model", "supplier", "diskMaterial"];
@@ -198,9 +156,8 @@
      return;
    }
    // åŒå±‚重复检查
    if (parseData.monofilamentNumber) {
      const exists = currentLayer.strandedWireDish?.some(
      const exists = steelData.value.some(
        (item: any) => item.monofilamentNumber === parseData.monofilamentNumber
      );
      if (exists) {
@@ -213,19 +170,18 @@
    const newItem = {
      wireId: paramsId.value,
      diskMaterial: parseData.diskMaterial || "",
      batchNo,
      batchCode: batchNo,
      model: parseData.model || "",
      monofilamentNumber: parseData.monofilamentNumber || "",
      amount: parseData.oneLength || parseData.amount || "",
      weight: parseData.weight || "",
      supplier: parseData.supplier || "",
      type: "钢芯",
      saleTwistId: null,
    };
    if (!currentLayer.strandedWireDish) {
      currentLayer.strandedWireDish = [];
    }
    currentLayer.strandedWireDish.push(newItem);
    getList();
    steelData.value.push(newItem);
    toast.success("扫码成功");
  } catch (error) {
    toast.error("二维码异常,请更换二维码!");
@@ -239,25 +195,11 @@
// ä¿å­˜ï¼ˆæ‰¹é‡æäº¤ï¼‰
const save = () => {
  // æ£€æŸ¥æ˜¯å¦æœ‰æ–°æ•°æ®éœ€è¦ä¿å­˜
  let hasNewData = false;
  let newCount = 0;
  const newCount = steelData.value.filter(
    (item: { id?: number }) => item.id === undefined || item.id === null
  ).length;
  nodeList.value.forEach((node) => {
    if (node.strandedWireDish && Array.isArray(node.strandedWireDish)) {
      const hasNewInLayer = node.strandedWireDish.some(
        (item: { id?: number }) => item.id === undefined || item.id === null
      );
      if (hasNewInLayer) {
        hasNewData = true;
        newCount += node.strandedWireDish.filter(
          (item: { id?: number }) => item.id === undefined || item.id === null
        ).length;
      }
    }
  });
  if (!hasNewData) {
  if (!newCount) {
    toast.error("没有新的钢芯数据需要保存");
    return;
  }
@@ -276,23 +218,21 @@
// æ‰§è¡Œæ‰¹é‡ä¿å­˜
const handleConfirmSave = async () => {
  const newData: any[] = [];
  nodeList.value.forEach((node) => {
    // é™„加上下级 twistId
    if (node.strandedWireDish && Array.isArray(node.strandedWireDish)) {
      node.strandedWireDish.forEach((item: any) => {
        item.saleTwistId = node.twistId;
      });
      const layerNewData = node.strandedWireDish.filter(
        (item: { id?: number }) => item.id === undefined || item.id === null
      );
      newData.push(...layerNewData);
    }
  });
  const newData = steelData.value
    .filter((item: { id?: number }) => item.id === undefined || item.id === null)
    .map((item) => ({
      ...item,
      saleTwistId: null,
    }));
  if (newData.length === 0) {
    toast.error("没有新的钢芯数据需要保存");
    return;
  }
  const invalidBatchItem = newData.find((item) => !(item.batchNo || item.batchCode));
  if (invalidBatchItem) {
    toast.error(`${invalidBatchItem.monofilamentNumber || "钢芯"}:请选择批次号`);
    return;
  }
@@ -307,7 +247,7 @@
};
// åˆ é™¤å¡ç‰‡
const handleDeleteCard = async (layer: any, cardData: any) => {
const handleDeleteCard = async (cardData: any) => {
  uni.showModal({
    title: "提示",
    content: "确定要删除该钢芯吗?",
@@ -323,16 +263,12 @@
            }
          }
          // å‰ç«¯ç§»é™¤
          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();
            }
          const index = steelData.value.findIndex(
            (item: any) => item.monofilamentNumber === cardData.monofilamentNumber
          );
          if (index !== -1) {
            steelData.value.splice(index, 1);
            toast.success("删除成功");
          }
        } catch (error: any) {
          toast.error(error.msg || "删除失败");
@@ -345,7 +281,7 @@
onLoad(async (options: any) => {
  uni.$on("scanSteelCore", getScanCode);
  paramsId.value = options.id;
  fetchSteelCoreData(options.id);
  fetchSteelCoreData(Number(options.id));
});
onShow(() => {
@@ -379,15 +315,22 @@
  color: #0d867f;
}
.tabs-container {
  height: calc(100vh - 200px);
  display: flex;
  flex-direction: column;
.content {
  width: 100%;
  padding-bottom: 76px;
  box-sizing: border-box;
}
.content {
  height: calc(100vh - 200px);
  width: 100%;
.save-footer {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 20;
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
  background: #ffffff;
  border-top: 1px solid #eeeeee;
  box-shadow: 0 -2px 10px rgb(0 0 0 / 6%);
}
:deep(.zp-paging-container) {
@@ -404,38 +347,7 @@
  flex-direction: column;
}
:deep(.wd-tabs) {
  background: transparent !important;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
:deep(.wd-tabs__nav) {
  margin-bottom: 10px;
  flex-shrink: 0;
  position: sticky;
  top: 0;
  z-index: 10;
  background: #f3f9f8;
}
:deep(.wd-tabs__content) {
  flex: 1;
  overflow: visible;
}
:deep(.wd-tab__pane) {
  height: 100%;
}
:deep(.zp-paging-container-top) {
  flex-shrink: 0;
}
:deep(.zp-paging-container-bottom) {
  flex-shrink: 0;
  padding: 8px 12px;
}
</style>
src/pages/timely/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,727 @@
<template>
  <view class="detail-page">
    <view class="detail-header">
      <CardTitle title="时效任务明细" :hideAction="false">
        <template #action>
          <wd-button
            v-if="isWaiting && USE_TIME_SENSITIVE_MOCK"
            size="small"
            plain
            @click="simulateScan"
          >
            æ¨¡æ‹Ÿæ‰«ç 
          </wd-button>
          <wd-button
            v-if="isWaiting"
            type="icon"
            icon="scan"
            color="#0D867F"
            :loading="scanSubmitting"
            @click="openScan"
          />
          <wd-button
            v-if="isWaiting"
            type="icon"
            icon="add-circle"
            color="#0D867F"
            @click="openCandidatePopup"
          />
        </template>
      </CardTitle>
    </view>
    <scroll-view class="detail-scroll" scroll-y>
      <wd-card v-if="task.id" custom-class="summary-card">
        <template #title>
          <view class="summary-title">
            <text>{{ task.taskCode || `任务-${task.id}` }}</text>
            <wd-tag :type="statusType" size="small">{{ statusLabel }}</wd-tag>
          </view>
        </template>
        <view class="summary-grid">
          <view>
            <text class="label">类型</text>
            {{ materialType }}
          </view>
          <view>
            <text class="label">明细</text>
            {{ items.length }} ä»¶
          </view>
          <view>
            <text class="label">温度</text>
            {{ temperatureText }}
          </view>
          <view>
            <text class="label">时长</text>
            {{ durationText }}
          </view>
          <view>
            <text class="label">机台</text>
            {{ task.machineName || task.machine || "-" }}
          </view>
          <view>
            <text class="label">巡检员</text>
            {{ inspectorName }}
          </view>
        </view>
      </wd-card>
      <view class="section-title">
        <text>时效明细({{ items.length }})</text>
        <view v-if="isWaiting" class="section-actions">
          <text class="section-link" @click="toggleSelectAll">
            {{ allItemsSelected ? "取消全选" : "全选" }}
          </text>
          <text class="section-link danger" @click="deleteSelectedItems">
            åˆ é™¤æ‰€é€‰({{ selectedItems.length }})
          </text>
          <text class="section-link" @click="openCandidatePopup">手动添加</text>
        </view>
      </view>
      <view v-if="loading" class="state-box">
        <wd-loading />
        <text>加载中...</text>
      </view>
      <view v-else-if="!items.length" class="state-box">
        <wd-status-tip image="content" tip="暂无时效明细" />
      </view>
      <view v-else class="item-list">
        <wd-card v-for="(item, index) in items" :key="getItemKey(item, index)">
          <template #title>
            <view class="item-title">
              <view class="item-title-main">
                <wd-checkbox v-if="isWaiting" v-model="item._selected" @click.stop />
                <text>{{ getMaterialCode(item) || `明细${index + 1}` }}</text>
                <wd-tag
                  size="small"
                  :type="
                    normalizeMaterialType(item.materialType || materialType) === '单丝'
                      ? 'primary'
                      : 'success'
                  "
                >
                  {{ normalizeMaterialType(item.materialType || materialType) }}
                </wd-tag>
              </view>
              <wd-button
                v-if="isWaiting"
                type="text"
                size="small"
                custom-class="delete-button"
                @click="deleteItem(item)"
              >
                åˆ é™¤
              </wd-button>
            </view>
          </template>
          <wd-cell-group>
            <wd-cell title="规格型号" :value="item.model || '-'" />
            <wd-cell
              v-if="normalizeMaterialType(item.materialType || materialType) === '单丝'"
              title="铝杆型号"
              :value="item.poleModel || item.rodModel || '-'"
            />
            <wd-cell
              v-if="normalizeMaterialType(item.materialType || materialType) === '单丝'"
              title="铝杆号"
              :value="item.rodNo || item.poleNumber || '-'"
            />
            <wd-cell title="流转码/批次号" :value="getMaterialCode(item) || '-'" />
            <wd-cell title="重量(kg)" :value="String(item.weight ?? '-')" />
            <wd-cell v-if="item.supplier" title="厂家" :value="item.supplier" />
          </wd-cell-group>
        </wd-card>
      </view>
    </scroll-view>
    <wd-popup
      v-model="candidatePopupVisible"
      position="bottom"
      :z-index="30"
      :close-on-click-modal="false"
      custom-style="height: 86vh; border-radius: 24rpx 24rpx 0 0;"
    >
      <view class="popup-layout">
        <view class="popup-header">
          <view>
            <text class="popup-title">新增时效明细</text>
            <text class="popup-subtitle">{{ materialType }}</text>
          </view>
          <wd-icon name="close" size="20px" @click="candidatePopupVisible = false" />
        </view>
        <scroll-view class="candidate-list" scroll-y>
          <view v-if="candidateLoading && !candidates.length" class="state-box">
            <wd-loading />
            <text>查询中...</text>
          </view>
          <view v-else-if="!candidates.length" class="state-box">
            <wd-status-tip image="search" tip="暂无可加入的待时效物料" />
          </view>
          <view
            v-for="(candidate, index) in candidates"
            :key="getItemKey(candidate, index)"
            class="candidate-item"
            @click="candidate._selected = !candidate._selected"
          >
            <wd-checkbox v-model="candidate._selected" @click.stop />
            <view class="candidate-info">
              <text class="candidate-code">{{ getMaterialCode(candidate) || "-" }}</text>
              <text>{{ candidate.model || "-" }} Â· {{ candidate.weight ?? "-" }} kg</text>
              <text v-if="materialType === '单丝'" class="candidate-meta">
                {{ candidate.poleModel || candidate.rodModel || "-" }} Â·
                {{ candidate.rodNo || candidate.poleNumber || "-" }}
              </text>
            </view>
          </view>
          <wd-button
            v-if="candidates.length < candidateTotal"
            block
            plain
            :loading="candidateLoading"
            @click="loadMoreCandidates"
          >
            åŠ è½½æ›´å¤š
          </wd-button>
        </scroll-view>
        <view class="popup-footer">
          <text>已选 {{ selectedCandidates.length }} æ¡</text>
          <view class="footer-buttons">
            <wd-button type="info" plain @click="candidatePopupVisible = false">取消</wd-button>
            <wd-button :loading="candidateSubmitting" @click="addSelectedCandidates">
              ç¡®å®šæ·»åŠ 
            </wd-button>
          </view>
        </view>
      </view>
    </wd-popup>
    <Scan ref="scanRef" emitName="scanTimeSensitiveItem" />
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import { onHide, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
import { useToast } from "wot-design-uni";
import type { TagType } from "wot-design-uni/components/wd-tag/types";
import CardTitle from "@/components/card-title/index.vue";
import Scan from "@/components/scan/index.vue";
import TimeSensitiveTaskApi, {
  USE_TIME_SENSITIVE_MOCK,
  type TimelinessMaterialType,
} from "@/api/timeSensitiveTask";
import TwistApi from "@/api/product/twist";
import OutboundApi from "@/api/product/outbound";
const toast = useToast();
const scanRef = ref();
const taskId = ref<number | string>("");
const task = ref<any>({});
const items = ref<any[]>([]);
const loading = ref(false);
const scanSubmitting = ref(false);
const pageVisible = ref(false);
const candidatePopupVisible = ref(false);
const candidateLoading = ref(false);
const candidateSubmitting = ref(false);
const candidates = ref<any[]>([]);
const candidateTotal = ref(0);
const candidatePage = ref(1);
const candidatePageSize = 20;
const materialType = computed<TimelinessMaterialType>(() =>
  normalizeMaterialType(task.value.materialType)
);
const status = computed(() => normalizeStatus(task.value));
const isWaiting = computed(() => status.value === 0);
const statusLabel = computed(() => ["待开始", "时效中", "时效结束"][status.value] || "未知");
const statusType = computed<TagType>(() => {
  if (status.value === 0) return "warning";
  if (status.value === 1) return "primary";
  if (status.value === 2) return "success";
  return "default";
});
const inspectorName = computed(
  () => task.value.inspectorName || task.value.routingInspectionUser || "-"
);
const temperatureText = computed(() => {
  const value = task.value.setTemperature ?? task.value.temperature;
  return value === undefined || value === "" ? "-" : `${value}℃`;
});
const durationText = computed(() => {
  const value = task.value.agingDuration ?? task.value.duration;
  return value === undefined || value === "" ? "-" : `${value} å°æ—¶`;
});
const selectedItems = computed(() => items.value.filter((item) => item._selected));
const allItemsSelected = computed(
  () => items.value.length > 0 && selectedItems.value.length === items.value.length
);
const selectedCandidates = computed(() => candidates.value.filter((item) => item._selected));
function normalizeStatus(value: any) {
  const key = String(value?.taskStatus ?? value?.timelinessStatus ?? value?.status ?? value ?? "")
    .trim()
    .toUpperCase();
  if (["0", "WAITING", "PENDING", "待开始", "待时效"].includes(key)) return 0;
  if (["1", "RUNNING", "PROCESSING", "时效中"].includes(key)) return 1;
  if (["2", "FINISHED", "COMPLETED", "时效结束"].includes(key)) return 2;
  return Number(key);
}
function normalizeMaterialType(value: unknown): TimelinessMaterialType {
  const text = String(value ?? "");
  return text.includes("绞") || text === "成品" ? "绞线" : "单丝";
}
function getMaterialCode(item: any) {
  return String(
    item?.flowCode ||
      item?.batchCode ||
      item?.monofilamentNumber ||
      item?.monofilamentnumber ||
      item?.materialCode ||
      item?.systemNo ||
      item?.systemno ||
      ""
  ).trim();
}
function getCandidateId(item: any): number | string | undefined {
  return item?.id ?? item?.awaitTimelinessId ?? item?.qualitativeProductionId ?? item?.outputId;
}
function getItemKey(item: any, index: number) {
  return getCandidateId(item) || getMaterialCode(item) || index;
}
async function loadTask() {
  if (!taskId.value) return;
  loading.value = true;
  try {
    const [detailResult, itemsResult] = await Promise.all([
      TimeSensitiveTaskApi.getDetail(taskId.value),
      TimeSensitiveTaskApi.getItems(taskId.value),
    ]);
    task.value = detailResult.data || { id: taskId.value };
    const itemData = itemsResult.data as any;
    const list = Array.isArray(itemData)
      ? itemData
      : itemData?.records || itemData?.list || itemData?.items || [];
    items.value = list.map((item: any) => ({ ...item, _selected: false }));
  } catch (error: any) {
    toast.error(error?.message || "获取时效任务明细失败");
  } finally {
    loading.value = false;
  }
}
function showConfirm(title: string, content: string, confirmText = "确定") {
  return new Promise<boolean>((resolve) => {
    uni.showModal({
      title,
      content,
      confirmText,
      success: (result) => resolve(Boolean(result.confirm)),
      fail: () => resolve(false),
    });
  });
}
async function deleteItems(rows: any[]) {
  if (!isWaiting.value) return toast.error("只有待开始的任务可以删除明细");
  if (!rows.length) return toast.error("请至少选择一条时效明细");
  const itemIds = rows
    .map((item) => getCandidateId(item))
    .filter((id): id is number | string => id !== undefined && id !== null && id !== "");
  if (!itemIds.length) return toast.error("明细ID不存在,无法删除");
  const confirmed = await showConfirm(
    "删除明细",
    `确定删除选中的 ${itemIds.length} æ¡æ—¶æ•ˆæ˜Žç»†å—?删除后可重新添加。`,
    "删除"
  );
  if (!confirmed) return;
  try {
    await TimeSensitiveTaskApi.deleteItems({ taskId: taskId.value, itemIds });
    toast.success("时效明细删除成功");
    await loadTask();
  } catch (error: any) {
    toast.error(error?.message || "删除时效明细失败");
  }
}
function deleteItem(item: any) {
  deleteItems([item]);
}
function deleteSelectedItems() {
  deleteItems(selectedItems.value);
}
function toggleSelectAll() {
  const nextValue = !allItemsSelected.value;
  items.value.forEach((item) => {
    item._selected = nextValue;
  });
}
function openCandidatePopup() {
  if (!isWaiting.value) return toast.error("只有待开始的任务可以新增明细");
  candidatePopupVisible.value = true;
  loadCandidates(true);
}
async function loadCandidates(reset = false) {
  if (reset) {
    candidatePage.value = 1;
    candidates.value = [];
  }
  candidateLoading.value = true;
  try {
    const { data } = await TimeSensitiveTaskApi.queryCandidates({
      current: candidatePage.value,
      size: candidatePageSize,
      taskId: taskId.value,
      materialType: materialType.value,
    });
    const rows = (data?.records || []).map((item: any) => ({ ...item, _selected: false }));
    candidates.value = reset ? rows : [...candidates.value, ...rows];
    candidateTotal.value = Number(data?.total || candidates.value.length);
  } catch (error: any) {
    toast.error(error?.message || "查询可选时效明细失败");
  } finally {
    candidateLoading.value = false;
  }
}
function loadMoreCandidates() {
  if (candidateLoading.value || candidates.value.length >= candidateTotal.value) return;
  candidatePage.value += 1;
  loadCandidates(false);
}
async function addCandidates(rows: any[], successMessage = "时效明细新增成功") {
  const itemIds = rows
    .map((item) => getCandidateId(item))
    .filter((id): id is number | string => id !== undefined && id !== null && id !== "");
  if (!itemIds.length) return toast.error("所选物料缺少明细ID");
  await TimeSensitiveTaskApi.addItems({ taskId: taskId.value, itemIds });
  toast.success(successMessage);
  await loadTask();
}
async function addSelectedCandidates() {
  if (!selectedCandidates.value.length) return toast.error("请至少选择一条待时效数据");
  candidateSubmitting.value = true;
  try {
    await addCandidates(selectedCandidates.value);
    candidatePopupVisible.value = false;
  } catch (error: any) {
    toast.error(error?.message || "新增时效明细失败");
  } finally {
    candidateSubmitting.value = false;
  }
}
function readScanPayload(event: any) {
  const rawValue = event?.code ?? event;
  const raw = typeof rawValue === "string" ? rawValue.trim() : JSON.stringify(rawValue || "");
  let parsed: any = {};
  if (raw.startsWith("{") || raw.startsWith("[")) {
    try {
      parsed = JSON.parse(raw);
      if (Array.isArray(parsed)) parsed = parsed[0] || {};
    } catch {
      parsed = {};
    }
  }
  const directCode = getMaterialCode(parsed);
  const outputId = parsed.id ?? parsed.outPutId ?? parsed.outputId;
  if (directCode) return { raw, parsed, outputId, materialCode: directCode };
  const parts = raw.split(/[,,\s]/).filter(Boolean);
  const singleWireCode = parts.find((part) => part.startsWith("75"));
  return {
    raw,
    parsed,
    outputId,
    materialCode: singleWireCode || (!Object.keys(parsed).length ? raw : ""),
  };
}
async function resolveScannedMaterialCode(scan: ReturnType<typeof readScanPayload>) {
  if (scan.materialCode) return scan.materialCode;
  if (scan.outputId === undefined || scan.outputId === null || scan.outputId === "") return "";
  if (USE_TIME_SENSITIVE_MOCK) return String(scan.outputId);
  try {
    const result =
      materialType.value === "单丝"
        ? await TwistApi.getTagByIdLs({ outPutId: scan.outputId })
        : await OutboundApi.getTagByIdJx({ outPutId: scan.outputId });
    const data = Array.isArray(result.data) ? result.data[0] : result.data;
    return getMaterialCode(data) || String(scan.outputId);
  } catch {
    return String(scan.outputId);
  }
}
async function handleScan(event: any) {
  if (!pageVisible.value || scanSubmitting.value) return;
  if (!isWaiting.value) return toast.error("只有待开始的任务可以扫码新增明细");
  scanSubmitting.value = true;
  try {
    const scan = readScanPayload(event);
    const materialCode = await resolveScannedMaterialCode(scan);
    if (!materialCode) return toast.error("二维码格式错误,未识别到流转码或批次号");
    const { data } = await TimeSensitiveTaskApi.queryCandidates({
      current: 1,
      size: 20,
      taskId: taskId.value,
      materialType: materialType.value,
      materialCode,
    });
    const rows = data?.records || [];
    const normalizedCode = materialCode.trim().toUpperCase();
    const candidate =
      rows.find((item: any) => getMaterialCode(item).toUpperCase() === normalizedCode) ||
      rows.find((item: any) => String(getCandidateId(item)) === String(scan.outputId)) ||
      (rows.length === 1 ? rows[0] : undefined);
    if (!candidate) {
      return toast.error(`未找到可添加的${materialType.value},可能已添加或不在待时效范围`);
    }
    await addCandidates([candidate], "扫码添加成功");
  } catch (error: any) {
    toast.error(error?.message || "扫码添加时效明细失败");
  } finally {
    scanSubmitting.value = false;
  }
}
function openScan() {
  if (!isWaiting.value) return toast.error("只有待开始的任务可以扫码新增明细");
  scanRef.value?.triggerScan();
}
function simulateScan() {
  const mockIds = materialType.value === "单丝" ? [9301, 9302, 9501] : [9401, 9402, 9201, 9202];
  const existingIds = new Set(items.value.map((item) => String(getCandidateId(item))));
  const nextId = mockIds.find((id) => !existingIds.has(String(id)));
  if (!nextId) return toast.error(`没有更多可模拟扫码的${materialType.value}`);
  handleScan({ code: JSON.stringify({ id: nextId }) });
}
onLoad((options: any) => {
  taskId.value = options?.id || "";
  if (!taskId.value) {
    toast.error("时效任务ID不能为空");
    return;
  }
  uni.$off("scanTimeSensitiveItem", handleScan);
  uni.$on("scanTimeSensitiveItem", handleScan);
  loadTask();
});
onShow(() => {
  pageVisible.value = true;
});
onHide(() => {
  pageVisible.value = false;
});
onUnload(() => {
  pageVisible.value = false;
  uni.$off("scanTimeSensitiveItem", handleScan);
});
</script>
<style lang="scss" scoped>
.detail-page {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 50px);
  background: #f3f9f8;
}
.detail-header {
  flex-shrink: 0;
  background: #ffffff;
}
.detail-scroll {
  flex: 1;
  height: 0;
}
:deep(.summary-card) {
  margin: 10px;
}
.summary-title,
.item-title,
.item-title-main,
.popup-header,
.popup-footer,
.footer-buttons {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
}
.summary-title,
.item-title-main {
  min-width: 0;
  font-weight: 600;
}
.summary-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px 16px;
  font-size: 13px;
}
.summary-grid > view {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.label,
.popup-subtitle,
.candidate-meta {
  font-size: 12px;
  color: #999999;
}
.scan-tip {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  padding: 10px 12px;
  margin: 0 10px 10px;
  font-size: 12px;
  line-height: 1.5;
  color: #287b75;
  background: #eaf8f6;
  border-radius: 8px;
}
.section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 12px 10px;
  font-weight: 600;
}
.section-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: flex-end;
}
.section-link {
  font-size: 13px;
  font-weight: 400;
  color: #0d867f;
}
.section-link.danger {
  color: #fa4350;
}
.item-list {
  padding: 0 10px 24px;
}
.item-list :deep(.wd-card) {
  margin: 0 0 10px;
}
:deep(.delete-button .wd-button__content) {
  color: #fa4350;
}
.state-box {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: center;
  min-height: 130px;
  color: #999999;
}
.popup-layout {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #f7f8fa;
}
.popup-header {
  flex-shrink: 0;
  padding: 14px 16px;
  background: #ffffff;
  border-bottom: 1px solid #eeeeee;
}
.popup-header > view {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.popup-title {
  font-size: 17px;
  font-weight: 600;
}
.candidate-list {
  flex: 1;
  height: 0;
  padding: 10px 12px;
  box-sizing: border-box;
}
.candidate-item {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 12px;
  margin-bottom: 8px;
  background: #ffffff;
  border: 1px solid #eeeeee;
  border-radius: 8px;
}
.candidate-info {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
  font-size: 13px;
  color: #666666;
}
.candidate-code {
  overflow: hidden;
  font-weight: 600;
  color: #222222;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.popup-footer {
  flex-shrink: 0;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  font-size: 13px;
  background: #ffffff;
  border-top: 1px solid #eeeeee;
}
</style>
src/pages/timely/index.vue
@@ -1,108 +1,956 @@
<template>
  <view class="list_box">
    <z-paging ref="pagingRef" v-model="cardList" :fixed="false" :auto-show-back-to-top="true">
  <view class="timely-page">
    <z-paging
      ref="pagingRef"
      v-model="taskList"
      :fixed="false"
      :auto-show-back-to-top="true"
      @query="queryTaskList"
    >
      <template #top>
        <CardTitle title="时效报工" :hideAction="false">
          <template #action>
            <wd-button type="icon" icon="scan" color="#0D867F" @click="openScan"></wd-button>
          </template>
        </CardTitle>
      </template>
      <MonofilCard v-for="(item, index) in cardList" :key="index" :data="item" />
      <template #bottom>
        <view class="flex justify-center items-center">
          <wd-button block @click="save">
            <text class="text-[#fff]">保存</text>
          </wd-button>
        <view class="page-top">
          <CardTitle title="时效任务台账" :hideAction="false">
            <template #action>
              <wd-tag type="warning" size="small">Mock</wd-tag>
              <wd-button size="small" icon="add" @click="openCreatePopup">新增任务</wd-button>
            </template>
          </CardTitle>
          <view class="date-filter-card">
            <view class="date-filter-heading">
              <view class="date-filter-title">
                <view class="date-filter-icon">
                  <wd-icon name="calendar" size="18px" color="#078568" />
                </view>
                <view>
                  <view class="date-filter-name">选择日期</view>
                  <view class="date-filter-tip">按任务发起日期查询</view>
                </view>
              </view>
              <text v-if="hasDateFilter" class="date-filter-clear" @click="resetFilters">清除</text>
            </view>
            <view class="date-filter-fields">
              <view class="date-filter-field">
                <wd-datetime-picker v-model="filters.startTimestamp" type="date">
                  <view class="date-picker-trigger">
                    <text class="date-picker-label">开始日期</text>
                    <view class="date-picker-value">
                      <text :class="{ 'is-placeholder': !filters.startTimestamp }">
                        {{
                          filters.startTimestamp
                            ? formatDateOnly(Number(filters.startTimestamp))
                            : "请选择"
                        }}
                      </text>
                      <wd-icon name="arrow-right" size="13px" color="#aebbb7" />
                    </view>
                  </view>
                </wd-datetime-picker>
              </view>
              <view class="date-filter-separator">至</view>
              <view class="date-filter-field">
                <wd-datetime-picker v-model="filters.endTimestamp" type="date">
                  <view class="date-picker-trigger">
                    <text class="date-picker-label">结束日期</text>
                    <view class="date-picker-value">
                      <text :class="{ 'is-placeholder': !filters.endTimestamp }">
                        {{
                          filters.endTimestamp
                            ? formatDateOnly(Number(filters.endTimestamp))
                            : "请选择"
                        }}
                      </text>
                      <wd-icon name="arrow-right" size="13px" color="#aebbb7" />
                    </view>
                  </view>
                </wd-datetime-picker>
              </view>
            </view>
            <view class="date-filter-actions">
              <wd-button size="small" type="info" plain @click="resetFilters">重置</wd-button>
              <wd-button size="small" icon="search" @click="applyFilters">查询任务</wd-button>
            </view>
          </view>
        </view>
      </template>
      <view class="task-list">
        <wd-card v-for="task in taskList" :key="task.id" custom-class="task-card">
          <template #title>
            <view class="card-title" @click="openDetail(task)">
              <view class="title-main">
                <text class="task-code">{{ task.taskCode || `任务-${task.id}` }}</text>
                <wd-tag
                  size="small"
                  :type="
                    normalizeMaterialType(task.materialType) === '单丝' ? 'primary' : 'success'
                  "
                >
                  {{ normalizeMaterialType(task.materialType) }}
                </wd-tag>
              </view>
              <wd-tag size="small" :type="getStatusType(task)">
                {{ getStatusLabel(task) }}
              </wd-tag>
            </view>
          </template>
          <view @click="openDetail(task)">
            <view class="info-grid">
              <view class="info-item">
                <text class="info-label">发起任务人</text>
                <text>{{ getInitiator(task) }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">巡检员</text>
                <text>{{ getInspector(task) }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">设定温度</text>
                <text>{{ getTemperature(task) }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">时效时长</text>
                <text>{{ getDuration(task) }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">设定机台</text>
                <text>{{ task.machineName || task.machine || "-" }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">时效件数</text>
                <text>{{ task.itemCount ?? task.count ?? task.detailCount ?? 0 }} ä»¶</text>
              </view>
            </view>
            <view class="time-row">
              <text class="info-label">发起时间</text>
              <text>{{ task.initiateTime || task.startTime || task.createTime || "-" }}</text>
            </view>
            <view v-if="task.furnaceInTime || task.inTime" class="time-row">
              <text class="info-label">进炉时间</text>
              <text>{{ task.furnaceInTime || task.inTime }}</text>
            </view>
            <view v-if="task.furnaceOutTime || task.outTime" class="time-row">
              <text class="info-label">出炉时间</text>
              <text>{{ task.furnaceOutTime || task.outTime }}</text>
            </view>
          </view>
          <template #footer>
            <view class="task-actions">
              <wd-button size="small" plain @click="openDetail(task)">明细</wd-button>
              <wd-button
                v-if="isWaiting(task)"
                size="small"
                plain
                type="warning"
                @click="openEditPopup(task)"
              >
                ç¼–辑
              </wd-button>
              <wd-button v-if="isWaiting(task)" size="small" plain @click="startTask(task)">
                å¼€å§‹æ—¶æ•ˆ
              </wd-button>
              <wd-button
                v-if="isWaiting(task)"
                size="small"
                plain
                type="error"
                @click="deleteTask(task)"
              >
                åˆ é™¤
              </wd-button>
              <wd-button
                v-if="isRunning(task)"
                size="small"
                plain
                type="error"
                @click="endTask(task)"
              >
                ç»“束时效
              </wd-button>
              <wd-button
                v-if="!isWaiting(task)"
                size="small"
                plain
                type="success"
                @click="openInspection(task)"
              >
                å·¡æ£€è®°å½•
              </wd-button>
            </view>
          </template>
        </wd-card>
      </view>
    </z-paging>
    <Scan ref="scanRef" emitName="scanTime" />
    <wd-popup
      v-model="taskPopupVisible"
      position="bottom"
      :z-index="30"
      :close-on-click-modal="false"
      custom-style="height: 88vh; border-radius: 24rpx 24rpx 0 0;"
    >
      <view class="popup-layout">
        <view class="popup-header">
          <text class="popup-title">{{ editingTaskId ? "编辑时效任务" : "新增时效任务" }}</text>
          <wd-icon name="close" size="20px" @click="taskPopupVisible = false" />
        </view>
        <scroll-view class="popup-body" scroll-y>
          <wd-form ref="taskFormRef" :model="taskForm">
            <wd-cell-group border>
              <wd-picker
                v-model="taskForm.materialType"
                :columns="materialTypeOptions"
                label="任务类型"
                required
                :z-index="40"
              />
              <wd-picker
                v-model="taskForm.initiatorName"
                :columns="userOptions"
                label="发起任务人"
                placeholder="请选择"
                required
                :loading="optionsLoading"
                :z-index="40"
              />
              <wd-datetime-picker
                v-model="taskForm.initiateTimestamp"
                label="发起时间"
                type="datetime"
                required
                use-second
                :z-index="40"
              />
              <wd-picker
                v-model="taskForm.inspectorName"
                :columns="userOptions"
                label="巡检员"
                placeholder="请选择"
                required
                :loading="optionsLoading"
                :z-index="40"
              />
              <wd-input
                v-model="taskForm.setTemperature"
                label="设定温度(℃)"
                type="digit"
                required
                placeholder="请输入"
              />
              <wd-cell title="时效时长(小时)" required />
              <view class="duration-row">
                <wd-input
                  v-model="taskForm.durationRise"
                  type="digit"
                  placeholder="升温"
                  no-border
                />
                <text>+</text>
                <wd-input
                  v-model="taskForm.durationHold"
                  type="digit"
                  placeholder="保温"
                  no-border
                />
                <text>+</text>
                <wd-input
                  v-model="taskForm.durationSoak"
                  type="digit"
                  placeholder="焖炉"
                  no-border
                />
              </view>
              <wd-picker
                v-model="taskForm.machineName"
                :columns="machineOptions"
                label="设定机台"
                placeholder="请选择时效机台"
                required
                :loading="optionsLoading"
                :z-index="40"
              />
            </wd-cell-group>
          </wd-form>
        </scroll-view>
        <view class="popup-footer">
          <wd-button type="info" plain @click="taskPopupVisible = false">取消</wd-button>
          <wd-button :loading="taskSubmitting" @click="submitTask">
            {{ editingTaskId ? "保存修改" : "确定新增" }}
          </wd-button>
        </view>
      </view>
    </wd-popup>
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import { onShow } from "@dcloudio/uni-app";
import { useToast } from "wot-design-uni";
import type { TagType } from "wot-design-uni/components/wd-tag/types";
import CardTitle from "@/components/card-title/index.vue";
import zPaging from "@/components/z-paging/z-paging.vue";
import Scan from "@/components/scan/index.vue";
import MonofilCard from "../production/twist/components/MonofilCard.vue";
import TwistApi from "@/api/product/twist";
import { useToast } from "wot-design-uni";
import TimeSensitiveTaskApi, {
  type TimelinessMaterialType,
  type TimeSensitiveTaskForm,
} from "@/api/timeSensitiveTask";
import { useUserStore } from "@/store/modules/user";
const pagingRef = ref();
const scanRef = ref();
const toast = useToast();
const cardList = ref<any>([]);
const userStore = useUserStore();
const pagingRef = ref();
const taskFormRef = ref();
const taskList = ref<any[]>([]);
const taskPopupVisible = ref(false);
const taskSubmitting = ref(false);
const optionsLoading = ref(false);
const editingTaskId = ref<number | string | undefined>();
const pageReady = ref(false);
const getScanCode = async (code: any) => {
  console.log("时效扫码回调:", code);
  console.log("json:", JSON.parse(code.code));
  console.log("id:=============", JSON.parse(code.code).id);
  const { data } = await TwistApi.getScarn({
    outPutId: JSON.parse(code.code).id,
  });
  console.log("=======请求========", data);
  const exists = cardList.value.some(
    (item: any) => item.monofilamentNumber === data.monofilamentNumber
  );
  if (!exists) {
    const { id, outPutId, wireId, oneLength, ...rest } = data;
    console.log("sb", {
      outputId: id,
      amount: oneLength,
      ongLength: oneLength,
      ...rest,
    });
    cardList.value.push({
      outputId: id,
      amount: oneLength,
      ongLength: oneLength,
      ...rest,
    });
    pagingRef.value.complete(cardList.value);
  } else {
    toast.error("该单丝已领用,请勿重复扫码");
  }
};
const save = async () => {
  const { code } = await TwistApi.saveAge(cardList.value);
  if (code == 200) {
    toast.success("保存成功");
    pagingRef.value.refresh();
  } else {
    toast.error("保存失败");
  }
};
const materialTypeOptions = [
  { label: "单丝", value: "单丝" },
  { label: "绞线", value: "绞线" },
];
const openScan = () => {
  scanRef.value.triggerScan();
};
const statusOptions = [
  { label: "待开始", value: 0 },
  { label: "时效中", value: 1 },
  { label: "时效结束", value: 2 },
];
// ç¡®ä¿å…ˆç§»é™¤å†æ·»åŠ ç›‘å¬
const setupScanListener = () => {
  uni.$off("scanTime", getScanCode); // å…ˆç§»é™¤æ—§çš„
  uni.$on("scanTime", getScanCode); // å†æ·»åŠ æ–°çš„
};
const filters = reactive({
  startTimestamp: "" as "" | number,
  endTimestamp: "" as "" | number,
});
const hasDateFilter = computed(() => Boolean(filters.startTimestamp || filters.endTimestamp));
onMounted(() => {
  setupScanListener();
  console.log("页面显示 - æ‰«ç ç›‘听已设置");
const taskForm = reactive({
  materialType: "单丝" as TimelinessMaterialType,
  initiatorName: "",
  initiateTimestamp: Date.now() as number,
  setTemperature: "" as string | number,
  durationRise: 2 as string | number,
  durationHold: "" as string | number,
  durationSoak: "" as string | number,
  machineName: "",
  inspectorName: "",
});
onUnmounted(() => {
  // å¼€å¯å¹¿æ’­ç›‘听事件
  uni.$off("scanTime", getScanCode);
  console.log("页面卸载 - æ‰«ç ç›‘听已移除");
const userOptions = ref<Array<{ label: string; value: string }>>([]);
const machineOptions = ref<Array<{ label: string; value: string }>>([]);
function pad(value: number) {
  return String(value).padStart(2, "0");
}
function formatDateTime(value: number | Date) {
  const date = value instanceof Date ? value : new Date(value);
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
    date.getHours()
  )}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function formatDate(value: number, endOfDay = false) {
  const date = new Date(value);
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${
    endOfDay ? "23:59:59" : "00:00:00"
  }`;
}
function formatDateOnly(value: number) {
  return formatDate(value).slice(0, 10);
}
function parseDateTime(value: unknown) {
  if (!value) return Date.now();
  const timestamp = new Date(String(value).replace(/-/g, "/")).getTime();
  return Number.isFinite(timestamp) ? timestamp : Date.now();
}
function normalizeStatus(task: any) {
  const key = String(task?.taskStatus ?? task?.timelinessStatus ?? task?.status ?? "")
    .trim()
    .toUpperCase();
  if (["0", "WAITING", "PENDING", "待开始", "待时效"].includes(key)) return 0;
  if (["1", "RUNNING", "PROCESSING", "时效中"].includes(key)) return 1;
  if (["2", "FINISHED", "COMPLETED", "时效结束"].includes(key)) return 2;
  return Number(key);
}
function normalizeMaterialType(value: unknown): TimelinessMaterialType {
  const text = String(value ?? "");
  return text.includes("绞") || text === "成品" ? "绞线" : "单丝";
}
function isWaiting(task: any) {
  return normalizeStatus(task) === 0;
}
function isRunning(task: any) {
  return normalizeStatus(task) === 1;
}
function getStatusLabel(task: any) {
  return statusOptions.find((item) => item.value === normalizeStatus(task))?.label || "未知";
}
function getStatusType(task: any): TagType {
  const status = normalizeStatus(task);
  if (status === 0) return "warning";
  if (status === 1) return "primary";
  if (status === 2) return "success";
  return "default";
}
function getInitiator(task: any) {
  return task.initiatorName || task.userName || task.createUserName || "-";
}
function getInspector(task: any) {
  return task.inspectorName || task.routingInspectionUser || "-";
}
function getTemperature(task: any) {
  const value = task.setTemperature ?? task.temperature;
  return value === undefined || value === "" ? "-" : `${value}℃`;
}
function getDuration(task: any) {
  const value = task.agingDuration ?? task.duration;
  return value === undefined || value === "" ? "-" : `${value} å°æ—¶`;
}
async function queryTaskList(pageNo: number, pageSize: number) {
  try {
    const { data } = await TimeSensitiveTaskApi.queryPage({
      current: pageNo,
      size: pageSize,
      startTime: filters.startTimestamp ? formatDate(Number(filters.startTimestamp)) : undefined,
      endTime: filters.endTimestamp ? formatDate(Number(filters.endTimestamp), true) : undefined,
    });
    pagingRef.value?.complete(data?.records || []);
  } catch (error: any) {
    pagingRef.value?.complete(false);
    toast.error(error?.message || "获取时效任务失败");
  } finally {
    pageReady.value = true;
  }
}
function applyFilters() {
  if (
    filters.startTimestamp &&
    filters.endTimestamp &&
    Number(filters.startTimestamp) > Number(filters.endTimestamp)
  ) {
    toast.error("开始日期不能晚于结束日期");
    return;
  }
  pagingRef.value?.reload();
}
function resetFilters() {
  Object.assign(filters, {
    startTimestamp: "",
    endTimestamp: "",
  });
  applyFilters();
}
function getCurrentPerson() {
  const user = userStore.userInfo as any;
  return user?.nickName || user?.nickname || user?.userName || user?.username || "";
}
function getPersonName(item: any) {
  return String(item?.nickName || item?.nickname || item?.userName || item?.username || "");
}
function getMachineName(item: any) {
  return String(item?.deviceModel || item?.device_model || item?.deviceName || item?.name || "");
}
async function loadOptions() {
  if (userOptions.value.length && machineOptions.value.length) return;
  optionsLoading.value = true;
  try {
    const [usersResult, machinesResult] = await Promise.all([
      TimeSensitiveTaskApi.getUsers(),
      TimeSensitiveTaskApi.getMachines(),
    ]);
    userOptions.value = (usersResult.data || [])
      .map((item: any) => getPersonName(item))
      .filter(Boolean)
      .map((name: string) => ({ label: name, value: name }));
    machineOptions.value = (machinesResult.data || [])
      .map((item: any) => getMachineName(item))
      .filter(Boolean)
      .map((name: string) => ({ label: name, value: name }));
  } catch (error: any) {
    toast.error(error?.message || "获取人员或时效机台失败");
  } finally {
    optionsLoading.value = false;
  }
}
function resetTaskForm() {
  const currentPerson = getCurrentPerson();
  Object.assign(taskForm, {
    materialType: "单丝",
    initiatorName: currentPerson,
    initiateTimestamp: Date.now(),
    setTemperature: "",
    durationRise: 2,
    durationHold: "",
    durationSoak: "",
    machineName: "",
    inspectorName: currentPerson,
  });
}
function parseDuration(value: unknown) {
  const parts = String(value ?? "")
    .replace(/小时/g, "")
    .split("+")
    .map((item) => Number(item.trim()));
  if (parts.length >= 3 && parts.every(Number.isFinite)) return parts;
  return [2, Number.isFinite(parts[0]) ? parts[0] : "", ""];
}
async function openCreatePopup() {
  editingTaskId.value = undefined;
  resetTaskForm();
  taskPopupVisible.value = true;
  await loadOptions();
}
async function openEditPopup(task: any) {
  if (!isWaiting(task)) return;
  try {
    const { data } = await TimeSensitiveTaskApi.getDetail(task.id);
    const detail = { ...task, ...(data || {}) };
    const duration = parseDuration(detail.agingDuration ?? detail.duration);
    editingTaskId.value = detail.id;
    Object.assign(taskForm, {
      materialType: normalizeMaterialType(detail.materialType),
      initiatorName: getInitiator(detail) === "-" ? "" : getInitiator(detail),
      initiateTimestamp: parseDateTime(
        detail.initiateTime || detail.startTime || detail.createTime
      ),
      setTemperature: detail.setTemperature ?? detail.temperature ?? "",
      durationRise: duration[0],
      durationHold: duration[1],
      durationSoak: duration[2],
      machineName: detail.machineName || detail.machine || "",
      inspectorName: getInspector(detail) === "-" ? "" : getInspector(detail),
    });
    taskPopupVisible.value = true;
    await loadOptions();
  } catch (error: any) {
    toast.error(error?.message || "获取时效任务详情失败");
  }
}
function buildTaskPayload(): TimeSensitiveTaskForm | null {
  if (!taskForm.materialType) return (toast.error("请选择任务类型"), null);
  if (!taskForm.initiatorName) return (toast.error("请选择发起任务人"), null);
  if (!taskForm.initiateTimestamp) return (toast.error("请选择发起时间"), null);
  if (taskForm.setTemperature === "") return (toast.error("请输入设定温度"), null);
  if (taskForm.durationHold === "") return (toast.error("请输入保温时长"), null);
  if (!taskForm.machineName) return (toast.error("请选择设定机台"), null);
  if (!taskForm.inspectorName) return (toast.error("请选择巡检员"), null);
  return {
    materialType: taskForm.materialType,
    initiatorName: taskForm.initiatorName,
    initiateTime: formatDateTime(Number(taskForm.initiateTimestamp)),
    setTemperature: taskForm.setTemperature,
    agingDuration: `${taskForm.durationRise || 0}+${taskForm.durationHold || 0}+${
      taskForm.durationSoak || 0
    }`,
    machineName: taskForm.machineName,
    inspectorName: taskForm.inspectorName,
  };
}
async function submitTask() {
  const payload = buildTaskPayload();
  if (!payload || taskSubmitting.value) return;
  taskSubmitting.value = true;
  try {
    const result = editingTaskId.value
      ? await TimeSensitiveTaskApi.update({ id: editingTaskId.value, ...payload })
      : await TimeSensitiveTaskApi.create(payload);
    taskPopupVisible.value = false;
    toast.success(editingTaskId.value ? "时效任务修改成功" : "新增成功,请添加时效明细");
    pagingRef.value?.reload();
    if (!editingTaskId.value) {
      const resultData = result.data;
      const newId =
        resultData && typeof resultData === "object"
          ? resultData.id || resultData.taskId
          : resultData;
      if (newId) {
        setTimeout(() => {
          uni.navigateTo({ url: `/pages/timely/detail?id=${newId}` });
        }, 500);
      }
    }
  } catch (error: any) {
    toast.error(error?.message || `${editingTaskId.value ? "修改" : "新增"}时效任务失败`);
  } finally {
    taskSubmitting.value = false;
  }
}
function showConfirm(title: string, content: string, confirmText = "确定") {
  return new Promise<boolean>((resolve) => {
    uni.showModal({
      title,
      content,
      confirmText,
      success: (result) => resolve(Boolean(result.confirm)),
      fail: () => resolve(false),
    });
  });
}
async function deleteTask(task: any) {
  if (!isWaiting(task)) return toast.error("只有待开始的任务可以删除");
  const confirmed = await showConfirm(
    "删除任务",
    `确定删除时效任务“${task.taskCode || task.id}”吗?任务明细也会一并删除。`,
    "删除"
  );
  if (!confirmed) return;
  try {
    await TimeSensitiveTaskApi.delete(task.id);
    toast.success("时效任务删除成功");
    pagingRef.value?.reload();
  } catch (error: any) {
    toast.error(error?.message || "删除时效任务失败");
  }
}
async function startTask(task: any) {
  if (!isWaiting(task)) return;
  try {
    const [detailResult, itemsResult] = await Promise.all([
      TimeSensitiveTaskApi.getDetail(task.id),
      TimeSensitiveTaskApi.getItems(task.id),
    ]);
    const detail = { ...task, ...(detailResult.data || {}) };
    const items = Array.isArray(itemsResult.data)
      ? itemsResult.data
      : (itemsResult.data as any)?.records || [];
    const payload = {
      id: task.id,
      materialType: normalizeMaterialType(detail.materialType),
      initiatorName: getInitiator(detail) === "-" ? "" : getInitiator(detail),
      initiateTime: detail.initiateTime || detail.startTime || detail.createTime || "",
      setTemperature: detail.setTemperature ?? detail.temperature ?? "",
      agingDuration: detail.agingDuration ?? detail.duration ?? "",
      machineName: detail.machineName || detail.machine || "",
      furnaceInTime: detail.furnaceInTime || formatDateTime(new Date()),
      inspectorName: getInspector(detail) === "-" ? "" : getInspector(detail),
    };
    if (
      Object.values(payload).some(
        (value) => value === undefined || value === null || String(value) === ""
      )
    ) {
      return toast.error("任务基础信息不完整,请先编辑完善");
    }
    if (!items.length) return toast.error("当前任务没有时效明细,不能开始时效");
    const confirmed = await showConfirm(
      "开始时效",
      `任务:${task.taskCode || task.id}\n明细:${items.length}ä»¶\n机台:${payload.machineName}\n确认开始时效吗?`,
      "开始"
    );
    if (!confirmed) return;
    await TimeSensitiveTaskApi.start(payload);
    toast.success("时效已开始");
    pagingRef.value?.reload();
  } catch (error: any) {
    toast.error(error?.message || "开始时效失败");
  }
}
function parseCanEnd(data: any) {
  if (typeof data === "boolean") return data;
  return Boolean(data?.canEnd ?? data?.reachedDuration);
}
async function endTask(task: any) {
  if (!isRunning(task)) return;
  try {
    const { data } = await TimeSensitiveTaskApi.checkEnd(task.id);
    const canEnd = parseCanEnd(data);
    const durationText =
      data?.elapsedMinutes != null && data?.requiredMinutes != null
        ? `当前已时效 ${data.elapsedMinutes} åˆ†é’Ÿï¼Œè®¾å®šæ—¶é•¿ ${data.requiredMinutes} åˆ†é’Ÿã€‚`
        : "当前尚未达到设定时效时长。";
    const confirmed = await showConfirm(
      canEnd ? "结束时效" : "时长未达到",
      canEnd
        ? "已达到设定时效时长,确定结束时效吗?"
        : `${data?.message || durationText}是否仍要结束时效?`,
      canEnd ? "结束" : "仍然结束"
    );
    if (!confirmed) return;
    await TimeSensitiveTaskApi.end({ id: task.id, forceEnd: !canEnd });
    toast.success("时效已结束");
    pagingRef.value?.reload();
  } catch (error: any) {
    toast.error(error?.message || "结束时效失败");
  }
}
function openDetail(task: any) {
  uni.navigateTo({ url: `/pages/timely/detail?id=${task.id}` });
}
function openInspection(task: any) {
  const inspectionId = task.timelinessInspectionId || task.inspectionId || task.id;
  const readonly = normalizeStatus(task) === 2 ? 1 : 0;
  uni.navigateTo({
    url: `/pages/timely/inspection?id=${inspectionId}&readonly=${readonly}`,
  });
}
onShow(() => {
  if (pageReady.value) pagingRef.value?.reload();
});
</script>
<style lang="scss" scoped>
.list_box {
  height: calc(100vh - 100px);
.timely-page {
  height: calc(100vh - 50px);
  background: #f3f9f8;
}
:deep(.wd-button__content) {
  color: #0d867f;
.page-top {
  background: #ffffff;
}
.date-filter-card {
  padding: 12px;
  margin: 0 10px 10px;
  background: linear-gradient(135deg, #f3fbf8 0%, #ffffff 72%);
  border: 1px solid rgba(7, 133, 104, 0.12);
  border-radius: 12px;
  box-shadow: 0 4px 14px rgba(19, 94, 78, 0.06);
}
.date-filter-heading,
.date-filter-title,
.date-filter-fields,
.date-filter-actions,
.popup-footer,
.task-actions {
  display: flex;
  align-items: center;
}
.date-filter-heading {
  justify-content: space-between;
  margin-bottom: 10px;
}
.date-filter-title {
  gap: 8px;
}
.date-filter-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: rgba(7, 133, 104, 0.1);
  border-radius: 9px;
}
.date-filter-name {
  font-size: 14px;
  font-weight: 600;
  color: #243832;
}
.date-filter-tip {
  margin-top: 2px;
  font-size: 11px;
  color: #91a09c;
}
.date-filter-clear {
  padding: 5px 0 5px 10px;
  font-size: 12px;
  color: #078568;
}
.date-filter-fields {
  gap: 7px;
}
.date-filter-field {
  flex: 1;
  min-width: 0;
  background: #ffffff;
  border: 1px solid #e5efec;
  border-radius: 8px;
}
.date-picker-trigger {
  padding: 9px 10px;
}
.date-picker-label {
  display: block;
  margin-bottom: 5px;
  font-size: 11px;
  color: #8c9b97;
}
.date-picker-value {
  display: flex;
  gap: 4px;
  align-items: center;
  justify-content: space-between;
  font-size: 13px;
  font-weight: 500;
  color: #2b3c37;
}
.is-placeholder {
  font-weight: 400;
  color: #b7c1be;
}
.date-filter-separator {
  flex: none;
  font-size: 12px;
  color: #8c9b97;
}
.date-filter-actions {
  gap: 8px;
  justify-content: flex-end;
  margin-top: 10px;
}
.popup-footer,
.task-actions {
  flex-wrap: wrap;
  gap: 8px;
  justify-content: flex-end;
}
.task-list {
  padding: 10px 10px 24px;
}
:deep(.task-card) {
  margin: 0 0 10px;
}
.card-title,
.title-main {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
}
.title-main {
  min-width: 0;
  justify-content: flex-start;
}
.task-code {
  overflow: hidden;
  font-weight: 600;
  color: #222222;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.info-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px 16px;
}
.info-item,
.time-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  font-size: 13px;
  color: #333333;
}
.time-row {
  flex-direction: row;
  justify-content: space-between;
  padding-top: 10px;
  margin-top: 10px;
  border-top: 1px solid #f2f2f2;
}
.info-label {
  font-size: 12px;
  color: #999999;
}
.popup-layout {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #f7f8fa;
}
.popup-header {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-between;
  padding: 16px;
  background: #ffffff;
  border-bottom: 1px solid #eeeeee;
}
.popup-title {
  font-size: 17px;
  font-weight: 600;
}
.popup-body {
  flex: 1;
  height: 0;
}
.popup-footer {
  flex-shrink: 0;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  background: #ffffff;
  border-top: 1px solid #eeeeee;
}
.duration-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto 1fr;
  gap: 4px;
  align-items: center;
  padding: 0 12px 10px;
  background: #ffffff;
}
.duration-row :deep(.wd-input) {
  padding: 0;
}
</style>
src/pages/timely/inspection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,248 @@
<template>
  <view class="inspection-page">
    <CardTitle title="巡检范围及温度详情" :hideAction="false">
      <template v-if="!readonly" #action>
        <view class="header-actions">
          <wd-button size="small" plain icon="add" @click="addRecord">新增</wd-button>
          <wd-button size="small" plain type="warning" @click="editRecords">编辑</wd-button>
          <wd-button size="small" :loading="saving" @click="saveRecords">保存</wd-button>
        </view>
      </template>
    </CardTitle>
    <view v-if="loading" class="state-box">
      <wd-loading />
      <text>加载中...</text>
    </view>
    <wd-status-tip v-else-if="!records.length" image="content" tip="暂无巡检温度记录" />
    <view v-else class="record-list">
      <wd-card v-for="(record, index) in records" :key="record.id || index">
        <template #title>
          <view class="record-title">
            <text>巡检记录 {{ index + 1 }}</text>
            <wd-button
              v-if="!readonly"
              type="text"
              size="small"
              custom-class="delete-button"
              @click="deleteRecord(record, index)"
            >
              åˆ é™¤
            </wd-button>
          </view>
        </template>
        <wd-cell-group v-if="readonly || (record.id && !record._editing)">
          <wd-cell title="巡检员" :value="record.routingInspectionUser || '-'" />
          <wd-cell title="一区" :value="temperatureValue(record.zoneOne)" />
          <wd-cell title="二区" :value="temperatureValue(record.zoneTwo)" />
          <wd-cell title="三区" :value="temperatureValue(record.zoneThree)" />
          <wd-cell title="四区" :value="temperatureValue(record.zoneFour)" />
          <wd-cell title="五区" :value="temperatureValue(record.zoneFive)" />
          <wd-cell title="六区" :value="temperatureValue(record.zoneSix)" />
          <wd-cell title="巡检时间" :value="record.createTime || record.inspectionTime || '-'" />
        </wd-cell-group>
        <wd-cell-group v-else border>
          <wd-input v-model="record.routingInspectionUser" label="巡检员" readonly />
          <wd-input v-model="record.zoneOne" label="一区(℃)" type="digit" placeholder="请输入" />
          <wd-input v-model="record.zoneTwo" label="二区(℃)" type="digit" placeholder="请输入" />
          <wd-input v-model="record.zoneThree" label="三区(℃)" type="digit" placeholder="请输入" />
          <wd-input v-model="record.zoneFour" label="四区(℃)" type="digit" placeholder="请输入" />
          <wd-input v-model="record.zoneFive" label="五区(℃)" type="digit" placeholder="请输入" />
          <wd-input v-model="record.zoneSix" label="六区(℃)" type="digit" placeholder="请输入" />
        </wd-cell-group>
      </wd-card>
    </view>
    <wd-toast />
  </view>
</template>
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app";
import { useToast } from "wot-design-uni";
import CardTitle from "@/components/card-title/index.vue";
import TimeSensitiveTaskApi, {
  USE_TIME_SENSITIVE_MOCK,
  type TimelinessInspectionDetailForm,
} from "@/api/timeSensitiveTask";
import { useUserStore } from "@/store/modules/user";
import { getPrepareId } from "@/utils/cache";
type InspectionRecord = TimelinessInspectionDetailForm & {
  createTime?: string;
  inspectionTime?: string;
  _editing?: boolean;
};
const toast = useToast();
const userStore = useUserStore();
const inspectionId = ref<number | string>("");
const readonly = ref(false);
const loading = ref(false);
const saving = ref(false);
const records = ref<InspectionRecord[]>([]);
function currentPerson() {
  const user = userStore.userInfo as any;
  return user?.nickName || user?.nickname || user?.userName || user?.username || "";
}
function temperatureValue(value: unknown) {
  return value === undefined || value === null || value === "" ? "-" : `${value}℃`;
}
function ensurePrepared(action: string) {
  if (USE_TIME_SENSITIVE_MOCK || getPrepareId()) return true;
  toast.error(`请完成生产准备确认,再进行${action}操作`);
  return false;
}
async function loadRecords() {
  loading.value = true;
  try {
    const { data } = await TimeSensitiveTaskApi.getInspectionDetails(inspectionId.value);
    const list = Array.isArray(data) ? data : data ? [data] : [];
    records.value = list.map((item: any) => ({
      ...item,
      routingInspectionUser: item.routingInspectionUser || currentPerson(),
      timelinessInspectionId: item.timelinessInspectionId || inspectionId.value,
      _editing: false,
    }));
  } catch (error: any) {
    records.value = [];
    toast.error(error?.message || "加载巡检详情失败");
  } finally {
    loading.value = false;
  }
}
function addRecord() {
  if (readonly.value || !ensurePrepared("新增巡检范围温度")) return;
  records.value.push({
    routingInspectionUser: currentPerson(),
    zoneOne: "",
    zoneTwo: "",
    zoneThree: "",
    zoneFour: "",
    zoneFive: "",
    zoneSix: "",
    timelinessInspectionId: inspectionId.value,
    _editing: true,
  });
}
function editRecords() {
  if (readonly.value || !ensurePrepared("编辑巡检范围温度")) return;
  if (!records.value.some((item) => item.id)) return toast.error("没有已保存的数据可编辑");
  records.value.forEach((item) => {
    if (item.id) item._editing = true;
  });
  toast.show("已进入编辑模式,修改后请点击保存");
}
async function saveRecords() {
  if (readonly.value || !ensurePrepared("保存巡检范围温度")) return;
  if (!records.value.length) return toast.error("没有数据需要保存");
  saving.value = true;
  try {
    const payload = records.value.map((item) => ({
      id: item.id,
      routingInspectionUser: item.routingInspectionUser || currentPerson(),
      zoneOne: item.zoneOne,
      zoneTwo: item.zoneTwo,
      zoneThree: item.zoneThree,
      zoneFour: item.zoneFour,
      zoneFive: item.zoneFive,
      zoneSix: item.zoneSix,
      timelinessInspectionId: item.timelinessInspectionId || inspectionId.value,
    }));
    await TimeSensitiveTaskApi.saveInspectionDetails(payload);
    toast.success("保存成功");
    await loadRecords();
  } catch (error: any) {
    toast.error(error?.message || "保存失败");
  } finally {
    saving.value = false;
  }
}
function showDeleteConfirm() {
  return new Promise<boolean>((resolve) => {
    uni.showModal({
      title: "提示",
      content: "确定删除该巡检详情吗?",
      confirmText: "删除",
      success: (result) => resolve(Boolean(result.confirm)),
      fail: () => resolve(false),
    });
  });
}
async function deleteRecord(record: InspectionRecord, index: number) {
  if (readonly.value) return;
  const confirmed = await showDeleteConfirm();
  if (!confirmed) return;
  if (!record.id) {
    records.value.splice(index, 1);
    toast.success("删除成功");
    return;
  }
  try {
    await TimeSensitiveTaskApi.deleteInspectionDetail(record.id);
    toast.success("删除成功");
    await loadRecords();
  } catch (error: any) {
    toast.error(error?.message || "删除失败");
  }
}
onLoad((options: any) => {
  inspectionId.value = options?.id || "";
  readonly.value = String(options?.readonly || "0") === "1";
  if (!inspectionId.value) return toast.error("巡检记录ID不能为空");
  loadRecords();
});
</script>
<style lang="scss" scoped>
.inspection-page {
  min-height: calc(100vh - 50px);
  padding-bottom: 24px;
  background: #f3f9f8;
}
.header-actions,
.record-title {
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: space-between;
}
.record-list {
  padding: 10px;
}
.record-list :deep(.wd-card) {
  margin: 0 0 10px;
}
.record-title {
  font-weight: 600;
}
:deep(.delete-button .wd-button__content) {
  color: #fa4350;
}
.state-box {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: center;
  min-height: 180px;
  color: #999999;
}
</style>