| | |
| | | <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" |
| | | /> |
| | |
| | | type="icon" |
| | | icon="add-circle" |
| | | color="#0D867F" |
| | | custom-class="header-icon-button" |
| | | @click="openCandidatePopup" |
| | | /> |
| | | </template> |
| | |
| | | {{ materialType }} |
| | | </view> |
| | | <view> |
| | | <text class="label">明细</text> |
| | | <text class="label">已领用</text> |
| | | {{ items.length }} 件 |
| | | </view> |
| | | <view> |
| | |
| | | </wd-card> |
| | | |
| | | <view class="section-title"> |
| | | <text>时效明细({{ items.length }})</text> |
| | | <view v-if="isWaiting" class="section-actions"> |
| | | <text class="section-link" @click="toggleSelectAll"> |
| | | {{ allItemsSelected ? "取消全选" : "全选" }} |
| | | </text> |
| | | <text class="section-link danger" @click="deleteSelectedItems"> |
| | | 删除所选({{ selectedItems.length }}) |
| | | </text> |
| | | <text class="section-link" @click="openCandidatePopup">手动添加</text> |
| | | <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> |
| | | |
| | |
| | | <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" |
| | |
| | | {{ 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) === '单丝'" |
| | |
| | | :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" |
| | |
| | | <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> |
| | |
| | | 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" |
| | | > |
| | |
| | | <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> |
| | |
| | | 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) |
| | | ); |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | 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; |
| | |
| | | ? 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) { |
| | |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | 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")); |
| | |
| | | raw, |
| | | parsed, |
| | | outputId, |
| | | ironPlateNo, |
| | | materialCode: singleWireCode || (!Object.keys(parsed).length ? raw : ""), |
| | | }; |
| | | } |
| | |
| | | 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); |
| | |
| | | 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) => { |
| | |
| | | .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 { |
| | |
| | | } |
| | | |
| | | .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 { |
| | |
| | | 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 { |
| | |
| | | |
| | | .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 { |
| | |
| | | .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> |