|
@@ -0,0 +1,426 @@
|
|
|
|
|
+import { useState } from 'react';
|
|
|
|
|
+import * as XLSX from 'xlsx';
|
|
|
|
|
+import { message } from 'antd';
|
|
|
|
|
+import { ExcelRow, SheetConfig } from '../types';
|
|
|
|
|
+
|
|
|
|
|
+type ExcelJsonData = Array<Array<string | number | null>>;
|
|
|
|
|
+
|
|
|
|
|
+export const useExcelParser = () => {
|
|
|
|
|
+ const [jsonData, setJsonData] = useState<{ [key: string]: ExcelRow[] }>({});
|
|
|
|
|
+ // 存储所有字段的原始数据,不受exportFields配置影响
|
|
|
|
|
+ const [allFieldsData, setAllFieldsData] = useState<{ [key: string]: ExcelRow[] }>({});
|
|
|
|
|
+
|
|
|
|
|
+ const processDataForExport = (
|
|
|
|
|
+ data: ExcelRow[],
|
|
|
|
|
+ sheetConfig: SheetConfig,
|
|
|
|
|
+ tableIndex: number = 0
|
|
|
|
|
+ ): ExcelRow[] => {
|
|
|
|
|
+ return data
|
|
|
|
|
+ .map((row, index) => {
|
|
|
|
|
+ const processedRow: ExcelRow = {};
|
|
|
|
|
+ processedRow._id = `table-${tableIndex}-row-${index}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果exportFields为空,保留所有原始字段
|
|
|
|
|
+ if (sheetConfig.exportFields.length === 0) {
|
|
|
|
|
+ Object.keys(row).forEach(field => {
|
|
|
|
|
+ processedRow[field] = row[field] ?? null;
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 按照exportFields配置处理
|
|
|
|
|
+ sheetConfig.exportFields.forEach(field => {
|
|
|
|
|
+ const mappedField = sheetConfig.fieldMappings[field] || field;
|
|
|
|
|
+ processedRow[mappedField] = row[field] ?? null;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return processedRow;
|
|
|
|
|
+ })
|
|
|
|
|
+ .filter(row => {
|
|
|
|
|
+ // 如果没有设置必需字段或exportFields为空,则不过滤
|
|
|
|
|
+ if (sheetConfig.requiredFields.length === 0 || sheetConfig.exportFields.length === 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查必需字段是否有值
|
|
|
|
|
+ return sheetConfig.requiredFields.every(field => {
|
|
|
|
|
+ const value = row[sheetConfig.fieldMappings[field] || field];
|
|
|
|
|
+ return value !== null && value !== undefined && value !== '';
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理所有原始字段数据,不过滤
|
|
|
|
|
+ const processAllFieldsData = (
|
|
|
|
|
+ data: ExcelRow[],
|
|
|
|
|
+ tableIndex: number = 0
|
|
|
|
|
+ ): ExcelRow[] => {
|
|
|
|
|
+ return data.map((row, index) => {
|
|
|
|
|
+ const processedRow: ExcelRow = { ...row };
|
|
|
|
|
+ processedRow._id = `table-${tableIndex}-row-${index}`;
|
|
|
|
|
+ return processedRow;
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 获取所有可用字段
|
|
|
|
|
+ const getAllAvailableFields = (data: { [key: string]: ExcelRow[] }): { [key: string]: string[] } => {
|
|
|
|
|
+ // 按工作表名收集字段
|
|
|
|
|
+ const fieldsMap: { [key: string]: Set<string> } = {};
|
|
|
|
|
+
|
|
|
|
|
+ // 遍历每个工作表
|
|
|
|
|
+ Object.entries(data).forEach(([sheetName, rows]) => {
|
|
|
|
|
+ if (!fieldsMap[sheetName]) {
|
|
|
|
|
+ fieldsMap[sheetName] = new Set<string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从每行中收集字段
|
|
|
|
|
+ rows.forEach(row => {
|
|
|
|
|
+ Object.keys(row).forEach(key => {
|
|
|
|
|
+ // 过滤掉内部属性(如_id, tableIndex等)
|
|
|
|
|
+ if (!key.startsWith('_')) {
|
|
|
|
|
+ fieldsMap[sheetName].add(key);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 将Set转换为数组
|
|
|
|
|
+ const result: { [key: string]: string[] } = {};
|
|
|
|
|
+ Object.entries(fieldsMap).forEach(([sheetName, fieldsSet]) => {
|
|
|
|
|
+ result[sheetName] = Array.from(fieldsSet);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理单个表格的数据
|
|
|
|
|
+ const parseSingleTable = (
|
|
|
|
|
+ json: ExcelJsonData,
|
|
|
|
|
+ headers: string[],
|
|
|
|
|
+ startRow: number,
|
|
|
|
|
+ endMarker: string,
|
|
|
|
|
+ orderNumberRow: number,
|
|
|
|
|
+ orderNumberCol: number,
|
|
|
|
|
+ productNameRow?: number,
|
|
|
|
|
+ productNameCol?: number
|
|
|
|
|
+ ): { data: ExcelRow[]; endIndex: number } => {
|
|
|
|
|
+ const tableData: ExcelRow[] = [];
|
|
|
|
|
+ let currentOrderNumber: string | null = null;
|
|
|
|
|
+ let currentProductName: string | null = null;
|
|
|
|
|
+ let endIndex = json.length;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取订单号
|
|
|
|
|
+ if (orderNumberRow > 0 && orderNumberCol > 0) {
|
|
|
|
|
+ currentOrderNumber = String(json[orderNumberRow - 1]?.[orderNumberCol - 1] ?? '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取产品名称(如果配置了产品名称行列号)
|
|
|
|
|
+ if (productNameRow && productNameCol && productNameRow > 0 && productNameCol > 0) {
|
|
|
|
|
+ currentProductName = String(json[productNameRow - 1]?.[productNameCol - 1] ?? '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = startRow; i < json.length; i++) {
|
|
|
|
|
+ const row = json[i] || [];
|
|
|
|
|
+ const rowString = row.join('');
|
|
|
|
|
+
|
|
|
|
|
+ if (rowString.includes(endMarker)) {
|
|
|
|
|
+ endIndex = i;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const obj: ExcelRow = {};
|
|
|
|
|
+ let hasData = false;
|
|
|
|
|
+
|
|
|
|
|
+ headers.forEach((header, index) => {
|
|
|
|
|
+ if (header) {
|
|
|
|
|
+ const value = row[index];
|
|
|
|
|
+ if (value !== undefined) {
|
|
|
|
|
+ obj[header] = value;
|
|
|
|
|
+ hasData = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ obj[header] = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (currentOrderNumber) {
|
|
|
|
|
+ obj['订单号'] = currentOrderNumber;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (currentProductName) {
|
|
|
|
|
+ obj['产品名称'] = currentProductName;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (hasData) {
|
|
|
|
|
+ tableData.push(obj);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { data: tableData, endIndex };
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理多表格模式
|
|
|
|
|
+ const parseMultiTable = (
|
|
|
|
|
+ json: ExcelJsonData,
|
|
|
|
|
+ sheetConfig: SheetConfig
|
|
|
|
|
+ ): ExcelRow[][] => {
|
|
|
|
|
+ const tableDataSets: ExcelRow[][] = [];
|
|
|
|
|
+ let currentIndex = sheetConfig.dataStartRow - 1;
|
|
|
|
|
+ const headerOffset = sheetConfig.multiTableHeaderOffset || 1;
|
|
|
|
|
+ const dataOffset = sheetConfig.multiTableDataOffset || 1;
|
|
|
|
|
+ const orderNumberOffset = sheetConfig.multiTableOrderNumberOffset || -1;
|
|
|
|
|
+ const productNameOffset = sheetConfig.multiTableProductNameOffset || -1;
|
|
|
|
|
+
|
|
|
|
|
+ while (currentIndex < json.length) {
|
|
|
|
|
+ // 获取当前表格的表头行号
|
|
|
|
|
+ const headerIndex = currentIndex - (sheetConfig.dataStartRow - sheetConfig.headerRowIndex);
|
|
|
|
|
+ // 获取当前表格的表头
|
|
|
|
|
+ const headers = (json[headerIndex] || []) as string[];
|
|
|
|
|
+ if (!headers || headers.length === 0) break;
|
|
|
|
|
+
|
|
|
|
|
+ // 计算订单号行号
|
|
|
|
|
+ const orderNumberIndex = headerIndex + (orderNumberOffset || 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算产品名称行号(如果配置了产品名称行列号)
|
|
|
|
|
+ let productNameIndex = undefined;
|
|
|
|
|
+ if (sheetConfig.productNameRow && sheetConfig.productNameCol) {
|
|
|
|
|
+ productNameIndex = headerIndex + (productNameOffset || 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析当前表格
|
|
|
|
|
+ const { data, endIndex } = parseSingleTable(
|
|
|
|
|
+ json,
|
|
|
|
|
+ headers,
|
|
|
|
|
+ currentIndex,
|
|
|
|
|
+ sheetConfig.endMarker,
|
|
|
|
|
+ orderNumberIndex + 1, // +1 因为 parseSingleTable 内部会 -1
|
|
|
|
|
+ sheetConfig.orderNumberCol,
|
|
|
|
|
+ productNameIndex !== undefined ? productNameIndex + 1 : undefined, // +1 因为 parseSingleTable 内部会 -1
|
|
|
|
|
+ sheetConfig.productNameCol
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (data.length > 0) {
|
|
|
|
|
+ tableDataSets.push(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有找到结束标记,退出循环
|
|
|
|
|
+ if (endIndex === json.length) break;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新下一个表格的起始位置
|
|
|
|
|
+ currentIndex = endIndex + headerOffset + dataOffset;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return tableDataSets;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const parseExcelFile = async (file: File, sheetConfigs: SheetConfig[]) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 显示开始处理的消息
|
|
|
|
|
+ const loadingMessage = message.loading({ content: `正在解析Excel文件: ${file.name}`, key: 'excelParsing', duration: 0 });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('开始处理文件:', file.name);
|
|
|
|
|
+ const data = await file.arrayBuffer();
|
|
|
|
|
+ // 更新进度提示
|
|
|
|
|
+ message.loading({ content: '正在读取Excel数据...', key: 'excelParsing', duration: 0 });
|
|
|
|
|
+
|
|
|
|
|
+ const workbook = XLSX.read(data);
|
|
|
|
|
+
|
|
|
|
|
+ const rawData: { [key: string]: ExcelRow[] } = {};
|
|
|
|
|
+ let totalTables = 0;
|
|
|
|
|
+ let processedSheets = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 处理每个配置的工作表
|
|
|
|
|
+ for (const sheetConfig of sheetConfigs) {
|
|
|
|
|
+ // 更新进度提示,显示当前处理的工作表
|
|
|
|
|
+ message.loading({
|
|
|
|
|
+ content: `正在处理工作表 "${sheetConfig.sheetName}" (${processedSheets + 1}/${sheetConfigs.length})`,
|
|
|
|
|
+ key: 'excelParsing',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const worksheet = workbook.Sheets[sheetConfig.sheetName];
|
|
|
|
|
+
|
|
|
|
|
+ if (!worksheet) {
|
|
|
|
|
+ console.error('未找到工作表:', sheetConfig.sheetName);
|
|
|
|
|
+ message.error({
|
|
|
|
|
+ content: `未找到工作表"${sheetConfig.sheetName}"!`,
|
|
|
|
|
+ key: 'excelParsing'
|
|
|
|
|
+ });
|
|
|
|
|
+ processedSheets++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const json = XLSX.utils.sheet_to_json<string[]>(worksheet, { header: 1 }) as ExcelJsonData;
|
|
|
|
|
+ const headers = json[sheetConfig.headerRowIndex - 1] as string[];
|
|
|
|
|
+
|
|
|
|
|
+ if (!headers || headers.length === 0) {
|
|
|
|
|
+ console.error(`工作表 ${sheetConfig.sheetName} 的表头数据无效`);
|
|
|
|
|
+ message.warning({
|
|
|
|
|
+ content: `工作表 "${sheetConfig.sheetName}" 的表头数据无效,已跳过`,
|
|
|
|
|
+ key: 'excelParsing'
|
|
|
|
|
+ });
|
|
|
|
|
+ processedSheets++;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (sheetConfig.isMultiTable) {
|
|
|
|
|
+ // 处理多表格模式
|
|
|
|
|
+ message.loading({
|
|
|
|
|
+ content: `正在处理 "${sheetConfig.sheetName}" 的多表格数据...`,
|
|
|
|
|
+ key: 'excelParsing',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const tableDataSets = parseMultiTable(json, sheetConfig);
|
|
|
|
|
+
|
|
|
|
|
+ if (tableDataSets.length > 0) {
|
|
|
|
|
+ message.loading({
|
|
|
|
|
+ content: `"${sheetConfig.sheetName}" 中发现 ${tableDataSets.length} 个数据表,正在处理...`,
|
|
|
|
|
+ key: 'excelParsing',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 保存所有原始字段数据
|
|
|
|
|
+ rawData[sheetConfig.sheetName] = tableDataSets.flatMap((tableData, tableIndex) => {
|
|
|
|
|
+ return processAllFieldsData(tableData, tableIndex + 1).map(row => ({
|
|
|
|
|
+ ...row,
|
|
|
|
|
+ tableIndex: tableIndex + 1,
|
|
|
|
|
+ }));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ totalTables += tableDataSets.length;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message.warning({
|
|
|
|
|
+ content: `未在 "${sheetConfig.sheetName}" 中找到有效的数据表`,
|
|
|
|
|
+ duration: 2
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 处理单表格模式
|
|
|
|
|
+ message.loading({
|
|
|
|
|
+ content: `正在处理 "${sheetConfig.sheetName}" 的单表格数据...`,
|
|
|
|
|
+ key: 'excelParsing',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const { data } = parseSingleTable(
|
|
|
|
|
+ json,
|
|
|
|
|
+ headers,
|
|
|
|
|
+ sheetConfig.dataStartRow - 1,
|
|
|
|
|
+ sheetConfig.endMarker,
|
|
|
|
|
+ sheetConfig.orderNumberRow,
|
|
|
|
|
+ sheetConfig.orderNumberCol,
|
|
|
|
|
+ sheetConfig.productNameRow || undefined,
|
|
|
|
|
+ sheetConfig.productNameCol || undefined
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (data.length > 0) {
|
|
|
|
|
+ // 保存所有原始字段数据
|
|
|
|
|
+ rawData[sheetConfig.sheetName] = processAllFieldsData(data, 1);
|
|
|
|
|
+
|
|
|
|
|
+ totalTables += 1;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ message.warning({
|
|
|
|
|
+ content: `未在 "${sheetConfig.sheetName}" 中找到有效数据`,
|
|
|
|
|
+ duration: 2
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ processedSheets++;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新进度提示
|
|
|
|
|
+ if (processedSheets < sheetConfigs.length) {
|
|
|
|
|
+ message.loading({
|
|
|
|
|
+ content: `已处理 ${processedSheets}/${sheetConfigs.length} 个工作表...`,
|
|
|
|
|
+ key: 'excelParsing',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新进度提示
|
|
|
|
|
+ message.loading({
|
|
|
|
|
+ content: '正在整理数据字段...',
|
|
|
|
|
+ key: 'excelParsing',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 更新状态
|
|
|
|
|
+ setAllFieldsData(rawData);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算可用字段(按工作表分类)
|
|
|
|
|
+ const availableFieldsBySheet = getAllAvailableFields(rawData);
|
|
|
|
|
+
|
|
|
|
|
+ // 完成解析,显示成功消息
|
|
|
|
|
+ message.success({
|
|
|
|
|
+ content: `Excel文件解析成功!共处理 ${Object.keys(rawData).length} 个工作表,${totalTables} 个数据表`,
|
|
|
|
|
+ key: 'excelParsing',
|
|
|
|
|
+ duration: 3
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('所有字段原始数据:', rawData);
|
|
|
|
|
+ console.log('按工作表分类的可用字段:', availableFieldsBySheet);
|
|
|
|
|
+
|
|
|
|
|
+ // 返回原始数据、分类字段
|
|
|
|
|
+ return {
|
|
|
|
|
+ rawData,
|
|
|
|
|
+ availableFieldsBySheet,
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('文件处理错误:', error);
|
|
|
|
|
+ message.error({
|
|
|
|
|
+ content: '文件处理失败,请检查文件格式是否正确!',
|
|
|
|
|
+ key: 'excelParsing'
|
|
|
|
|
+ });
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 根据当前配置动态生成导出数据
|
|
|
|
|
+ const generateExportData = (sheetConfigs: SheetConfig[]) => {
|
|
|
|
|
+ // 如果没有原始数据,直接返回空对象
|
|
|
|
|
+ if (Object.keys(allFieldsData).length === 0) {
|
|
|
|
|
+ return {};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const exportData: { [key: string]: ExcelRow[] } = {};
|
|
|
|
|
+
|
|
|
|
|
+ // 处理每个工作表的数据
|
|
|
|
|
+ for (const sheetConfig of sheetConfigs) {
|
|
|
|
|
+ const rawDataForSheet = allFieldsData[sheetConfig.sheetName];
|
|
|
|
|
+
|
|
|
|
|
+ if (!rawDataForSheet || rawDataForSheet.length === 0) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 根据当前配置处理数据
|
|
|
|
|
+ exportData[sheetConfig.sheetName] = processDataForExport(
|
|
|
|
|
+ rawDataForSheet,
|
|
|
|
|
+ sheetConfig,
|
|
|
|
|
+ 1
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 只有当数据确实发生变化时才更新状态,避免不必要的渲染
|
|
|
|
|
+ const currentJsonData = JSON.stringify(jsonData);
|
|
|
|
|
+ const newJsonData = JSON.stringify(exportData);
|
|
|
|
|
+
|
|
|
|
|
+ if (currentJsonData !== newJsonData) {
|
|
|
|
|
+ // 更新 jsonData 状态
|
|
|
|
|
+ setJsonData(exportData);
|
|
|
|
|
+ console.log('生成的导出数据:', exportData);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return exportData;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getFieldsBySheet = getAllAvailableFields;
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ jsonData,
|
|
|
|
|
+ allFieldsData,
|
|
|
|
|
+ parseExcelFile,
|
|
|
|
|
+ generateExportData,
|
|
|
|
|
+ getFieldsBySheet
|
|
|
|
|
+ };
|
|
|
|
|
+};
|