spring
5 天以前 4d9eac00f0c1613d6279fbc75affd9ce31df5c6d
fix: 对接时效接口
已添加1个文件
已修改4个文件
2094 ■■■■■ 文件已修改
src/api/timeSensitiveTask.ts 243 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/timely/detail.vue 817 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/timely/index.vue 711 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/timely/materialDetail.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/timeSensitiveTask.ts
@@ -24,9 +24,15 @@
  inspectorName: string;
}
export interface TimeSensitiveTaskStartForm extends TimeSensitiveTaskForm {
export interface TimeSensitiveTaskStartForm {
  id: number | string;
  initiatorName: string;
  initiateTime: string;
  setTemperature: number | string;
  agingDuration: number | string;
  machineName: string;
  furnaceInTime: string;
  inspectorName: string;
}
export interface TimeSensitiveCandidateQuery {
@@ -34,8 +40,15 @@
  size?: number;
  taskId: number | string;
  materialType: TimelinessMaterialType;
  categoryName?: string;
  model?: string;
  materialCode?: string;
}
export interface TimeSensitiveTaskItemInput {
  itemId: number | string;
  /** å•丝传“铁架号-铁牌序号”,绞线传数字序号。 */
  ironPlateNo: number | string;
}
export interface TimelinessInspectionDetailForm {
@@ -50,8 +63,8 @@
  timelinessInspectionId?: number | string;
}
/** è”调完成后改为 false å³å¯æ¢å¤ä¸‹æ–¹çœŸå®žæŽ¥å£ã€‚ */
export const USE_TIME_SENSITIVE_MOCK = true;
/** æœ¬åœ°æ¼”示开关;正常运行时始终请求与 PC ç«¯ä¸€è‡´çš„真实接口。 */
export const USE_TIME_SENSITIVE_MOCK = false;
const mockTasks: any[] = [
  {
@@ -197,6 +210,161 @@
  ],
};
const mockMaterialDetails: Record<string, Record<string, any>> = {
  "9101": {
    qualitySetTemperature: 180,
    qualitySetDuration: "2+4+1",
    confirmTime: "2026-09-04 08:35:00",
    confirmUserName: "å¼ å·¥",
    status: 0,
    ledgerNo: "SO2609010188",
    schedulingDetailNo: "LS-PC-260904-001",
    deviceCode: "LS006",
    clientName: "山东元旺电工科技有限公司",
    productionDate: "2026-09-04",
    groupTeam: "白班",
    groupTeamLeader: "王班长",
    productionNumber: 1,
    unit: "盘",
  },
  "9102": {
    qualitySetTemperature: 175,
    qualitySetDuration: "2+3+1",
    confirmTime: "2026-09-04 08:42:00",
    confirmUserName: "å¼ å·¥",
    status: 0,
    ledgerNo: "SO2609010192",
    schedulingDetailNo: "LS-PC-260904-002",
    deviceCode: "LS008",
    clientName: "华东电缆有限公司",
    productionDate: "2026-09-04",
    groupTeam: "白班",
    groupTeamLeader: "王班长",
    productionNumber: 1,
    unit: "盘",
  },
  "9301": {
    qualitySetTemperature: 180,
    qualitySetDuration: "2+4+1",
    confirmTime: "2026-09-05 09:15:00",
    confirmUserName: "李工",
    status: 0,
    ledgerNo: "SO2609050106",
    schedulingDetailNo: "LS-PC-260905-006",
    deviceCode: "LS003",
    clientName: "山东元旺电工科技有限公司",
    productionDate: "2026-09-05",
    groupTeam: "白班",
    groupTeamLeader: "赵班长",
    productionNumber: 1,
    unit: "盘",
  },
  "9302": {
    qualitySetTemperature: 180,
    qualitySetDuration: "2+4+1",
    confirmTime: "2026-09-05 09:32:00",
    confirmUserName: "李工",
    status: 0,
    ledgerNo: "SO2609050112",
    schedulingDetailNo: "LS-PC-260905-007",
    deviceCode: "LS005",
    clientName: "华北线缆有限公司",
    productionDate: "2026-09-05",
    groupTeam: "白班",
    groupTeamLeader: "赵班长",
    productionNumber: 1,
    unit: "盘",
  },
  "9501": {
    qualitySetTemperature: 180,
    qualitySetDuration: "2+4+1",
    confirmTime: "2026-09-03 10:20:00",
    confirmUserName: "刘工",
    status: 2,
    ledgerNo: "SO2609030088",
    schedulingDetailNo: "LS-PC-260903-003",
    deviceCode: "LS002",
    clientName: "鲁中电缆有限公司",
    productionDate: "2026-09-03",
    groupTeam: "中班",
    groupTeamLeader: "孙班长",
    productionNumber: 1,
    unit: "盘",
  },
  "9201": {
    qualitySetTemperature: 175,
    qualitySetDuration: "2+3+1",
    confirmTime: "2026-09-04 09:05:00",
    confirmUserName: "王工",
    status: 1,
    ledgerNo: "SO2609020116",
    schedulingDetailNo: "JX-PC-260904-001",
    deviceCode: "JX003",
    clientName: "华北电缆有限公司",
    productionDate: "2026-09-04",
    groupTeam: "中班",
    groupTeamLeader: "赵班长",
    productionNumber: 1,
    productionWeight: 1180,
    unit: "盘",
  },
  "9202": {
    qualitySetTemperature: 175,
    qualitySetDuration: "2+3+1",
    confirmTime: "2026-09-04 09:12:00",
    confirmUserName: "王工",
    status: 1,
    ledgerNo: "SO2609020118",
    schedulingDetailNo: "JX-PC-260904-002",
    deviceCode: "JX004",
    clientName: "华北电缆有限公司",
    productionDate: "2026-09-04",
    groupTeam: "中班",
    groupTeamLeader: "赵班长",
    productionNumber: 1,
    productionWeight: 1260,
    unit: "盘",
  },
  "9401": {
    qualitySetTemperature: 175,
    qualitySetDuration: "2+3+1",
    confirmTime: "2026-09-05 10:10:00",
    confirmUserName: "李工",
    status: 0,
    ledgerNo: "SO2609050216",
    schedulingDetailNo: "JX-PC-260905-002",
    deviceCode: "JX006",
    clientName: "华北电缆有限公司",
    productionDate: "2026-09-05",
    groupTeam: "中班",
    groupTeamLeader: "赵班长",
    productionNumber: 1,
    productionWeight: 1380,
    unit: "盘",
  },
  "9402": {
    qualitySetTemperature: 175,
    qualitySetDuration: "2+3+1",
    confirmTime: "2026-09-05 10:22:00",
    confirmUserName: "李工",
    status: 0,
    ledgerNo: "SO2609050220",
    schedulingDetailNo: "JX-PC-260905-003",
    deviceCode: "JX007",
    clientName: "华南电缆有限公司",
    productionDate: "2026-09-05",
    groupTeam: "中班",
    groupTeamLeader: "赵班长",
    productionNumber: 1,
    productionWeight: 1460,
    unit: "盘",
  },
};
[...mockMaterialPool.单丝, ...mockMaterialPool.绞线].forEach((item) => {
  Object.assign(item, mockMaterialDetails[String(item.id)] || {});
});
const findMockMaterial = (id: number | string) =>
  [...mockMaterialPool.单丝, ...mockMaterialPool.绞线].find(
    (item) => String(item.id) === String(id)
@@ -207,10 +375,19 @@
  return item ? { ...item } : undefined;
};
const cloneMockMaterialWithIronPlate = (id: number | string, ironPlateNo: number | string) => {
  const item = cloneMockMaterial(id);
  return item ? { ...item, ironPlateNo } : 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),
  "9001": [cloneMockMaterialWithIronPlate(9101, 1), cloneMockMaterialWithIronPlate(9102, 2)].filter(
    Boolean
  ),
  "9002": [cloneMockMaterialWithIronPlate(9201, 1), cloneMockMaterialWithIronPlate(9202, 2)].filter(
    Boolean
  ),
  "9003": [cloneMockMaterialWithIronPlate(9501, 1)].filter(Boolean),
};
const mockInspections: Record<string, any[]> = {
@@ -376,7 +553,7 @@
    }
    return request<BaseResult<PageResult>>({
      url: "/timeSensitiveTask/queryPage",
      method: "GET",
      method: "POST",
      data: params,
    });
  },
