| src/api/timeSensitiveTask.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/timely/detail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/timely/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/timely/materialDetail.vue | ●●●●● 补丁 | 查看 | 原始文档 | 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> {{ materialType === "åä¸" ? "å½åéæ¶" : "é¢ç¨ç©æ" }}ï¼{{ displayItems.length }}ï¼ </text> <text class="section-link danger" @click="deleteSelectedItems"> å 餿é({{ selectedItems.length }}) </text> <text class="section-link" @click="openCandidatePopup">æå¨æ·»å </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-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 || "-" }} · {{ candidate.weight ?? "-" }} kg</text> <text v-if="materialType === 'åä¸'" class="candidate-meta"> {{ candidate.poleModel || candidate.rodModel || "-" }} · {{ candidate.rodNo || candidate.poleNumber || "-" }} </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("åªæå¾ å¼å§çä»»å¡å¯ä»¥æ«ç æ°å¢æç»"); scanRef.value?.triggerScan(); if (materialType.value === "åä¸") { await loadRackOptions(); syncActiveRackNo(); if (!activeRackNo.value || activeRackNo.value === UNASSIGNED_LAYER) { toast.error("请å éæ©åä¸é¢ç¨éæ¶å·"); openRackSelector(); return; } 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 }) }); } scanRef.value?.triggerScan(); } 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>