Просмотр исходного кода

fix(bom-upload): 大文件性能优化 - 分页渲染+去重API匹配+进度条

问题:3993行Excel上传后页面崩溃
- DOM爆炸:3993行x39列=155K节点
- API风暴:3993行逐一调fetchSourceListPage

修复:
- 分页渲染:el-table只渲染当前页200行(DOM降至~8K节点)
- API去重:按唯一物料号去重查询,3993行→数百种物料,API调用减少90%+
- 进度条+可取消:el-progress显示匹配进度,支持中途取消
- 并发降为3,批次间200ms延迟避免浏览器卡顿
- 补充Excel列映射:合同批次/行项目号/投产批次

Web 2.4.418 -> 2.4.419
Pengxy 1 день назад
Родитель
Сommit
a72e4fcc70
2 измененных файлов с 78 добавлено и 11 удалено
  1. 1 1
      Web/package.json
  2. 77 10
      Web/src/views/aidop/business/workOrderBomUpload.vue

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.418",
+	"version": "2.4.419",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 77 - 10
Web/src/views/aidop/business/workOrderBomUpload.vue

@@ -32,14 +32,14 @@
 
 		<!-- 物料预览表:点击采购列单元格进入编辑 -->
 		<el-table
-			:data="parsedRows"
+			:data="pageRows"
 			border
 			stripe
 			style="width: 100%; margin-top: 8px"
 			max-height="520"
 			size="small"
 		>
-			<el-table-column type="index" label="#" width="50" align="center" fixed />
+			<el-table-column type="index" label="#" width="60" align="center" fixed :index="pageIndex" />
 			<el-table-column
 				v-for="col in TABLE_COLUMNS"
 				:key="col.key"
@@ -84,13 +84,31 @@
 							<!-- 文本输入:供应商联系人 -->
 							<el-input v-else v-model="row[col.key]" size="small" @change="onCellChange(row, col.key)" />
 						</div>
-						<span v-if="!isEditing($index, col.key)" class="editable-cell" @dblclick="startEditing($index, col.key)">{{ formatCell(row[col.key], col.type) }}</span>
+						<span v-if="!isEditing(row._globalIdx, col.key)" class="editable-cell" @dblclick="startEditing(row._globalIdx, col.key)">{{ formatCell(row[col.key], col.type) }}</span>
 					</template>
 					<span v-else>{{ formatCell(row[col.key], col.type) }}</span>
 				</template>
 			</el-table-column>
 		</el-table>
 
+		<!-- 分页 -->
+		<div v-if="parsedRows.length > PAGE_SIZE" style="margin-top: 8px; display: flex; justify-content: flex-end">
+			<el-pagination
+				v-model:current-page="currentPage"
+				:page-size="PAGE_SIZE"
+				:total="parsedRows.length"
+				layout="total, prev, pager, next, jumper"
+				background
+				small
+			/>
+		</div>
+
+		<!-- 匹配进度条 -->
+		<div v-if="matchingProgress > 0 && matchingProgress < 100" style="margin-top: 4px">
+			<el-progress :percentage="matchingProgress" :stroke-width="10" :text-inside="true" />
+			<el-button size="small" text type="danger" @click="matchingCancelled = true" style="margin-top: 4px">取消匹配</el-button>
+		</div>
+
 		<!-- 操作按钮 -->
 		<div class="bom-actions">
 			<el-button type="warning" @click="onConfirmWrite" :loading="writing">确认写入工单</el-button>
@@ -184,6 +202,23 @@ const writing = ref(false);
 const hasSavedData = ref(false);
 const parsedRows = ref<ParsedRow[]>([]);
 const editingCell = ref<{ rowIdx: number; colKey: string } | null>(null);