@@ -387,8 +564,8 @@
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/getDetailById",
      method: "GET",
      data: { id },
      method: "POST",
      data: id as any,
    });
  },
@@ -398,8 +575,8 @@
    }
    return request<BaseResult<any[]>>({
      url: "/timeSensitiveTask/getItems",
      method: "GET",
      data: { taskId },
      method: "POST",
      data: taskId as any,
    });
  },
@@ -468,8 +645,8 @@
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/delete",
      method: "DELETE",
      data: { id },
      method: "POST",
      data: id as any,
    });
  },
@@ -507,8 +684,8 @@
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/checkEnd",
      method: "GET",
      data: { id },
      method: "POST",
      data: id as any,
    });
  },
@@ -537,7 +714,9 @@
  queryCandidates(params: TimeSensitiveCandidateQuery) {
    if (USE_TIME_SENSITIVE_MOCK) {
      const existingIds = new Set(
        (mockItems[String(params.taskId)] || []).map((item) => String(item.id))
        Object.values(mockItems)
          .flat()
          .map((item) => String(item.id))
      );
      let rows = mockMaterialPool[params.materialType]
        .filter((item) => !existingIds.has(String(item.id)))
@@ -571,19 +750,25 @@
    }
    return request<BaseResult<PageResult>>({
      url: "/timeSensitiveTask/queryCandidates",
      method: "GET",
      method: "POST",
      data: params,
    });
  },
  addItems(data: { taskId: number | string; itemIds: Array<number | string> }) {
  addItems(data: {
    taskId: number | string;
    itemIds: Array<number | string>;
    items: TimeSensitiveTaskItemInput[];
  }) {
    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);
      data.items.forEach(({ itemId, ironPlateNo }) => {
        const material = cloneMockMaterial(itemId);
        if (material && !existingIds.has(String(itemId))) {
          mockItems[key].push({ ...material, ironPlateNo });
        }
      });
      updateMockItemCount(data.taskId);
      const task = mockTasks.find((item) => String(item.id) === key);
@@ -593,10 +778,10 @@
        "新增明细",
        "待开始",
        "待开始",
        `新增 ${data.itemIds.length} æ¡æ—¶æ•ˆæ˜Žç»†`,
        `领用 ${data.itemIds.length} æ¡ç‰©æ–™`,
        task?.initiatorName || "操作人"
      );
      return mockResult(true, "时效明细新增成功");
      return mockResult(true, "物料领用成功");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/addItems",
