<template>
|
<view class="detail-page">
|
<view class="detail-header">
|
<CardTitle title="领用物料" :hideAction="false">
|
<template #action>
|
<wd-button
|
v-if="isWaiting"
|
type="icon"
|
icon="scan"
|
color="#0D867F"
|
custom-class="header-icon-button"
|
:loading="scanSubmitting"
|
@click="openScan"
|
/>
|
<wd-button
|
v-if="isWaiting"
|
type="icon"
|
icon="add-circle"
|
color="#0D867F"
|
custom-class="header-icon-button"
|
@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>
|
{{ 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>
|
|
<view v-if="loading" class="state-box">
|
<wd-loading />
|
<text>加载中...</text>
|
</view>
|
<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 displayItems" :key="getItemKey(item, index)">
|
<template #title>
|
<view class="item-title">
|
<view class="item-title-main">
|
<text>{{ getMaterialCode(item) || `明细${index + 1}` }}</text>
|
<wd-tag
|
size="small"
|
:type="
|
normalizeMaterialType(item.materialType || materialType) === '单丝'
|
? 'primary'
|
: 'success'
|
"
|
>
|
{{ normalizeMaterialType(item.materialType || materialType) }}
|
</wd-tag>
|
</view>
|
</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) === '单丝'"
|
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="质检温度(℃)" :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="received-item-actions">
|
<wd-button
|
v-if="isWaiting"
|
type="error"
|
plain
|
custom-class="delete-received-button"
|
:loading="String(deletingItemId) === String(getCandidateId(item))"
|
@click="deleteReceivedItem(item)"
|
>
|
删除领用
|
</wd-button>
|
<view class="material-detail-link" @click="openMaterialDetail(item)">
|
<text>查看详情</text>
|
<wd-icon name="arrow-right" size="16px" color="#0d867f" />
|
</view>
|
</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="closeRackSelector" />
|
</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="closeRackSelector"
|
>
|
取消
|
</wd-button>
|
<wd-button custom-class="footer-action-button" @click="confirmRackSelection">
|
确认
|
</wd-button>
|
</view>
|
</view>
|
</view>
|
</wd-popup>
|
|
<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>
|
|
<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>
|
<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"
|
>
|
<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
|
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"
|
>
|
加载更多
|
</wd-button>
|
</scroll-view>
|
|
<view class="popup-footer">
|
<text>已选 {{ selectedCandidates.length }} 条</text>
|
<view class="footer-buttons">
|
<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>
|
</view>
|
</wd-popup>
|
|
<Scan ref="scanRef" emitName="scanTimeSensitiveItem" />
|
<wd-message-box />
|
<wd-toast />
|
</view>
|
</template>
|
|
<script setup lang="ts">
|
import { onHide, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
import { useMessage, 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, { type TimelinessMaterialType } from "@/api/timeSensitiveTask";
|
|
const toast = useToast();
|
const message = useMessage();
|
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 deletingItemId = ref<number | string>("");
|
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 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 pendingScanEvent = ref<any>(null);
|
const scanAfterRackSelection = ref(false);
|
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 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) {
|
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 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 hasSelectedActiveRack() {
|
return Boolean(
|
activeRackNo.value &&
|
activeRackNo.value !== UNASSIGNED_LAYER &&
|
selectedRackNos.value.includes(activeRackNo.value)
|
);
|
}
|
|
function getMaterialCode(item: any) {
|
return getMaterialCodes(item)[0] || "";
|
}
|
|
function getMaterialCodes(item: any) {
|
const source = item?.data && typeof item.data === "object" ? { ...item, ...item.data } : item;
|
const values = [
|
source?.flowCode,
|
source?.batchCode,
|
source?.batchNo,
|
source?.productionBatchNo,
|
source?.productionBatchNumber,
|
source?.productionBatchCode,
|
source?.monofilamentNumber,
|
source?.monofilamentnumber,
|
source?.materialCode,
|
source?.systemNo,
|
source?.systemno,
|
];
|
return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
|
}
|
|
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;
|
}
|
|
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 closeRackSelector() {
|
rackSelectorVisible.value = false;
|
pendingScanEvent.value = null;
|
scanAfterRackSelection.value = false;
|
}
|
|
async function confirmRackSelection() {
|
const selectedValues = rackSelectionRows.value
|
.filter((item) => item.selected)
|
.map((item) => item.value);
|
if ((pendingScanEvent.value || scanAfterRackSelection.value) && !selectedValues.length) {
|
return toast.error("请至少选择一个铁架号");
|
}
|
|
selectedRackNos.value = selectedValues;
|
syncActiveRackNo();
|
if (
|
(pendingScanEvent.value || scanAfterRackSelection.value) &&
|
(!activeRackNo.value ||
|
activeRackNo.value === UNASSIGNED_LAYER ||
|
!selectedValues.includes(activeRackNo.value))
|
) {
|
activeRackNo.value = selectedValues[0];
|
candidateRackNo.value = selectedValues[0];
|
}
|
|
const scanEvent = pendingScanEvent.value;
|
const shouldStartScan = scanAfterRackSelection.value;
|
pendingScanEvent.value = null;
|
scanAfterRackSelection.value = false;
|
rackSelectorVisible.value = false;
|
|
if (scanEvent) {
|
await handleScan(scanEvent);
|
} else if (shouldStartScan) {
|
scanRef.value?.triggerScan();
|
}
|
}
|
|
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) => {
|
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 "";
|
}
|
|
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 }));
|
if (materialType.value === "单丝") await loadRackOptions();
|
syncActiveRackNo();
|
} catch (error: any) {
|
toast.error(error?.message || "获取领用物料失败");
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
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) {
|
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 = 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 || "查询可领用物料失败");
|
} 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 = "物料领用成功",
|
layer = candidateRackNo.value
|
) {
|
const mismatchedMaterial = rows.find(
|
(item) => item?.materialType && normalizeMaterialType(item.materialType) !== materialType.value
|
);
|
if (mismatchedMaterial) {
|
toast.error(
|
`${materialType.value}任务只能领用${materialType.value},不能领用${mismatchedMaterial.materialType}`
|
);
|
return false;
|
}
|
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 !== 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 {
|
const added = await addCandidates(selectedCandidates.value);
|
if (added) candidatePopupVisible.value = false;
|
} catch (error: any) {
|
toast.error(error?.message || "领用物料失败");
|
} finally {
|
candidateSubmitting.value = false;
|
}
|
}
|
|
async function deleteReceivedItem(item: any) {
|
if (!isWaiting.value) return toast.error("只有待开始的任务可以删除已领用物料");
|
const itemId = getCandidateId(item);
|
if (itemId === undefined || itemId === null || itemId === "") {
|
return toast.error("该物料缺少明细ID,无法删除");
|
}
|
try {
|
await message.confirm({
|
title: "删除领用",
|
msg: `确定删除已领用物料“${getMaterialCode(item) || itemId}”吗?`,
|
confirmButtonText: "确定删除",
|
cancelButtonText: "取消",
|
closeOnClickModal: false,
|
});
|
} catch {
|
return;
|
}
|
|
deletingItemId.value = itemId;
|
try {
|
await TimeSensitiveTaskApi.deleteItems({ taskId: taskId.value, itemIds: [itemId] });
|
toast.success("已删除领用物料");
|
await loadTask();
|
} catch (error: any) {
|
toast.error(error?.message || "删除领用物料失败");
|
} finally {
|
deletingItemId.value = "";
|
}
|
}
|
|
function addSingleWireCodeVariants(codes: string[]) {
|
const result = new Set(codes);
|
codes.forEach((code) => {
|
if (!/^75\d+(?:-\d+)?$/i.test(code)) return;
|
const compactCode = code.replace(/-/g, "");
|
result.add(compactCode);
|
if (compactCode.length > 3) {
|
result.add(`${compactCode.slice(0, -3)}-${compactCode.slice(-3)}`);
|
}
|
});
|
return [...result];
|
}
|
|
function readScanPayload(event: any) {
|
const rawValue = event?.code ?? event?.data ?? event;
|
let raw = typeof rawValue === "string" ? rawValue.trim() : JSON.stringify(rawValue || "");
|
try {
|
raw = decodeURIComponent(raw);
|
} catch {
|
// 扫描内容不是 URI 编码时直接使用原值。
|
}
|
|
let parsed: any = {};
|
let jsonValue: any = raw;
|
// 兼容被扫码设备再次包成字符串的 JSON:"{\"monofilamentNumber\":\"...\"}"。
|
for (let index = 0; index < 2 && typeof jsonValue === "string"; index += 1) {
|
const text = jsonValue.trim();
|
if (!text.startsWith("{") && !text.startsWith("[") && !text.startsWith('"')) break;
|
try {
|
jsonValue = JSON.parse(text);
|
} catch {
|
break;
|
}
|
}
|
if (Array.isArray(jsonValue)) parsed = jsonValue[0] || {};
|
else if (jsonValue && typeof jsonValue === "object") parsed = jsonValue;
|
|
const outputId = parsed.id ?? parsed.outPutId ?? parsed.outputId;
|
const ironPlateNo = parsed.ironPlateNo ?? parsed.ironPlateNumber;
|
const parsedCodes = getMaterialCodes(parsed);
|
const singleWireMatch = raw.match(/(?:^|[^0-9])(75\d+(?:-\d+)?)(?=$|[^0-9])/i)?.[1];
|
const strandedWireMatch = raw.match(/(?:^|[^0-9])(74\d+)(?=$|[^0-9])/i)?.[1];
|
const typeCode = materialType.value === "单丝" ? singleWireMatch : strandedWireMatch;
|
let materialCodes = [...parsedCodes, ...(typeCode ? [typeCode] : [])];
|
|
// 流转卡正常为 monofilamentNumber JSON;同时兼容纯编号和“单丝编号:...”文本。
|
if (!materialCodes.length && !Object.keys(parsed).length) {
|
const plainCode = raw.match(/[A-Za-z0-9][A-Za-z0-9._/-]*/)?.[0];
|
if (plainCode) materialCodes.push(plainCode);
|
}
|
materialCodes = [...new Set(materialCodes.map((code) => code.trim()).filter(Boolean))];
|
if (materialType.value === "单丝") materialCodes = addSingleWireCodeVariants(materialCodes);
|
|
return {
|
raw,
|
parsed,
|
outputId,
|
ironPlateNo,
|
monofilamentNumber: String(
|
parsed.monofilamentNumber ?? parsed.monofilamentnumber ?? typeCode ?? ""
|
).trim(),
|
materialCodes,
|
};
|
}
|
|
async function handleScan(event: any) {
|
if (!pageVisible.value || scanSubmitting.value) return;
|
if (!isWaiting.value) return toast.error("只有待开始的任务可以扫码新增明细");
|
if (materialType.value === "单丝" && !hasSelectedActiveRack()) {
|
pendingScanEvent.value = event;
|
await openRackSelector();
|
return;
|
}
|
scanSubmitting.value = true;
|
try {
|
const scan = readScanPayload(event);
|
const batchCode = scan.monofilamentNumber || scan.materialCodes[0];
|
if (!batchCode) {
|
return toast.error("二维码格式错误,未识别到单丝编号或生产批次号");
|
}
|
|
const { data: candidate } = await TimeSensitiveTaskApi.scanByBatchCode({ batchCode });
|
if (!candidate) {
|
return toast.error(`未找到批次号为 ${batchCode} 的待时效物料`);
|
}
|
if (candidate.available === false) {
|
return toast.error(candidate.unavailableReason || "该物料当前不可领用");
|
}
|
if (
|
candidate.materialType &&
|
normalizeMaterialType(candidate.materialType) !== materialType.value
|
) {
|
return toast.error(
|
`当前任务只能领用${materialType.value},该流转卡为${candidate.materialType}`
|
);
|
}
|
const ironPlateNo =
|
materialType.value === "单丝"
|
? Number(scan.ironPlateNo) || getNextIronPlateNo(activeRackNo.value)
|
: getNextIronPlateNo();
|
await addCandidates([{ ...candidate, ironPlateNo }], "扫码领用成功", activeRackNo.value);
|
} catch (error: any) {
|
toast.error(error?.message || "扫码领用物料失败");
|
} finally {
|
scanSubmitting.value = false;
|
}
|
}
|
|
async function openScan() {
|
if (!isWaiting.value) return toast.error("只有待开始的任务可以扫码新增明细");
|
if (materialType.value === "单丝") {
|
await loadRackOptions();
|
syncActiveRackNo();
|
if (!hasSelectedActiveRack()) {
|
scanAfterRackSelection.value = true;
|
await openRackSelector();
|
return;
|
}
|
}
|
scanRef.value?.triggerScan();
|
}
|
|
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: 100vh;
|
background: #f3f9f8;
|
}
|
|
.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 {
|
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-title {
|
font-size: 19px;
|
}
|
|
.summary-grid {
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 16px 18px;
|
font-size: 17px;
|
font-weight: 500;
|
line-height: 1.45;
|
color: #333333;
|
}
|
|
.summary-grid > view {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
min-width: 0;
|
overflow-wrap: anywhere;
|
}
|
|
.label,
|
.popup-subtitle,
|
.candidate-meta {
|
font-size: 12px;
|
color: #999999;
|
}
|
|
.summary-grid .label {
|
font-size: 14px;
|
font-weight: 400;
|
color: #7e8987;
|
}
|
|
.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 {
|
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;
|
gap: 6px;
|
align-items: center;
|
justify-content: center;
|
min-height: 44px;
|
padding: 0 12px;
|
font-size: 12px;
|
color: #999999;
|
}
|
|
.layer-tabs-wrap :deep(.wd-tabs__nav),
|
.candidate-layer-tabs :deep(.wd-tabs__nav) {
|
background: #ffffff;
|
}
|
|
.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 {
|
padding: 0 10px 12px;
|
}
|
|
.item-list :deep(.wd-card) {
|
margin: 0 0 10px;
|
}
|
|
.received-item-actions {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 10px 12px;
|
background: #f8fbfa;
|
border-top: 1px solid #edf2f0;
|
}
|
|
.received-item-actions :deep(.delete-received-button) {
|
min-width: 108px;
|
height: 44px;
|
font-size: 16px;
|
font-weight: 600;
|
border-radius: 9px;
|
}
|
|
.material-detail-link {
|
display: flex;
|
gap: 6px;
|
align-items: center;
|
min-height: 44px;
|
padding: 0 4px 0 18px;
|
font-size: 15px;
|
color: #0d867f;
|
}
|
|
.received-item-actions .material-detail-link {
|
margin-left: auto;
|
}
|
|
.candidate-item > .material-detail-link {
|
justify-content: flex-end;
|
padding: 0 12px;
|
background: #f8fbfa;
|
border-top: 1px solid #edf2f0;
|
}
|
|
.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;
|
flex-direction: column;
|
overflow: hidden;
|
margin-bottom: 8px;
|
background: #ffffff;
|
border: 1px solid #eeeeee;
|
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 {
|
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: 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>
|