|
|
@@ -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 {
|