@@ -618,10 +803,10 @@
        "删除明细",
        "待开始",
        "待开始",
        `删除 ${data.itemIds.length} æ¡æ—¶æ•ˆæ˜Žç»†`,
        `删除 ${data.itemIds.length} æ¡é¢†ç”¨ç‰©æ–™`,
        task?.initiatorName || "操作人"
      );
      return mockResult(true, "时效明细删除成功");
      return mockResult(true, "领用物料删除成功");
    }
    return request<BaseResult<any>>({
      url: "/timeSensitiveTask/deleteItems",
@@ -646,6 +831,14 @@
    });
  },
  /** ä¸Ž PC ç«¯ aging_rack_number å­—典一致,用作单丝领用铁架号。 */
  getAgingRackNumbers() {
    return request<BaseResult<any[]>>({
      url: "/system/dict/data/type/aging_rack_number",
      method: "GET",
    });
  },
  getOperationRecords(recordId: number | string) {
    if (USE_TIME_SENSITIVE_MOCK) {
      return mockResult<any[]>(
src/pages.json
@@ -274,7 +274,13 @@
    {
      "path": "pages/timely/detail",
      "style": {
        "navigationBarTitleText": "时效任务明细"
        "navigationBarTitleText": "领用物料"
      }
    },
    {
      "path": "pages/timely/materialDetail",
      "style": {
        "navigationBarTitleText": "物料详情"
      }
    },
    {
src/pages/timely/detail.vue
@@ -1,21 +1,14 @@
<template>
  <view class="detail-page">
    <view class="detail-header">
      <CardTitle title="时效任务明细" :hideAction="false">
      <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"
            custom-class="header-icon-button"
            :loading="scanSubmitting"
            @click="openScan"
          />
@@ -24,6 +17,7 @@
            type="icon"
            icon="add-circle"
            color="#0D867F"
            custom-class="header-icon-button"
            @click="openCandidatePopup"
          />
        </template>
@@ -44,7 +38,7 @@
            {{ materialType }}
          </view>
          <view>
            <text class="label">明细</text>
            <text class="label">已领用</text>
            {{ items.length }} ä»¶
          </view>
          <view>
@@ -67,15 +61,31 @@
      </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>
        <text>
          {{ materialType === "单丝" ? "当前铁架" : "领用物料" }}({{ displayItems.length }})
        </text>
      </view>
      <view v-if="isWaiting && materialType === '单丝'" class="section-actions">
        <wd-button block plain custom-class="section-action-button" @click="openRackSelector">
          é€‰æ‹©é“æž¶å·
        </wd-button>
      </view>
      <view v-if="materialType === '单丝'" class="layer-tabs-wrap">
        <view v-if="rackLoading" class="layer-loading">
          <wd-loading size="16px" />
          <text>加载铁架号...</text>
        </view>
        <wd-tabs v-else-if="rackTabs.length" v-model="activeRackNo" slidable="always">
          <wd-tab
            v-for="layer in rackTabs"
            :key="layer.value"
            :name="layer.value"
            :title="`${layer.label}(${getLayerItemCount(layer.value)})`"
          />
        </wd-tabs>
        <view v-else class="layer-empty">
          {{ rackOptions.length ? "请先选择铁架号" : "暂无可用铁架号" }}
        </view>
      </view>
@@ -83,15 +93,23 @@
        <wd-loading />
        <text>加载中...</text>
      </view>
      <view v-else-if="!items.length" class="state-box">
        <wd-status-tip image="content" tip="暂无时效明细" />
      <view v-else-if="!displayItems.length" class="state-box">
        <wd-status-tip
          image="content"
          :tip="
            materialType === '单丝'
              ? activeRackNo
                ? '当前铁架暂无领用单丝'
                : '请先选择铁架号'
              : '暂无领用物料'
          "
        />
      </view>
      <view v-else class="item-list">
        <wd-card v-for="(item, index) in items" :key="getItemKey(item, index)">
        <wd-card v-for="(item, index) in displayItems" :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"
@@ -104,18 +122,15 @@
                  {{ 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
              v-if="normalizeMaterialType(item.materialType || materialType) === '单丝'"
              title="铁架号"
              :value="getLayerLabel(getSingleWireLayer(item))"
            />
            <wd-cell title="铁牌序号" :value="String(getDisplayIronPlateNo(item))" />
            <wd-cell title="规格型号" :value="item.model || '-'" />
            <wd-cell
              v-if="normalizeMaterialType(item.materialType || materialType) === '单丝'"
@@ -128,12 +143,77 @@
              :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 title="质检温度(℃)" :value="String(getQualitySetTemperature(item))" />
            <wd-cell title="质检时长" :value="String(getQualitySetDuration(item))" />
            <wd-cell
              :title="materialType === '单丝' ? '重量(kg)' : '生产重量'"
              :value="String(getCandidateWeight(item))"
            />
          </wd-cell-group>
          <view class="material-detail-link" @click="openMaterialDetail(item)">
            <text>查看详情</text>
            <wd-icon name="arrow-right" size="14px" color="#0d867f" />
          </view>
        </wd-card>
      </view>
    </scroll-view>
    <wd-popup
      v-model="rackSelectorVisible"
      position="bottom"
      :z-index="32"
      :close-on-click-modal="false"
      custom-style="height: 58vh; border-radius: 24rpx 24rpx 0 0;"
    >
      <view class="popup-layout">
        <view class="popup-header">
          <view>
            <text class="popup-title">选择铁架号</text>
          </view>
          <wd-icon name="close" size="20px" @click="rackSelectorVisible = false" />
        </view>
        <scroll-view class="rack-option-list" scroll-y>
          <view v-if="rackLoading" class="state-box">
            <wd-loading />
            <text>加载铁架号...</text>
          </view>
          <wd-status-tip
            v-else-if="!rackSelectionRows.length"
            image="content"
            tip="暂无可用铁架号"
          />
          <block v-else>
            <view
              v-for="rack in rackSelectionRows"
              :key="rack.value"
              class="rack-option-item"
              @click="rack.selected = !rack.selected"
            >
              <wd-checkbox v-model="rack.selected" @click.stop />
              <text>{{ rack.label }}</text>
            </view>
          </block>
        </scroll-view>
        <view class="popup-footer">
          <text>已选 {{ pendingRackCount }} ä¸ª</text>
          <view class="footer-buttons">
            <wd-button
              type="info"
              plain
              custom-class="footer-action-button"
              @click="rackSelectorVisible = false"
            >
              å–消
            </wd-button>
            <wd-button custom-class="footer-action-button" @click="confirmRackSelection">
              ç¡®è®¤
            </wd-button>
          </view>
        </view>
      </view>
    </wd-popup>
    <wd-popup
      v-model="candidatePopupVisible"
@@ -145,10 +225,28 @@
      <view class="popup-layout">
        <view class="popup-header">
          <view>
            <text class="popup-title">新增时效明细</text>
            <text class="popup-title">领用物料</text>
            <text class="popup-subtitle">{{ materialType }}</text>
          </view>
          <wd-icon name="close" size="20px" @click="candidatePopupVisible = false" />
        </view>
        <view v-if="materialType === '单丝'" class="candidate-layer-tabs">
          <text class="candidate-layer-label">先选择铁架号</text>
          <wd-tabs
            v-if="candidateRackTabs.length"
            v-model="candidateRackNo"
            slidable="always"
            @change="handleCandidateLayerChange"
          >
            <wd-tab
              v-for="layer in candidateRackTabs"
              :key="layer.value"
              :name="layer.value"
              :title="layer.label"
            />
          </wd-tabs>
          <view v-else class="layer-empty">暂无可用铁架号,请先维护时效铁架号字典</view>
        </view>
        <scroll-view class="candidate-list" scroll-y>
@@ -163,22 +261,56 @@
            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 class="candidate-header" @click="toggleCandidate(candidate)">
              <wd-checkbox
                v-model="candidate._selected"
                @click.stop
                @change="recalculateCandidateIronPlateNumbers"
              />
              <view class="candidate-info">
                <text class="candidate-code">{{ getMaterialCode(candidate) || "-" }}</text>
                <text>{{ candidate.model || "-" }}</text>
              </view>
            </view>
            <wd-cell-group>
              <wd-input
                v-model="candidate.ironPlateNo"
                label="铁牌序号"
                type="number"
                required
                :readonly="materialType === '单丝'"
                placeholder="请输入正整数"
              />
              <wd-cell title="规格型号" :value="candidate.model || '-'" />
              <wd-cell
                v-if="materialType === '单丝'"
                title="铝杆型号"
                :value="candidate.poleModel || candidate.rodModel || '-'"
              />
              <wd-cell
                v-if="materialType === '单丝'"
                title="铝杆号"
                :value="candidate.rodNo || candidate.poleNumber || '-'"
              />
              <wd-cell title="流转码/批次号" :value="getMaterialCode(candidate) || '-'" />
              <wd-cell title="质检温度(℃)" :value="String(getQualitySetTemperature(candidate))" />
              <wd-cell title="质检时长" :value="String(getQualitySetDuration(candidate))" />
              <wd-cell
                :title="materialType === '单丝' ? '重量(kg)' : '生产重量'"
                :value="String(getCandidateWeight(candidate))"
              />
            </wd-cell-group>
            <view class="material-detail-link" @click="openMaterialDetail(candidate)">
              <text>查看详情</text>
              <wd-icon name="arrow-right" size="14px" color="#0d867f" />
            </view>
          </view>
          <wd-button
            v-if="candidates.length < candidateTotal"
            block
            plain
            custom-class="load-more-button"
            :loading="candidateLoading"
            @click="loadMoreCandidates"
          >
@@ -189,9 +321,20 @@
        <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
              type="info"
              plain
              custom-class="footer-action-button"
              @click="candidatePopupVisible = false"
            >
              å–消
            </wd-button>
            <wd-button
              custom-class="footer-action-button"
              :loading="candidateSubmitting"
              @click="addSelectedCandidates"
            >
              ç¡®è®¤é¢†ç”¨
            </wd-button>
          </view>
        </view>
@@ -231,6 +374,14 @@
const candidateTotal = ref(0);
const candidatePage = ref(1);
const candidatePageSize = 20;
const UNASSIGNED_LAYER = "__unassigned__";
const rackLoading = ref(false);
const rackOptions = ref<Array<{ label: string; value: string }>>([]);
const selectedRackNos = ref<string[]>([]);
const rackSelectorVisible = ref(false);
const rackSelectionRows = ref<Array<{ label: string; value: string; selected: boolean }>>([]);
const activeRackNo = ref("");
const candidateRackNo = ref("");
const materialType = computed<TimelinessMaterialType>(() =>
  normalizeMaterialType(task.value.materialType)
);
@@ -254,10 +405,37 @@
  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 pendingRackCount = computed(
  () => rackSelectionRows.value.filter((item) => item.selected).length
);
const rackTabs = computed(() => {
  const tabs = selectedRackNos.value.map((value) => {
    const option = rackOptions.value.find((item) => item.value === value);
    return option || { label: value, value };
  });
  const knownValues = new Set(tabs.map((item) => item.value));
  items.value.forEach((item) => {
    const frameNo = getSingleWireLayer(item);
    const value = frameNo || UNASSIGNED_LAYER;
    if (knownValues.has(value)) return;
    tabs.push({
      value,
      label: frameNo || "未分配铁架号",
    });
    knownValues.add(value);
  });
  return tabs;
});
const candidateRackTabs = computed(() =>
  rackTabs.value.filter((item) => item.value !== UNASSIGNED_LAYER)
);
const displayItems = computed(() => {
  if (materialType.value !== "单丝") return items.value;
  if (!activeRackNo.value) return [];
  return items.value.filter(
    (item) => (getSingleWireLayer(item) || UNASSIGNED_LAYER) === activeRackNo.value
  );
});
const selectedCandidates = computed(() => candidates.value.filter((item) => item._selected));
function normalizeStatus(value: any) {
@@ -273,6 +451,45 @@
function normalizeMaterialType(value: unknown): TimelinessMaterialType {
  const text = String(value ?? "");
  return text.includes("绞") || text === "成品" ? "绞线" : "单丝";
}
function parseSingleWireIronPlateNo(item: any) {
  const explicitLayer = String(
    item?.ironFrameNo ?? item?.ironRackNo ?? item?.rackNo ?? item?.frameNo ?? ""
  ).trim();
  const rawIronPlateNo = item?.ironPlateNo;
  if (explicitLayer) return { layer: explicitLayer, plateNo: rawIronPlateNo };
  const combinedNo = String(rawIronPlateNo ?? "").trim();
  const separatorIndex = combinedNo.lastIndexOf("-");
  if (separatorIndex > 0 && separatorIndex < combinedNo.length - 1) {
    return {
      layer: combinedNo.slice(0, separatorIndex),
      plateNo: combinedNo.slice(separatorIndex + 1),
    };
  }
  return { layer: "", plateNo: rawIronPlateNo };
}
function getSingleWireLayer(item: any) {
  return parseSingleWireIronPlateNo(item).layer;
}
function getDisplayIronPlateNo(item: any) {
  if (normalizeMaterialType(item?.materialType || materialType.value) !== "单丝") {
    return item?.ironPlateNo ?? "-";
  }
  return parseSingleWireIronPlateNo(item).plateNo ?? "-";
}
function getLayerLabel(value: string) {
  if (!value || value === UNASSIGNED_LAYER) return "未分配铁架号";
  return rackTabs.value.find((item) => item.value === value)?.label || value;
}
function getLayerItemCount(value: string) {
  return items.value.filter((item) => (getSingleWireLayer(item) || UNASSIGNED_LAYER) === value)
    .length;
}
function getMaterialCode(item: any) {
@@ -296,6 +513,218 @@
  return getCandidateId(item) || getMaterialCode(item) || index;
}
function getQualitySetTemperature(item: any) {
  const value =
    item.qualitySetTemperature ??
    item.qualityTemperature ??
    item.inspectionTemperature ??
    item.temperature;
  return value === undefined || value === null || value === "" ? "-" : value;
}
function getQualitySetDuration(item: any) {
  return (
    item.qualitySetDuration ||
    item.qualityDuration ||
    item.inspectionDuration ||
    item.duration ||
    "-"
  );
}
function getCandidateWeight(item: any) {
  return item.weight ?? item.productionWeight ?? item.weightSum ?? "-";
}
function openMaterialDetail(item: any) {
  uni.navigateTo({
    url: "/pages/timely/materialDetail",
    success: (result) => {
      result.eventChannel.emit("materialDetail", {
        ...item,
        materialType: normalizeMaterialType(item.materialType || materialType.value),
      });
    },
  });
}
function syncActiveRackNo() {
  const tabs = rackTabs.value;
  if (!tabs.length) {
    activeRackNo.value = "";
    candidateRackNo.value = "";
    return;
  }
  if (!tabs.some((item) => item.value === activeRackNo.value)) {
    const firstUsedLayer = items.value
      .map((item) => getSingleWireLayer(item) || UNASSIGNED_LAYER)
      .find((value) => tabs.some((tab) => tab.value === value));
    activeRackNo.value = firstUsedLayer || tabs[0].value;
  }
  if (!candidateRackTabs.value.some((item) => item.value === candidateRackNo.value)) {
    candidateRackNo.value =
      candidateRackTabs.value.find((item) => item.value === activeRackNo.value)?.value ||
      candidateRackTabs.value[0]?.value ||
      "";
  }
}
async function loadRackOptions() {
  if (rackOptions.value.length || rackLoading.value) return;
  rackLoading.value = true;
  try {
    const { data } = await TimeSensitiveTaskApi.getAgingRackNumbers();
    const rows = Array.isArray(data) ? data : (data as any)?.records || [];
    const seen = new Set<string>();
    rackOptions.value = rows
      .map((item: any) => ({
        label: String(
          item?.dictLabel ?? item?.label ?? item?.dictValue ?? item?.value ?? ""
        ).trim(),
        value: String(item?.dictValue ?? item?.value ?? "").trim(),
      }))
      .filter((item: { label: string; value: string }) => {
        if (!item.value || seen.has(item.value)) return false;
        seen.add(item.value);
        return true;
      });
    syncActiveRackNo();
  } catch (error: any) {
    toast.error(error?.message || "获取时效铁架号失败");
  } finally {
    rackLoading.value = false;
  }
}
async function openRackSelector() {
  if (!isWaiting.value) return;
  rackSelectorVisible.value = true;
  await loadRackOptions();
  const selected = new Set(selectedRackNos.value);
  rackSelectionRows.value = rackOptions.value.map((item) => ({
    ...item,
    selected: selected.has(item.value),
  }));
}
function confirmRackSelection() {
  selectedRackNos.value = rackSelectionRows.value
    .filter((item) => item.selected)
    .map((item) => item.value);
  syncActiveRackNo();
  rackSelectorVisible.value = false;
}
function fillIronPlateNumbers(rows: any[], otherCandidates: any[] = []) {
  if (materialType.value === "单丝") {
    return rows.map((item) => ({ ...item, ironPlateNo: undefined, _selected: false }));
  }
  const used = new Set(
    [...items.value, ...otherCandidates]
      .map((item) => Number(item.ironPlateNo))
      .filter((value) => Number.isInteger(value) && value > 0)
  );
  let nextNumber = 1;
  return rows.map((item) => {
    const original = Number(item.ironPlateNo);
    if (Number.isInteger(original) && original > 0 && !used.has(original)) {
      used.add(original);
      return { ...item, ironPlateNo: original, _selected: false };
    }
    while (used.has(nextNumber)) nextNumber += 1;
    const result = { ...item, ironPlateNo: nextNumber, _selected: false };
    used.add(nextNumber);
    nextNumber += 1;
    return result;
  });
}
function recalculateCandidateIronPlateNumbers() {
  if (materialType.value !== "单丝") return;
  if (!candidateRackNo.value) {
    candidates.value.forEach((item) => (item.ironPlateNo = undefined));
    return;
  }
  const used = new Set(
    items.value
      .filter((item) => getSingleWireLayer(item) === candidateRackNo.value)
      .map((item) => Number(getDisplayIronPlateNo(item)))
      .filter((value) => Number.isInteger(value) && value > 0)
  );
  candidates.value.forEach((item) => {
    if (!item._selected) {
      item.ironPlateNo = undefined;
      return;
    }
    let nextNumber = 1;
    while (used.has(nextNumber)) nextNumber += 1;
    item.ironPlateNo = nextNumber;
    used.add(nextNumber);
  });
}
function toggleCandidate(candidate: any) {
  candidate._selected = !candidate._selected;
  recalculateCandidateIronPlateNumbers();
}
function getNextIronPlateNo(layer = activeRackNo.value) {
  const used = new Set(
    items.value
      .filter((item) => materialType.value !== "单丝" || getSingleWireLayer(item) === layer)
      .map((item) => Number(getDisplayIronPlateNo(item)))
      .filter((value) => Number.isInteger(value) && value > 0)
  );
  let nextNumber = 1;
  while (used.has(nextNumber)) nextNumber += 1;
  return nextNumber;
}
function validateIronPlateNumbers(rows: any[], layer = candidateRackNo.value) {
  if (materialType.value === "单丝" && (!layer || layer === UNASSIGNED_LAYER)) {
    return "请先选择铁架号";
  }
  const numbers = rows.map((item) => Number(item.ironPlateNo));
  if (numbers.some((value) => !Number.isInteger(value) || value <= 0)) {
    return "请填写正确的铁牌序号";
  }
  const existingNumbers = new Set(
    items.value
      .filter((item) => materialType.value !== "单丝" || getSingleWireLayer(item) === layer)
      .map((item) => Number(getDisplayIronPlateNo(item)))
      .filter((value) => Number.isInteger(value) && value > 0)
  );
  if (
    new Set(numbers).size !== numbers.length ||
    numbers.some((value) => existingNumbers.has(value))
  ) {
    return materialType.value === "单丝" ? "同一铁架号内铁牌序号不能重复" : "铁牌序号不能重复";
  }
  return "";
}
function requestIronPlateNo(defaultValue: number) {
  return new Promise<number | undefined>((resolve) => {
    uni.showModal({
      title: "填写铁牌序号",
      content: String(defaultValue),
      editable: true,
      placeholderText: "请输入正整数",
      confirmText: "确认领用",
      success: (result) => {
        if (!result.confirm) return resolve(undefined);
        const value = Number((result as any).content);
        if (!Number.isInteger(value) || value <= 0) {
          toast.error("请填写正确的铁牌序号");
          return resolve(undefined);
        }
        resolve(value);
      },
      fail: () => resolve(undefined),
    });
  });
}
async function loadTask() {
  if (!taskId.value) return;
  loading.value = true;
@@ -310,66 +739,41 @@
      ? itemData
      : itemData?.records || itemData?.list || itemData?.items || [];
    items.value = list.map((item: any) => ({ ...item, _selected: false }));
    if (materialType.value === "单丝") await loadRackOptions();
    syncActiveRackNo();
  } catch (error: any) {
    toast.error(error?.message || "获取时效任务明细失败");
    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() {
async function openCandidatePopup() {
  if (!isWaiting.value) return toast.error("只有待开始的任务可以新增明细");
  if (materialType.value === "单丝") {
    await loadRackOptions();
    syncActiveRackNo();
    if (!candidateRackTabs.value.length) {
      if (!rackOptions.value.length) {
        return toast.error("暂无可用铁架号,请先维护时效铁架号字典");
      }
      toast.error("请先选择铁架号");
      openRackSelector();
      return;
    }
    candidateRackNo.value =
      candidateRackTabs.value.find((item) => item.value === activeRackNo.value)?.value ||
      candidateRackTabs.value[0].value;
  }
  candidatePopupVisible.value = true;
  loadCandidates(true);
}
function handleCandidateLayerChange() {
  candidates.value.forEach((item) => {
    item._selected = false;
    if (materialType.value === "单丝") item.ironPlateNo = undefined;
  });
}
async function loadCandidates(reset = false) {
@@ -385,11 +789,12 @@
      taskId: taskId.value,
      materialType: materialType.value,
    });
    const rows = (data?.records || []).map((item: any) => ({ ...item, _selected: false }));
    const rows = fillIronPlateNumbers(data?.records || [], reset ? [] : candidates.value);
    candidates.value = reset ? rows : [...candidates.value, ...rows];
    recalculateCandidateIronPlateNumbers();
    candidateTotal.value = Number(data?.total || candidates.value.length);
  } catch (error: any) {
    toast.error(error?.message || "查询可选时效明细失败");
    toast.error(error?.message || "查询可领用物料失败");
  } finally {
    candidateLoading.value = false;
  }
@@ -401,24 +806,48 @@
  loadCandidates(false);
}
async function addCandidates(rows: any[], successMessage = "时效明细新增成功") {
async function addCandidates(
  rows: any[],
  successMessage = "物料领用成功",
  layer = candidateRackNo.value
) {
  const validationMessage = validateIronPlateNumbers(rows, layer);
  if (validationMessage) {
    toast.error(validationMessage);
    return false;
  }
  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 });
  if (itemIds.length !== rows.length) {
    toast.error("所选物料缺少明细ID");
    return false;
  }
  await TimeSensitiveTaskApi.addItems({
    taskId: taskId.value,
    itemIds,
    items: rows.map((item, index) => ({
      itemId: itemIds[index],
      ironPlateNo:
        materialType.value === "单丝"
          ? `${layer}-${Number(item.ironPlateNo)}`
          : Number(item.ironPlateNo),
    })),
  });
  toast.success(successMessage);
  await loadTask();
  if (materialType.value === "单丝") activeRackNo.value = layer;
  return true;
}
async function addSelectedCandidates() {
  if (!selectedCandidates.value.length) return toast.error("请至少选择一条待时效数据");
  candidateSubmitting.value = true;
  try {
    await addCandidates(selectedCandidates.value);
    candidatePopupVisible.value = false;
    const added = await addCandidates(selectedCandidates.value);
    if (added) candidatePopupVisible.value = false;
  } catch (error: any) {
    toast.error(error?.message || "新增时效明细失败");
    toast.error(error?.message || "领用物料失败");
  } finally {
    candidateSubmitting.value = false;
  }
@@ -438,7 +867,8 @@
  }
  const directCode = getMaterialCode(parsed);
  const outputId = parsed.id ?? parsed.outPutId ?? parsed.outputId;
  if (directCode) return { raw, parsed, outputId, materialCode: directCode };
  const ironPlateNo = parsed.ironPlateNo ?? parsed.ironPlateNumber;
  if (directCode) return { raw, parsed, outputId, ironPlateNo, materialCode: directCode };
  const parts = raw.split(/[,,\s]/).filter(Boolean);
  const singleWireCode = parts.find((part) => part.startsWith("75"));
@@ -446,6 +876,7 @@
    raw,
    parsed,
    outputId,
    ironPlateNo,
    materialCode: singleWireCode || (!Object.keys(parsed).length ? raw : ""),
  };
}
@@ -469,6 +900,12 @@
async function handleScan(event: any) {
  if (!pageVisible.value || scanSubmitting.value) return;
  if (!isWaiting.value) return toast.error("只有待开始的任务可以扫码新增明细");
  if (
    materialType.value === "单丝" &&
    (!activeRackNo.value || activeRackNo.value === UNASSIGNED_LAYER)
  ) {
    return toast.error("请先选择单丝领用铁架号");
  }
  scanSubmitting.value = true;
  try {
    const scan = readScanPayload(event);
@@ -491,25 +928,30 @@
    if (!candidate) {
      return toast.error(`未找到可添加的${materialType.value},可能已添加或不在待时效范围`);
    }
    await addCandidates([candidate], "扫码添加成功");
    const suggestedNumber = Number(scan.ironPlateNo) || getNextIronPlateNo(activeRackNo.value);
    const ironPlateNo =
      materialType.value === "单丝" ? suggestedNumber : await requestIronPlateNo(suggestedNumber);
    if (ironPlateNo === undefined) return;
    await addCandidates([{ ...candidate, ironPlateNo }], "扫码领用成功", activeRackNo.value);
  } catch (error: any) {
    toast.error(error?.message || "扫码添加时效明细失败");
    toast.error(error?.message || "扫码领用物料失败");
  } finally {
    scanSubmitting.value = false;
  }
}
function openScan() {
async function openScan() {
  if (!isWaiting.value) return toast.error("只有待开始的任务可以扫码新增明细");
  if (materialType.value === "单丝") {
    await loadRackOptions();
    syncActiveRackNo();
    if (!activeRackNo.value || activeRackNo.value === UNASSIGNED_LAYER) {
      toast.error("请先选择单丝领用铁架号");
      openRackSelector();
      return;
    }
  }
  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) => {
@@ -548,6 +990,16 @@
.detail-header {
  flex-shrink: 0;
  background: #ffffff;
}
.detail-header :deep(.header-icon-button) {
  width: 48px;
  height: 48px;
  margin-left: 6px;
}
.detail-header :deep(.header-icon-button .wd-icon) {
  font-size: 26px !important;
}
.detail-scroll {
@@ -611,28 +1063,79 @@
}
.section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 12px 10px;
  padding: 4px 16px 10px;
  font-size: 18px;
  font-weight: 600;
}
.section-actions {
  display: grid;
  grid-template-columns: 1fr;
  padding: 0 12px 12px;
}
.section-actions :deep(.section-action-button) {
  height: 50px;
  font-size: 17px;
  font-weight: 600;
  border-radius: 10px;
}
.layer-tabs-wrap,
.candidate-layer-tabs {
  margin: 0 10px 10px;
  overflow: hidden;
  background: #ffffff;
  border-radius: 10px;
}
.candidate-layer-tabs {
  flex-shrink: 0;
  margin: 10px 12px 0;
}
.candidate-layer-label {
  display: block;
  padding: 10px 12px 4px;
  font-size: 12px;
  color: #666666;
}
.layer-loading,
.layer-empty {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: flex-end;
  gap: 6px;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 12px;
  font-size: 12px;
  color: #999999;
}
.section-link {
  font-size: 13px;
  font-weight: 400;
  color: #0d867f;
.layer-tabs-wrap :deep(.wd-tabs__nav),
.candidate-layer-tabs :deep(.wd-tabs__nav) {
  background: #ffffff;
}
.section-link.danger {
  color: #fa4350;
.rack-option-list {
  flex: 1;
  height: 0;
  padding: 10px 12px;
  box-sizing: border-box;
}
.rack-option-item {
  display: flex;
  gap: 12px;
  align-items: center;
  min-height: 58px;
  padding: 0 14px;
  margin-bottom: 8px;
  font-size: 17px;
  background: #ffffff;
  border: 1px solid #eeeeee;
  border-radius: 10px;
}
.item-list {
@@ -643,8 +1146,16 @@
  margin: 0 0 10px;
}
:deep(.delete-button .wd-button__content) {
  color: #fa4350;
.material-detail-link {
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: flex-end;
  padding: 10px 12px;
  font-size: 13px;
  color: #0d867f;
  background: #f8fbfa;
  border-top: 1px solid #edf2f0;
}
.state-box {
@@ -690,13 +1201,23 @@
.candidate-item {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 12px;
  flex-direction: column;
  overflow: hidden;
  margin-bottom: 8px;
  background: #ffffff;
  border: 1px solid #eeeeee;
  border-radius: 8px;
  border-radius: 10px;
}
.candidate-header {
  display: flex;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 12px;
  box-sizing: border-box;
  background: #f8fbfa;
  border-bottom: 1px solid #edf2f0;
}
.candidate-info {
@@ -720,8 +1241,22 @@
.popup-footer {
  flex-shrink: 0;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  font-size: 13px;
  font-size: 15px;
  background: #ffffff;
  border-top: 1px solid #eeeeee;
}
.popup-footer :deep(.footer-action-button) {
  min-width: 96px;
  height: 48px;
  font-size: 16px;
  font-weight: 600;
  border-radius: 10px;
}
:deep(.load-more-button) {
  height: 48px;
  font-size: 16px;
  border-radius: 10px;
}
</style>
src/pages/timely/index.vue
@@ -9,68 +9,7 @@
    >
      <template #top>
        <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>
          <CardTitle title="时效任务台账" :hideAction="false" />
        </view>
      </template>
@@ -118,7 +57,7 @@
                <text>{{ task.machineName || task.machine || "-" }}</text>
              </view>
              <view class="info-item">
                <text class="info-label">时效件数</text>
                <text class="info-label">领料数量</text>
                <text>{{ task.itemCount ?? task.count ?? task.detailCount ?? 0 }} ä»¶</text>
              </view>
            </view>
@@ -138,150 +77,14 @@
          <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 block custom-class="claim-button" @click="openDetail(task)">
                {{ isWaiting(task) ? "领料" : "查看领料记录" }}
              </wd-button>
            </view>
          </template>
        </wd-card>
      </view>
    </z-paging>
    <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>
@@ -293,82 +96,18 @@
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 TimeSensitiveTaskApi, {
  type TimelinessMaterialType,
  type TimeSensitiveTaskForm,
} from "@/api/timeSensitiveTask";
import { useUserStore } from "@/store/modules/user";
import TimeSensitiveTaskApi, { type TimelinessMaterialType } from "@/api/timeSensitiveTask";
const toast = useToast();
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 materialTypeOptions = [
  { label: "单丝", value: "单丝" },
  { label: "绞线", value: "绞线" },
];
const statusOptions = [
  { label: "待开始", value: 0 },
  { label: "时效中", value: 1 },
  { label: "时效结束", value: 2 },
];
const filters = reactive({
  startTimestamp: "" as "" | number,
  endTimestamp: "" as "" | number,
});
const hasDateFilter = computed(() => Boolean(filters.startTimestamp || filters.endTimestamp));
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: "",
});
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 ?? "")
@@ -387,10 +126,6 @@
function isWaiting(task: any) {
  return normalizeStatus(task) === 0;
}
function isRunning(task: any) {
  return normalizeStatus(task) === 1;
}
function getStatusLabel(task: any) {
@@ -428,8 +163,6 @@
    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) {
@@ -440,285 +173,8 @@
  }
}
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(() => {
@@ -736,117 +192,17 @@
  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;
  width: 100%;
  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;
:deep(.claim-button) {
  height: 42px;
  font-size: 15px;
  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;
  border-radius: 10px;
}
.task-list {
@@ -905,52 +261,5 @@
.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/materialDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,315 @@
<template>
  <scroll-view class="material-page" scroll-y>
    <view v-if="materialReady" class="page-content">
      <view class="hero-card">
        <view class="hero-top">
          <wd-tag :type="materialType === '单丝' ? 'primary' : 'success'" size="small">
            {{ materialType }}
          </wd-tag>
          <view class="iron-plate">铁盘号 {{ material.ironPlateNo ?? "-" }}</view>
        </view>
        <text class="material-code">{{ materialCode || "-" }}</text>
        <text class="material-model">{{ material.model || "规格型号未填写" }}</text>
      </view>
      <view class="detail-card">
        <view class="section-heading">物料信息</view>
        <view class="detail-row">
          <text class="detail-label">规格型号</text>
          <text class="detail-value">{{ material.model || "-" }}</text>
        </view>
        <view v-if="materialType === '单丝'" class="detail-row">
          <text class="detail-label">铝杆型号</text>
          <text class="detail-value">{{ material.poleModel || material.rodModel || "-" }}</text>
        </view>
        <view v-if="materialType === '单丝'" class="detail-row">
          <text class="detail-label">铝杆号</text>
          <text class="detail-value">{{ material.rodNo || material.poleNumber || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">流转码/批次号</text>
          <text class="detail-value">{{ materialCode || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">{{ materialType === "单丝" ? "重量(kg)" : "生产重量" }}</text>
          <text class="detail-value">{{ materialWeight }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">单位</text>
          <text class="detail-value">{{ material.unit || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">厂家</text>
          <text class="detail-value">{{ material.supplier || "-" }}</text>
        </view>
      </view>
      <view class="detail-card">
        <view class="section-heading">质检信息</view>
        <view class="quality-grid">
          <view class="quality-item">
            <text class="quality-label">质检温度</text>
            <text class="quality-value">{{ qualityTemperature }}</text>
          </view>
          <view class="quality-item">
            <text class="quality-label">质检时长</text>
            <text class="quality-value">{{ qualityDuration }}</text>
          </view>
        </view>
        <view class="detail-row">
          <text class="detail-label">状态</text>
          <wd-tag :type="statusType" size="small">{{ materialStatus }}</wd-tag>
        </view>
        <view class="detail-row">
          <text class="detail-label">确认时效时间</text>
          <text class="detail-value">{{ confirmTime }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">确认人</text>
          <text class="detail-value">{{ confirmUser }}</text>
        </view>
      </view>
      <view class="detail-card">
        <view class="section-heading">生产信息</view>
        <view class="detail-row">
          <text class="detail-label">销售合同号</text>
          <text class="detail-value">{{ material.ledgerNo || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">排产明细号</text>
          <text class="detail-value">{{ material.schedulingDetailNo || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">设备编号</text>
          <text class="detail-value">{{ material.deviceCode || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">客户名称</text>
          <text class="detail-value">{{ material.clientName || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">生产日期</text>
          <text class="detail-value">{{ material.productionDate || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">班次</text>
          <text class="detail-value">{{ material.groupTeam || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">班组长</text>
          <text class="detail-value">{{ material.groupTeamLeader || "-" }}</text>
        </view>
        <view class="detail-row">
          <text class="detail-label">生产数量</text>
          <text class="detail-value">{{ material.productionNumber ?? "-" }}</text>
        </view>
      </view>
    </view>
    <view v-else class="empty-state">
      <wd-status-tip image="content" tip="未获取到物料详情" />
    </view>
  </scroll-view>
</template>
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app";
import type { TagType } from "wot-design-uni/components/wd-tag/types";
const material = ref<any>({});
const materialReady = computed(() => Object.keys(material.value).length > 0);
const materialType = computed(() => {
  const value = String(material.value.materialType || "");
  return value.includes("绞") || value === "成品" ? "绞线" : "单丝";
});
const materialCode = computed(() =>
  String(
    material.value.flowCode ||
      material.value.batchCode ||
      material.value.monofilamentNumber ||
      material.value.monofilamentnumber ||
      material.value.materialCode ||
      material.value.systemNo ||
      material.value.systemno ||
      ""
  ).trim()
);
const materialWeight = computed(
  () => material.value.weight ?? material.value.productionWeight ?? material.value.weightSum ?? "-"
);
const qualityTemperature = computed(() => {
  const value =
    material.value.qualitySetTemperature ??
    material.value.qualityTemperature ??
    material.value.inspectionTemperature ??
    material.value.temperature;
  return value === undefined || value === null || value === "" ? "-" : `${value}℃`;
});
const qualityDuration = computed(
  () =>
    material.value.qualitySetDuration ||
    material.value.qualityDuration ||
    material.value.inspectionDuration ||
    material.value.duration ||
    "-"
);
const confirmTime = computed(
  () =>
    material.value.confirmTime ||
    material.value.confirmTimelinessTime ||
    material.value.createTime ||
    "-"
);
const confirmUser = computed(
  () =>
    material.value.confirmUserName ||
    material.value.confirmPerson ||
    material.value.createUserName ||
    material.value.userName ||
    "-"
);
const materialStatus = computed(() => {
  const value = material.value.status ?? material.value.timelinessStatus;
  if (value === 1 || value === "1" || value === "PROCESSING") return "时效中";
  if (value === 2 || value === "2" || value === "FINISHED") return "时效结束";
  return "待时效";
});
const statusType = computed<TagType>(() => {
  if (materialStatus.value === "时效中") return "primary";
  if (materialStatus.value === "时效结束") return "success";
  return "warning";
});
onLoad(() => {
  const pages = getCurrentPages();
  const currentPage = pages[pages.length - 1] as any;
  const eventChannel = currentPage?.getOpenerEventChannel?.();
  eventChannel?.on("materialDetail", (data: any) => {
    material.value = data || {};
  });
});
</script>
<style lang="scss" scoped>
.material-page {
  height: calc(100vh - 50px);
  background: #f3f9f8;
}
.page-content {
  padding: 12px 12px calc(24px + env(safe-area-inset-bottom));
}
.hero-card,
.detail-card {
  overflow: hidden;
  margin-bottom: 12px;
  background: #ffffff;
  border: 1px solid rgba(13, 134, 127, 0.08);
  border-radius: 14px;
  box-shadow: 0 5px 18px rgba(35, 91, 82, 0.06);
}
.hero-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 16px;
  background: linear-gradient(135deg, #ffffff 0%, #ecf8f5 100%);
}
.hero-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.iron-plate {
  padding: 5px 9px;
  font-size: 12px;
  font-weight: 600;
  color: #087d72;
  background: rgba(13, 134, 127, 0.1);
  border-radius: 999px;
}
.material-code {
  overflow-wrap: anywhere;
  font-size: 20px;
  font-weight: 700;
  color: #20322e;
}
.material-model {
  font-size: 14px;
  color: #697a76;
}
.section-heading {
  padding: 14px 15px 10px;
  font-size: 15px;
  font-weight: 700;
  color: #263a35;
  border-bottom: 1px solid #edf2f0;
}
.detail-row {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  justify-content: space-between;
  padding: 12px 15px;
  font-size: 14px;
  border-bottom: 1px solid #f1f4f3;
}
.detail-row:last-child {
  border-bottom: 0;
}
.detail-label {
  flex: none;
  color: #7e8d89;
}
.detail-value {
  min-width: 0;
  color: #293a36;
  text-align: right;
  overflow-wrap: anywhere;
}
.quality-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  padding: 12px 15px;
  background: #fbfdfc;
  border-bottom: 1px solid #f1f4f3;
}
.quality-item {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 12px;
  background: #f0f8f6;
  border-radius: 10px;
}
.quality-label {
  font-size: 12px;
  color: #788984;
}
.quality-value {
  font-size: 17px;
  font-weight: 700;
  color: #0d867f;
}
.empty-state {
  padding-top: 80px;
}
</style>