diff --git a/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts b/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts index 755dacaeee..23ceb87e57 100644 --- a/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts +++ b/packages/vtable-sheet/__tests__/multi-header-record-data.test.ts @@ -61,3 +61,49 @@ test('VTableSheet keeps record fields when top-level columns mix leaf and groupe removeDom(container); } }); + +test('VTableSheet normalizes object record data with nested field paths for formulas', () => { + const container = createDiv() as HTMLDivElement; + container.style.position = 'relative'; + container.style.width = '1000px'; + container.style.height = '800px'; + + const sheet = new VTableSheet(container, { + showFormulaBar: false, + showSheetTab: false, + defaultRowHeight: 25, + defaultColWidth: 100, + sheets: [ + { + sheetKey: 'nestedFieldSheet', + sheetTitle: '嵌套字段示例', + active: true, + columns: [ + { title: '名称', field: 'name', width: 120 }, + { title: '营业额', field: ['sales', 'amount'], width: 120 } + ], + data: [ + { + name: '商品A', + sales: { + amount: '125' + } + } + ] + } + ] + }); + + try { + const value = sheet.formulaManager.formulaEngine.getCellValue({ + sheet: 'nestedFieldSheet', + row: 1, + col: 1 + }); + + expect(value).toEqual({ value: 125, error: undefined }); + } finally { + sheet.release(); + removeDom(container); + } +}); diff --git a/packages/vtable-sheet/examples/menu.ts b/packages/vtable-sheet/examples/menu.ts index dfaba92593..2265304622 100644 --- a/packages/vtable-sheet/examples/menu.ts +++ b/packages/vtable-sheet/examples/menu.ts @@ -26,5 +26,9 @@ export const menus = [ { path: 'sheet', name: 'history' + }, + { + path: 'sheet', + name: 'issue-5204-formula-manager' } ]; diff --git a/packages/vtable-sheet/examples/sheet/issue-5204-formula-manager.ts b/packages/vtable-sheet/examples/sheet/issue-5204-formula-manager.ts new file mode 100644 index 0000000000..57a3b0afce --- /dev/null +++ b/packages/vtable-sheet/examples/sheet/issue-5204-formula-manager.ts @@ -0,0 +1,66 @@ +import { VTableSheet } from '../../src/index'; +import type { IColumnDefine, SheetData } from '../../src/ts-types'; + +const CONTAINER_ID = 'vTable'; + +const columns: IColumnDefine[] = [ + { title: '测试公式计算', field: 'test', width: 120 }, + { + title: 'B商品营业额', + field: ['sales', 'Bmoney'], + width: 120 + }, + { + title: 'A商品营业额', + field: 'Amoney', + width: 120 + } +]; + +const dataA: SheetData = Array.from({ length: 12 }, (_, index) => ({ + test: `${index}-row`, + sales: { + Bmoney: `${(index + 1) * 50}` + }, + Amoney: `${(index + 2) * 50}` +})); + +const columnsB = [ + { title: '测试公式计算', width: 120 }, + { + title: 'B商品营业额', + width: 120 + }, + { + title: 'A商品营业额', + width: 120 + } +]; + +const dataB = [['1', 2, 3]]; + +export function createTable() { + const container = document.getElementById(CONTAINER_ID)!; + window.sheetInstance = new VTableSheet(container, { + showFormulaBar: false, + showSheetTab: true, + defaultRowHeight: 32, + defaultColWidth: 120, + sheets: [ + { + sheetKey: 'Issue-5204-a', + sheetTitle: 'data是Array', + columns, + data: dataA, + active: true + }, + { + sheetKey: 'Issue-5204-b', + sheetTitle: 'data是Array>', + columns: columnsB, + data: dataB, + active: true + } + ] + }); +} diff --git a/packages/vtable-sheet/src/components/vtable-sheet.ts b/packages/vtable-sheet/src/components/vtable-sheet.ts index 1b312a085f..4122db8b26 100644 --- a/packages/vtable-sheet/src/components/vtable-sheet.ts +++ b/packages/vtable-sheet/src/components/vtable-sheet.ts @@ -10,7 +10,8 @@ import type { IVTableSheetUpdateOptions, ISheetDefine, IWorkSheetOptions, - IColumnDefine + IColumnDefine, + SheetData } from '../ts-types'; import type { MultiSheetImportResult } from '@visactor/vtable-plugins/src/excel-import/types'; import type { TableEventHandlersEventArgumentMap } from '@visactor/vtable/es/ts-types/events'; @@ -1356,7 +1357,7 @@ export default class VTableSheet { * @param sheetKey sheet的key * @returns 数据 */ - exportData(sheetKey: string): any[][] { + exportData(sheetKey: string): SheetData { const sheet = this.workSheetInstances.get(sheetKey); if (!sheet) { return []; @@ -1368,7 +1369,7 @@ export default class VTableSheet { * 导出所有sheet的数据 * @returns 数据 */ - exportAllData(): any[][] { + exportAllData(): SheetData[] { const sheets = Array.from(this.workSheetInstances.values()); return sheets.map(sheet => sheet.getData()); } diff --git a/packages/vtable-sheet/src/core/WorkSheet.ts b/packages/vtable-sheet/src/core/WorkSheet.ts index a1b0f45fff..0473123dea 100644 --- a/packages/vtable-sheet/src/core/WorkSheet.ts +++ b/packages/vtable-sheet/src/core/WorkSheet.ts @@ -11,7 +11,7 @@ import type { IThemeDefine, IFilterConfig, IFilterState, - ISheetDefine, + SheetData, IVTableSheetUpdateOptions } from '../ts-types'; import type { TYPES, VTableSheet } from '..'; @@ -27,7 +27,7 @@ import type { IWorksheetEventSource } from '../event/event-interfaces'; */ export type WorkSheetConstructorOptions = { /** 表格数据 */ - data?: any[][]; + data?: SheetData; /** 公式计算选项 */ formula?: IFormulaManagerOptions; /** Sheet 唯一标识 */ @@ -782,7 +782,7 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { /** * 获取表格数据 */ - getData(): any[][] { + getData(): SheetData { // 从表格实例获取数据 return this.options.data || []; } @@ -806,9 +806,9 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { } } - const data = this.getData(); - if (data && data[row] && data[row][col] !== undefined) { - return data[row][col]; + const rowData = this.getData()[row]; + if (Array.isArray(rowData) && rowData[col] !== undefined) { + return rowData[col]; } return null; } @@ -848,9 +848,9 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { * @param value 新值 */ setCellValue(col: number, row: number, value: any): void { - const data = this.getData(); - if (data && data[row]) { - data[row][col] = value; + const rowData = this.getData()[row]; + if (Array.isArray(rowData)) { + rowData[col] = value; // 更新表格实例 if (this.tableInstance) { diff --git a/packages/vtable-sheet/src/managers/formula-manager.ts b/packages/vtable-sheet/src/managers/formula-manager.ts index 3846876290..d87fe9e4b0 100644 --- a/packages/vtable-sheet/src/managers/formula-manager.ts +++ b/packages/vtable-sheet/src/managers/formula-manager.ts @@ -2,7 +2,7 @@ import { FormulaEngine } from '../formula/formula-engine'; import type VTableSheet from '../components/vtable-sheet'; import type { FormulaCell, FormulaResult, IFormulaManager } from '../ts-types/formula'; import { FormulaRangeSelector } from '../formula/formula-range-selector'; -import type { CellRange, ISheetDefine } from '../ts-types'; +import type { CellRange, ISheetDefine, SheetData } from '../ts-types'; import { CellHighlightManager } from '../formula'; import type * as VTable from '@visactor/vtable'; import { CrossSheetFormulaHandler } from '../formula/cross-sheet-formula-handler'; @@ -137,19 +137,39 @@ export class FormulaManager implements IFormulaManager { * @param data 工作表数据 * @returns 标准化后的工作表数据 */ - normalizeSheetData(data: unknown[][], tableInstance: VTable.ListTable): unknown[][] { + normalizeSheetData(data: SheetData, tableInstance: VTable.ListTable): unknown[][] { try { //将columns中的title追加到data中 const headerRows: unknown[][] = []; + const headerKeyColMap = new Map(); for (let i = 0; i < tableInstance.columnHeaderLevelCount; i++) { const headerRow: unknown[] = []; for (let j = 0; j < tableInstance.colCount; j++) { const cellValue = tableInstance.getCellValue(j, i); + const filedDefine = tableInstance.getHeaderDefine(j, i); + if (cellValue === filedDefine.title) { + headerKeyColMap.set(j, filedDefine.field); + } headerRow.push(cellValue); } headerRows.push(headerRow); } - const dataCopy = JSON.parse(JSON.stringify(data)); + const colCount = tableInstance.colCount; + let dataCopy: unknown[][]; + const firstRow = data[0]; + if (firstRow && typeof firstRow === 'object' && !Array.isArray(firstRow)) { + dataCopy = data.map((_, rowIndex) => { + const bodyRow = tableInstance.columnHeaderLevelCount + rowIndex; + const rowTemp = new Array(colCount); + for (let col = 0; col < colCount; col++) { + const field = tableInstance.getBodyField(col, bodyRow) ?? headerKeyColMap.get(col); + rowTemp[col] = field !== undefined ? tableInstance.getRawFieldData(field, col, bodyRow) ?? null : null; + } + return rowTemp; + }); + } else { + dataCopy = JSON.parse(JSON.stringify(data)); + } const toNormalizeData = tableInstance.columnHeaderLevelCount > 0 ? [...headerRows].concat(dataCopy) : dataCopy; @@ -1372,7 +1392,6 @@ export class FormulaManager implements IFormulaManager { // 如果还没有对应的 WorkSheet 实例,跳过,后续按需再补充 return; } - const normalizedData = this.normalizeSheetData(worksheetInstance.getData(), worksheetInstance.tableInstance); this.addSheet(sheetKey, normalizedData, sheetDefine.sheetTitle); }); diff --git a/packages/vtable-sheet/src/ts-types/index.ts b/packages/vtable-sheet/src/ts-types/index.ts index 2ee87297f2..ef04dc4076 100644 --- a/packages/vtable-sheet/src/ts-types/index.ts +++ b/packages/vtable-sheet/src/ts-types/index.ts @@ -14,11 +14,13 @@ export interface IFilterConfig { /** 扩展的列定义,添加筛选相关配置;field 可选,构建 ListTable 时由 WorkSheet 按列索引填充 */ export interface IColumnDefine extends Omit { /** 列字段,可选;未指定时由 WorkSheet 按列索引填充 */ - field?: string | number; + field?: ColumnDefine['field']; /** 是否启用筛选功能 */ filter?: boolean; } +export type SheetData = (CellValue[] | null)[] | Record[]; + /** Sheet定义 */ export interface ISheetDefine { /** 标题 */ @@ -32,7 +34,7 @@ export interface ISheetDefine { /** 表头定义 */ columns?: IColumnDefine[]; /** 数据 */ - data?: (CellValue[] | null)[]; + data?: SheetData; /** 是否是当前活动sheet TODO 是不是放到外层更好*/ active?: boolean; cellMerge?: VTableTypes.CustomMergeCellArray; diff --git a/packages/vtable-sheet/src/ts-types/sheet.ts b/packages/vtable-sheet/src/ts-types/sheet.ts index 9fef2af7e1..d20da91437 100644 --- a/packages/vtable-sheet/src/ts-types/sheet.ts +++ b/packages/vtable-sheet/src/ts-types/sheet.ts @@ -1,4 +1,4 @@ -import type { IColumnDefine, IFilterConfig, IFilterState, ISheetDefine } from './index'; +import type { IColumnDefine, IFilterConfig, IFilterState, ISheetDefine, SheetData } from './index'; import type { CellCoord, CellRange, CellValue } from './base'; import type { ListTableConstructorOptions } from '@visactor/vtable'; @@ -13,7 +13,7 @@ export interface IWorkSheetOptions extends Omit CellRange | null; /** 获取数据 */ - getData: () => CellValue[][]; + getData: () => SheetData; /** 获取行数 */ getRowCount: () => number;