From b180462e9702dc3ee10ec60e19bc4878d343f87f Mon Sep 17 00:00:00 2001 From: "googosoft.shixing" <15118577+shi1xing2@user.noreply.gitee.com> Date: Fri, 29 Aug 2025 14:00:21 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BC=98=E5=8C=96=E5=B1=9E=E6=80=A7=E5=80=BC?= =?UTF-8?q?=E7=A9=BF=E9=80=8F=E9=80=9F=E5=BA=A6=202.=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A4=9A=E9=80=89=E5=8D=95=E5=85=83=E6=A0=BC=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=88=E5=A4=9A=E4=B8=AA=E5=8D=95=E5=85=83=E6=A0=BC=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E8=BF=9B=E8=A1=8C=E5=B1=9E=E6=80=A7=E5=80=BC=E7=A9=BF?= =?UTF-8?q?=E9=80=8F=EF=BC=89=203.=E4=BF=AE=E6=94=B9=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NewReportForm/Viewer/hotTable.tsx | 259 +++++++++++++----- src/ts/base/model.ts | 12 +- src/ts/core/work/assign/assign.ts | 104 +++++-- 3 files changed, 280 insertions(+), 95 deletions(-) diff --git a/src/components/DataStandard/NewReportForm/Viewer/hotTable.tsx b/src/components/DataStandard/NewReportForm/Viewer/hotTable.tsx index f2d5afa58..f92bb66ec 100644 --- a/src/components/DataStandard/NewReportForm/Viewer/hotTable.tsx +++ b/src/components/DataStandard/NewReportForm/Viewer/hotTable.tsx @@ -53,6 +53,12 @@ import { CloseOutlined, ExportOutlined } from '@ant-design/icons'; registerLanguageDictionary(zhCN); registerAllModules(); +type SelectionRange = { + startRow: number; + startCol: number; + endRow: number; + endCol: number; +}; const HotTableView: React.FC<{ data: { [key: string]: any }; @@ -93,7 +99,8 @@ const HotTableView: React.FC<{ const [cacheCellsData, setCacheCellsData] = useState({}); const [fieldId, setFieldId] = useState(props.sheet.attributeId!); const [ready, setReady] = useState(false); - const [currentCell, setCurrentCell] = useState(null); + const [currentCells, setCurrentCells] = useState(null); + const [selectionRange, setSelectionRange] = useState(null); const fieldMap = useMemo(() => { return props.fields.reduce>((a, v) => { a[v.id] = v; @@ -101,6 +108,43 @@ const HotTableView: React.FC<{ }, {}); }, [props.fields]); + const summaryColumns = useMemo(() => { + const fixedColumns = [ + { + title: '节点名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '节点类型', + dataIndex: 'nodeTypeName', + key: 'nodeTypeName', + width: 120, + render: (v: string) => {v}, + }, + ]; + + const dynamicCellColumns = + currentCells?.map((cell) => { + const cellCoord = excelCellRef({ row: cell.row, col: cell.col }); + + return { + title: cellCoord, + key: cellCoord, + width: 200, + align: 'right' as const, + render: (_: any, record: model.SummaryAssignTaskView) => { + const cellValue = record.values?.[cellCoord] ?? record.value ?? ''; + return cellValue == null + ? '' + : formatNumber(cellValue, cell.accuracy ?? null, true); + }, + }; + }) || []; + + return [...fixedColumns, ...dynamicCellColumns]; + }, [currentCells]); + const assignTask = useContext(AssignTaskContext) as IAssignTask | null; const isSummary = assignTask?.metadata.nodeType == NodeType.Summary; @@ -576,7 +620,6 @@ const HotTableView: React.FC<{ for (const key of ruleKeys) { if (Array.isArray(acc)) { - debugger; const field = fieldMap[key]; acc = acc.map((item: any) => { if (info.valueType !== '分类型') { @@ -904,15 +947,37 @@ const HotTableView: React.FC<{ } }; - function afterSelection(row: number, col: number, _row2: number, _col2: number) { - const key = excelCellRef({ row: row, col: col }); - const cell = cells[key]; - if (cell) { - setCurrentCell(cell); - } else { - setCurrentCell(null); + const afterSelection = ( + startRow: number, + startCol: number, + endRow: number, + endCol: number, + ) => { + // 更新选中范围状态 + setSelectionRange({ startRow, startCol, endRow, endCol }); + + //遍历选中范围,收集所有单元格数据 + const currentCellsList: XCells[] = []; + // 双重循环:覆盖从 start 到 end 的所有行列 + const minRow = Math.min(startRow, endRow); + const maxRow = Math.max(startRow, endRow); + const minCol = Math.min(startCol, endCol); + const maxCol = Math.max(startCol, endCol); + + for (let r = minRow; r <= maxRow; r++) { + for (let c = minCol; c <= maxCol; c++) { + const key = excelCellRef({ row: r, col: c }); // 生成单元格唯一标识 + const cell = cells[key]; // 从 cells 中获取单元格数据 + if (cell) { + // 避免 cells 中无该单元格的情况 + currentCellsList.push(cell); + } + } } - } + + //更新选中单元格列表状态 + setCurrentCells(currentCellsList); + }; const afterChange = (changes: CellChange[] | null, source: ChangeSource) => { if (!ready) { @@ -964,18 +1029,29 @@ const HotTableView: React.FC<{ }; const flatTree = ( node: model.SummaryAssignTaskView, - acc: { name: string; nodeTypeName: string; value: any }[] = [], + acc: { + name: string; + nodeTypeName: string; + values: Partial> | undefined; + }[] = [], ) => { - acc.push({ name: node.name, nodeTypeName: node.nodeTypeName, value: node.value }); + acc.push({ + name: node.name, + nodeTypeName: node.nodeTypeName, + values: node.values || {}, + }); node.children?.forEach((c: any) => flatTree(c, acc)); return acc; }; /** 属性值汇总穿透 */ async function propSummaryDrill() { - if (!currentCell) return message.warning('请先选择有属性的单元格'); - if (!currentCell.rule.isSummary) return message.warning('当前属性不可汇总'); - if (currentCell.valueType !== '数字框') return message.warning('请选择数值型的属性'); + if (!currentCells) return message.warning('请先选择有属性的单元格'); + currentCells.forEach((currentCell: XCells) => { + if (!currentCell.rule.isSummary) return message.warning('当前属性不可汇总'); + if (currentCell.valueType !== '数字框') + return message.warning('请选择数值型的属性'); + }); const loadingKey = 'summaryLoading'; const hideLoading = () => message.destroy(loadingKey); @@ -997,75 +1073,120 @@ const HotTableView: React.FC<{ ), }); + + //从叶子节点提取所有单元格的有效值(处理空值和非数字情况) + const getNodeCellValues = ( + node: model.SummaryAssignTaskView, + ): Record => { + if ( + (!node.children || node.children.length === 0) && + node.nodeTypeName !== '汇总表' + ) { + const cellValues = node.values || {}; + + // 过滤并转换为有效数字(非数字/空值视为0) + return Object.entries(cellValues).reduce((acc, [cellRef, value]) => { + acc[cellRef] = typeof value === 'number' ? value : 0; + return acc; + }, {} as Record); + } + + // 汇总表或非叶子节点返回空对象 + return {}; + }; + + // 递归汇总所有叶子节点的单元格值(按cellRef分别累加) + const calcCellLeafSum = ( + node: model.SummaryAssignTaskView, + ): Record => { + // 叶子节点直接返回其单元格值 + if (!node.children || node.children.length === 0) { + return getNodeCellValues(node); + } + + // 递归处理子节点,合并所有单元格的汇总结果 + return node.children.reduce((total, child) => { + const childSum = calcCellLeafSum(child); + + // 按cellRef累加每个单元格的总和 + Object.entries(childSum).forEach(([cellRef, value]) => { + total[cellRef] = (total[cellRef] || 0) + value; + }); + + return total; + }, {} as Record); + }; + + // 汇总数据 let summaryData: model.SummaryAssignTaskView; try { - summaryData = await assignTask!.propertySummaryDrill( - currentCell, + summaryData = await assignTask!.propertySummaryDrills( + currentCells, props.sheet, props.form, ); + + // 计算多单元格汇总结果,存入values属性 + summaryData.values = calcCellLeafSum(summaryData); } catch (e) { console.error(e); hideLoading(); return message.error(e instanceof Error ? e.message : String(e)); } hideLoading(); - const calcUnitLeafSum = (node: model.SummaryAssignTaskView): number => { - if (!node.children || node.children.length === 0) { - return node.nodeTypeName === '汇总表' - ? 0 - : typeof node.value === 'number' - ? node.value - : 0; - } - return node.children.reduce((sum, child) => sum + calcUnitLeafSum(child), 0); - }; + const exportSummary = () => { + try { + if (!summaryData) { + message.warning('没有数据可以导出'); + return; + } - summaryData.value = calcUnitLeafSum(summaryData); - const summaryColumns = [ - { - title: '节点名称', - dataIndex: 'name', - key: 'name', - }, - { - title: '节点类型', - dataIndex: 'nodeTypeName', - key: 'nodeTypeName', - width: 120, - render: (v: string) => {v}, - }, - { - title: excelCellRef({ row: currentCell.row, col: currentCell.col }), - dataIndex: 'value', - key: 'value', - width: 200, - align: 'right' as const, - render: (v: any) => - v == null ? '' : formatNumber(v, (currentCell as any)?.accuracy ?? null, true), - }, - ]; + // 处理当前选中的单元格 + const cells = Array.isArray(currentCells) ? currentCells : [currentCells]; + if (cells.length === 0) { + message.warning('请选择要导出的单元格'); + return; + } - const exportSummary = () => { - const rows = flatTree(summaryData); - const aoa = [ - [ + //生成表头 + const headers = [ '节点名称', '节点类型', - excelCellRef({ row: currentCell.row, col: currentCell.col }), - ], - ...rows.map((r) => [ - r.name, - r.nodeTypeName, - r.value == null - ? '' - : formatNumber(r.value, (currentCell as any)?.accuracy ?? null, true), - ]), - ]; - const ws = XLSX.utils.aoa_to_sheet(aoa); - const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, '汇总明细'); - XLSX.writeFile(wb, '属性值汇总穿透.xlsx'); + ...cells.map((cell) => excelCellRef({ row: cell.row, col: cell.col })), + ]; + + // 扁平化树形数据 + const rows = flatTree(summaryData); + + //生成导出数据 + const aoa = [ + headers, + ...rows.map((row) => { + const baseColumns = [row.name, row.nodeTypeName]; + + // 动态列:每个单元格对应的值 + const cellColumns = cells.map((cell) => { + const cellRef = excelCellRef({ row: cell.row, col: cell.col }); + const cellValue = row.values?.[cellRef]; // 从values中取对应cellRef的值 + + // 格式化数值(使用当前单元格的精度配置) + return cellValue == null + ? '' + : formatNumber(cellValue, cell.accuracy ?? null, true); + }); + debugger; + return [...baseColumns, ...cellColumns]; + }), + ]; + + // 5. 生成Excel并下载 + const ws = XLSX.utils.aoa_to_sheet(aoa); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '汇总明细'); + XLSX.writeFile(wb, '属性值汇总穿透.xlsx'); + } catch (e) { + message.error(e instanceof Error ? e.message : String(e)); + } }; Modal.info({ diff --git a/src/ts/base/model.ts b/src/ts/base/model.ts index cfff4539e..ad6c021ac 100644 --- a/src/ts/base/model.ts +++ b/src/ts/base/model.ts @@ -27,6 +27,7 @@ import { } from './schema'; import { IIdentity } from '../core'; import { RelationType } from './enum'; +import { excelCellRef } from '@/components/DataStandard/NewReportForm/Utils/index'; // 请求类型定义 export type ReqestType = { // 模块 @@ -2459,7 +2460,16 @@ export interface AssignTaskView extends XAssignTask { isLeaf: boolean; directionChildrenComplete?: boolean; } -export interface SummaryAssignTaskView extends XAssignTask { + +type CellCoordsFromXCells = { + [K in keyof T]: T[K] extends XCells ? ReturnType : never; +}[number]; + +type DynamicCellValues = { + values?: Partial, number | null>>; +}; + +export interface SummaryAssignTaskView extends XAssignTask, DynamicCellValues { // 子节点 children: SummaryAssignTaskView[]; value?: number | null; diff --git a/src/ts/core/work/assign/assign.ts b/src/ts/core/work/assign/assign.ts index d36acd988..9015862f5 100644 --- a/src/ts/core/work/assign/assign.ts +++ b/src/ts/core/work/assign/assign.ts @@ -83,9 +83,9 @@ export interface IAssignTask extends IFileInfo { draft(id: string): Promise | undefined; /** 取消接收 */ cancelReceive(): Promise | undefined; - /** 属性值穿透 */ - propertySummaryDrill( - cell: XCells, + /** 多单元格属性值穿透 */ + propertySummaryDrills( + cells: XCells[], sheet: XSheet, form: XReport, ): Promise; @@ -635,8 +635,8 @@ export class AssignTask extends FileInfo implements IAssignT return this.distAssignTaskTree; } - async propertySummaryDrill( - cell: XCells, + async propertySummaryDrills( + cells: XCells[], sheet: XSheet, form: XReport, ): Promise { @@ -650,10 +650,14 @@ export class AssignTask extends FileInfo implements IAssignT } this.currentTree = distTree; } + + // 预计算所有单元格坐标 + const cellRefs = cells.map((cell) => excelCellRef({ row: cell.row, col: cell.col })); + let result: SummaryAssignTaskView = { ...this.metadata, children: [], - value: 0, + values: {}, // 初始化 values 对象 }; const loadAllChildren = async (nodeId: string): Promise => { @@ -683,23 +687,80 @@ export class AssignTask extends FileInfo implements IAssignT return map; }, {}); - const cellRef = excelCellRef({ row: cell.row, col: cell.col }); + const BATCH_SIZE = 5; // 每批处理的数量 + const MAX_RETRIES = 3; + const RETRY_DELAY = 1000; const childrenWithValues: SummaryAssignTaskView[] = []; - for (const child of children) { - const childData = detailMap[child.id] || {}; - const value = childData['R' + sheet.attributeId]?.[cellRef] ?? ''; + const errors: any[] = []; + //重试机制 + const retryOperation = async (operation: any, retries = MAX_RETRIES) => { + for (let i = 0; i < retries; i++) { + try { + return await operation(); + } catch (error) { + if (i === retries - 1) throw error; + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); + } + } + }; - let subChildren: SummaryAssignTaskView[] = []; - if (child.nodeTypeName === '汇总表') { - subChildren = await loadAllChildren(child.id); + // 处理子节点 + const processChild = async (child: XAssignTask) => { + try { + const childData = detailMap[child.id] || {}; + const values: Record = {}; + + for (const cellRef of cellRefs) { + const cellValue = childData['R' + sheet.attributeId]?.[cellRef] ?? null; + let numericValue: number | null = null; + + if (cellValue !== null && cellValue !== undefined && cellValue !== '') { + numericValue = + typeof cellValue === 'number' + ? cellValue + : parseFloat(cellValue as string); + + if (isNaN(numericValue)) { + numericValue = null; + } + } + + values[cellRef] = numericValue; + } + + let subChildren: SummaryAssignTaskView[] = []; + if (child.nodeTypeName === '汇总表') { + subChildren = await retryOperation(() => loadAllChildren(child.id)); + } + + return { + ...child, + children: subChildren, + values, + }; + } catch (error: any) { + errors.push({ + childId: child.id, + error: error.message, + }); + return null; } + }; - childrenWithValues.push({ - ...child, - children: subChildren, - value: value, - }); + // 批量处理 + for (let i = 0; i < children.length; i += BATCH_SIZE) { + const batchChildren = children.slice(i, i + BATCH_SIZE); + const batchResults = await Promise.all( + batchChildren.map((child) => processChild(child)), + ); + + const successfulResults = batchResults.filter((result) => result !== null); + childrenWithValues.push(...successfulResults); + } + + if (errors.length > 0) { + console.warn(`处理完成,但有 ${errors.length} 个错误`, errors); } return childrenWithValues; @@ -707,13 +768,6 @@ export class AssignTask extends FileInfo implements IAssignT result.children = await loadAllChildren(this.id); - const sumValues = (nodes: SummaryAssignTaskView[]): number => { - return nodes.reduce( - (sum, node) => sum + (node.value || 0) + sumValues(node.children), - 0, - ); - }; - result.value = sumValues(result.children); return result; } -- Gitee