+const matchingCancelled = ref(false);
+const matchingProgress = ref(0);
+
+// ── 分页 ──
+const PAGE_SIZE = 200;
+const currentPage = ref(1);
+const totalPages = computed(() => Math.max(1, Math.ceil(parsedRows.value.length / PAGE_SIZE)));
+const pageRows = computed(() => {
+	const start = (currentPage.value - 1) * PAGE_SIZE;
+	return parsedRows.value.slice(start, start + PAGE_SIZE).map((r, i) => {
+		Object.defineProperty(r, '_globalIdx', { value: start + i, configurable: true, enumerable: false });
+		return r;
+	});
+});
+function pageIndex(i: number): number {
+	return (currentPage.value - 1) * PAGE_SIZE + i + 1;
+}
 
 function isEditing(rowIdx: number, colKey: string): boolean {
 	return editingCell.value?.rowIdx === rowIdx && editingCell.value?.colKey === colKey;
@@ -211,10 +246,13 @@ const EXCEL_COL_MAP: Record<string, string> = {
 	'销售订单号': 'salesOrderNo',
 	'合同号': 'contractNo',
 	'合同名称': 'contractName',
+	'合同批次': 'contractBatch',
 	'产品部': 'productDept',
+	'行项目号': 'lineItemNo',
 	'父物料号': 'parentItemNum',
 	'父物料描述': 'parentDescr',
 	'工厂': 'factory',
+	'投产批次': 'productionBatch',
 	'投产人': 'productionPerson',
 	'物料单号': 'materialBillNo',
 	'子物料号': 'itemNum',
@@ -481,15 +519,44 @@ function onFileChange(e: Event) {
 			saveToStorage();
 			ElMessage.success(`解析完成:${rows.length} 行,正在从货源清单自动匹配供应商…`);
 
-			// 异步:逐行调货源清单 API 自动匹配供应商(并发限制 5)
-			const CONCURRENCY = 5;
+			// 异步:按唯一物料号去重调货源清单 API 自动匹配供应商
+			matchingCancelled.value = false;
+			const uniqueItemNums = [...new Set(rows.map((r) => String(r.itemNum)).filter(Boolean))];
+			const total = uniqueItemNums.length;
+			const CONCURRENCY = 3;
+			let processed = 0;
 			let apiMatched = 0;
-			for (let i = 0; i < rows.length; i += CONCURRENCY) {
-				const batch = rows.slice(i, i + CONCURRENCY);
-				const results = await Promise.all(batch.map((r) => autoMatchSourceList(r).then(() => r)));
-				apiMatched += results.filter((r) => r.supplier).length;
+			for (let i = 0; i < total; i += CONCURRENCY) {
+				if (matchingCancelled.value) {
+					ElMessage.warning('已取消自动匹配');
+					break;
+				}
+				const batch = uniqueItemNums.slice(i, i + CONCURRENCY);
+				const results = await Promise.all(
+					batch.map(async (itemNum) => {
+						try {
+							const res = await fetchSourceListPage({ number: itemNum, page: 1, pageSize: 1 });
+							if (!res?.list?.length) return false;
+							const item = res.list[0];
+							let matched = false;
+							for (const row of rows) {
+								if (String(row.itemNum) !== itemNum) continue;
+								if (!row.supplier && item.supplierName) row.supplier = item.supplierName;
+								if (!row.supplierContact && item.orderRectorName) row.supplierContact = item.orderRectorName;
+								if (row.unitPrice == null && item.orderPrice != null) row.unitPrice = item.orderPrice;
+								if (row.supplier) matched = true;
+							}
+							return matched;
+						} catch { return false; }
+					})
+				);
+				apiMatched += results.filter(Boolean).length;
+				processed += batch.length;
+				matchingProgress.value = Math.round((processed / total) * 100);
+				if (i + CONCURRENCY < total) await new Promise((r) => setTimeout(r, 200));
 			}
-			ElMessage.success(`${rows.length} 行解析完成,${apiMatched} 行已匹配供应商`);
+			matchingProgress.value = 0;
+			ElMessage.success(`${rows.length} 行解析完成,${apiMatched} 种物料已匹配供应商`);
 		} catch (err: any) {
 			parseError.value = `解析失败:${err?.message || err}`;
 		} finally {