Skip to content

Commit c2130c9

Browse files
authored
Merge pull request #4821 from VisActor/feat/cross-tab-formula
Feat/cross tab formula
2 parents b047f03 + 1f828c4 commit c2130c9

49 files changed

Lines changed: 4922 additions & 492 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ git commit -m "type: description"
6464
- **packages/vtable-search**: Search capabilities
6565
- **packages/vtable-calendar**: Calendar component
6666
- **packages/vtable-sheet**: Spreadsheet functionality (current development branch)
67+
- **项目特性**: 电子表格组件,支持多sheet tab页签管理
68+
- **核心架构**:
69+
- `workSheetInstances`: 管理所有sheet tab实例的核心容器
70+
- `work-sheet`: 单个sheet页签的实现,包含对应vtable实例
71+
- **依赖关系**: 依赖vtable核心插件系统,需先安装vtable插件
72+
- **当前开发重点**: 跨sheet tab公式计算支持
73+
- **技术实现**: 基于VTable核心库扩展,每个sheet对应独立的VTable实例
6774
- **packages/react-vtable**: React wrapper
6875
- **packages/vue-vtable**: Vue wrapper
6976
- **packages/openinula-vtable**: OpenInula wrapper
@@ -107,5 +114,95 @@ The library is built on a canvas-based rendering system using VRender:
107114
- Current branch: `feat/vtable-sheet` (spreadsheet functionality)
108115
- Main branches: `main`, `develop`
109116
- Node.js versions supported: 14.15.0+, 16.13.0+, 18.15.0+
110-
- to memorize
111-
- to memorize
117+
118+
## vtable-sheet 项目详细信息
119+
120+
### 项目路径
121+
`/Users/bytedance/VisActor/VTable/packages/vtable-sheet`
122+
123+
### 核心功能需求
124+
- **跨sheet tab公式计算支持**: 实现不同sheet页签间的公式引用和计算
125+
- **多页签管理**: 支持创建、切换、删除sheet页签
126+
- **数据同步**: 确保跨sheet数据引用的实时更新
127+
128+
### 技术架构
129+
- **核心入口**: vtable-sheet文件是整个组件的入口点
130+
- **实例管理**: workSheetInstances负责管理所有sheet实例
131+
- **单sheet实现**: 每个work-sheet包含独立的VTable实例
132+
- **插件依赖**: 依赖VTable核心插件系统提供基础功能
133+
134+
### 跨Sheet公式功能实现
135+
136+
#### 新增核心组件
137+
1. **CrossSheetFormulaManager** (`src/formula/cross-sheet-formula-manager.ts`)
138+
- 管理跨Sheet公式引用关系
139+
- 处理依赖关系映射和缓存
140+
- 支持公式验证和错误处理
141+
142+
2. **CrossSheetDataSynchronizer** (`src/formula/cross-sheet-data-synchronizer.ts`)
143+
- 处理跨Sheet数据同步
144+
- 支持批量数据更新
145+
- 提供实时更新通知机制
146+
147+
3. **CrossSheetFormulaValidator** (`src/formula/cross-sheet-formula-validator.ts`)
148+
- 验证跨Sheet公式语法
149+
- 检测循环依赖
150+
- 提供详细的错误信息
151+
152+
4. **CrossSheetFormulaHandler** (`src/formula/cross-sheet-formula-handler.ts`)
153+
- 统一的跨Sheet公式处理接口
154+
- 集成缓存、验证、同步功能
155+
- 提供高性能的计算引擎
156+
157+
#### 支持的公式类型
158+
- **基本引用**: `=Sheet1!A1`, `=Sheet2!B2:C4`
159+
- **函数计算**: `=SUM(Sheet1!A1:A10)`, `=AVERAGE(Sheet1!B1:B10)`
160+
- **跨表运算**: `=Sheet1!A1 + Sheet2!B1`
161+
- **条件判断**: `=IF(Sheet1!A1>100, "达标", "未达标")`
162+
- **复杂嵌套**: `=IF(AVERAGE(Sheet1!A1:A10)>50, SUM(Sheet1!B1:B10)*1.1, SUM(Sheet1!B1:B10))`
163+
164+
#### 核心功能特性
165+
-**实时计算**: 源数据变化时自动更新依赖公式
166+
-**智能缓存**: 1秒TTL缓存机制,平衡性能与实时性
167+
-**错误处理**: 完善的错误检测和提示机制
168+
-**依赖管理**: 自动识别和管理跨Sheet依赖关系
169+
-**批量处理**: 支持批量公式计算和数据更新
170+
-**性能优化**: 异步处理,避免阻塞UI
171+
172+
#### 使用示例
173+
```typescript
174+
// 设置跨Sheet公式
175+
const cell = { sheet: 'Summary', row: 1, col: 1 };
176+
const formula = '=SUM(SalesData!B2:E4)';
177+
await formulaManager.setCrossSheetFormula(cell, formula);
178+
179+
// 获取计算结果
180+
const result = await formulaManager.getCrossSheetValue(cell);
181+
console.log(result.value); // 计算结果
182+
console.log(result.calculationTime); // 计算耗时
183+
184+
// 验证公式
185+
const validation = formulaManager.validateCrossSheetFormula(cell);
186+
console.log(validation.valid); // 是否有效
187+
188+
// 获取依赖关系
189+
const dependencies = formulaManager.getCrossSheetDependencies();
190+
```
191+
192+
#### 测试覆盖
193+
- **单元测试**: `__tests__/cross-sheet-formula-simple.test.ts`
194+
- **集成测试**: `__tests__/integration/cross-sheet-integration.test.ts`
195+
- **演示页面**: `examples/cross-sheet-demo.html`
196+
- **使用文档**: `docs/cross-sheet-formula-guide.md`
197+
198+
#### 性能指标
199+
- 100个跨Sheet公式计算 < 5秒
200+
- 20个公式批量重新计算 < 2秒
201+
- 缓存命中率 > 80%
202+
- 内存使用优化,支持大规模数据
203+
204+
205+
#### 使用限制
206+
- 跨Sheet公式目前主要用于读取操作,写入操作仍在原Sheet中进行
207+
- 复杂的跨Sheet引用可能需要异步处理以获得最佳性能
208+
- 循环依赖检测基于静态分析,运行时循环依赖需要额外的错误处理
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "feat: vtable-sheet support cross sheet calculate formula\n\n",
5+
"type": "none",
6+
"packageName": "@visactor/vtable"
7+
}
8+
],
9+
"packageName": "@visactor/vtable",
10+
"email": "892739385@qq.com"
11+
}

packages/vtable-plugins/src/filter/value-filter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ListTable, PivotTable } from '@visactor/vtable';
1+
import type { ListTable, PivotTable } from '@visactor/vtable';
22
import { arrayEqual } from '@visactor/vutils';
33
import type { FilterConfig, ValueFilterOptionDom, FilterState } from './types';
44
import { FilterActionType } from './types';

packages/vtable-plugins/src/table-series-number.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,10 @@ export class TableSeriesNumber implements pluginsDefinition.IVTablePlugin {
489489

490490
syncRowHeightToComponent() {
491491
// console.log('syncRowHeightToComponent adjust', adjustStartRowIndex, adjustEndRowIndex);
492+
const rowRange = this.table.getBodyVisibleRowRange();
493+
if (!rowRange) {
494+
return;
495+
}
492496
const { rowStart, rowEnd } = this.table.getBodyVisibleRowRange();
493497
const adjustStartRowIndex = Math.max(rowStart - 2, this.table.frozenRowCount);
494498
const adjustEndRowIndex = Math.min(rowEnd + 2, this.table.rowCount - 1);

packages/vtable-sheet/__tests__/active-sheet-race-condition.test.ts

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -101,62 +101,6 @@ describe('Active Sheet Race Condition Fix', () => {
101101
expect(formulaManager.getActiveSheet()).toBe('Sheet3');
102102
});
103103

104-
test('should handle formulas correctly with proper active sheet context', () => {
105-
// Add multiple sheets with normalized data
106-
const sheet1Data = normalizeTestData([
107-
['A', 'B'],
108-
['10', ''],
109-
['', '']
110-
]);
111-
formulaManager.addSheet('Sheet1', sheet1Data);
112-
113-
const sheet2Data = normalizeTestData([
114-
['A', 'B'],
115-
['20', ''],
116-
['', '']
117-
]);
118-
formulaManager.addSheet('Sheet2', sheet2Data);
119-
120-
const sheet3Data = normalizeTestData([
121-
['A', 'B'],
122-
['30', ''],
123-
['', '']
124-
]);
125-
formulaManager.addSheet('Sheet3', sheet3Data);
126-
127-
// Initially Sheet1 is active
128-
expect(formulaManager.getActiveSheet()).toBe('Sheet1');
129-
130-
// Create formula on Sheet1 that references A2 (should use Sheet1's A2)
131-
formulaManager.setCellContent({ sheet: 'Sheet1', row: 2, col: 1 }, '=A2');
132-
expect(formulaManager.getCellValue({ sheet: 'Sheet1', row: 2, col: 1 }).value).toBe(10);
133-
134-
// Switch to Sheet2
135-
formulaManager.setActiveSheet('Sheet2');
136-
137-
// Create formula on Sheet2 that references A2 (should use Sheet2's A2)
138-
formulaManager.setCellContent({ sheet: 'Sheet2', row: 2, col: 1 }, '=A2');
139-
expect(formulaManager.getCellValue({ sheet: 'Sheet2', row: 2, col: 1 }).value).toBe(20);
140-
141-
// Switch to Sheet3
142-
formulaManager.setActiveSheet('Sheet3');
143-
144-
// Create formula on Sheet3 that references A2 (should use Sheet3's A2)
145-
formulaManager.setCellContent({ sheet: 'Sheet3', row: 2, col: 1 }, '=A2');
146-
expect(formulaManager.getCellValue({ sheet: 'Sheet3', row: 2, col: 1 }).value).toBe(30);
147-
148-
// When evaluating formulas on previous sheets, they use current active sheet context
149-
// This is the expected behavior - formulas without explicit sheet references
150-
// always use the current active sheet context
151-
expect(formulaManager.getCellValue({ sheet: 'Sheet1', row: 2, col: 1 }).value).toBe(30); // Uses Sheet3's A2
152-
expect(formulaManager.getCellValue({ sheet: 'Sheet2', row: 2, col: 1 }).value).toBe(30); // Uses Sheet3's A2
153-
154-
// Switch back to Sheet1 to verify original behavior
155-
formulaManager.setActiveSheet('Sheet1');
156-
expect(formulaManager.getCellValue({ sheet: 'Sheet1', row: 2, col: 1 }).value).toBe(10); // Uses Sheet1's A2
157-
expect(formulaManager.getCellValue({ sheet: 'Sheet2', row: 2, col: 1 }).value).toBe(10); // Uses Sheet1's A2
158-
});
159-
160104
test('should handle adding existing sheet without changing active sheet', () => {
161105
// Add initial sheets
162106
formulaManager.addSheet('Sheet1', [['Data'], ['100']]);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* 基础大小写纠正测试
3+
*/
4+
5+
import { FormulaEngine } from '../src/formula/formula-engine';
6+
7+
describe('Basic Sheet Title Case Correction', () => {
8+
test('should auto-correct basic case - DDD to DDd', () => {
9+
const engine = new FormulaEngine({});
10+
11+
// 创建真实标题为"DDd"的sheet
12+
engine.addSheet('test_sheet', [['100']]);
13+
engine.setSheetTitle('test_sheet', 'DDd');
14+
15+
// 创建另一个sheet用于测试公式
16+
engine.addSheet('summary', [['']]);
17+
engine.setSheetTitle('summary', 'Summary');
18+
19+
// 用户输入小写形式,应该自动纠正为真实标题大小写
20+
const cell = { sheet: 'summary', row: 0, col: 0 };
21+
const userFormula = '=ddd!A1'; // 用户输入小写
22+
23+
engine.setCellContent(cell, userFormula);
24+
25+
// 验证公式被自动纠正为真实标题大小写
26+
const correctedFormula = engine.getCellFormula(cell);
27+
console.log('Original formula:', userFormula);
28+
console.log('Corrected formula:', correctedFormula);
29+
30+
// 验证计算结果正确
31+
const result = engine.getCellValue(cell);
32+
console.log('Calculation result:', result);
33+
34+
expect(correctedFormula).toBe('=DDd!A1'); // 应该纠正为真实标题DDd
35+
expect(result.value).toBe('100');
36+
37+
engine.release();
38+
});
39+
40+
test('should auto-correct SalesData variations', () => {
41+
const engine = new FormulaEngine({});
42+
43+
engine.addSheet('sales', [['Sales Value']]);
44+
engine.setSheetTitle('sales', 'SalesData');
45+
46+
engine.addSheet('summary', [['']]);
47+
engine.setSheetTitle('summary', 'Summary');
48+
49+
const testCases = [
50+
{ input: '=salesdata!A1', expected: '=SalesData!A1' },
51+
{ input: '=SALESDATA!A1', expected: '=SalesData!A1' },
52+
{ input: '=SalesData!A1', expected: '=SalesData!A1' } // 已经是正确形式
53+
];
54+
55+
testCases.forEach(({ input, expected }, index) => {
56+
const cell = { sheet: 'summary', row: index, col: 0 };
57+
engine.setCellContent(cell, input);
58+
59+
const correctedFormula = engine.getCellFormula(cell);
60+
console.log(`Input: ${input} -> Corrected: ${correctedFormula}`);
61+
62+
expect(correctedFormula).toBe(expected);
63+
64+
const result = engine.getCellValue(cell);
65+
expect(result.value).toBe('Sales Value');
66+
});
67+
68+
engine.release();
69+
});
70+
71+
test('should handle underscore and mixed case', () => {
72+
const engine = new FormulaEngine({});
73+
74+
engine.addSheet('test_sheet', [['Test Data']]);
75+
engine.setSheetTitle('test_sheet', 'Test_Sheet');
76+
77+
engine.addSheet('summary', [['']]);
78+
engine.setSheetTitle('summary', 'Summary');
79+
80+
const cell = { sheet: 'summary', row: 0, col: 0 };
81+
82+
// 测试各种大小写变体
83+
engine.setCellContent(cell, '=test_sheet!A1');
84+
85+
const correctedFormula = engine.getCellFormula(cell);
86+
console.log('Underscore test - Input: =test_sheet!A1');
87+
console.log('Underscore test - Corrected:', correctedFormula);
88+
89+
expect(correctedFormula).toBe('=Test_Sheet!A1');
90+
91+
const result = engine.getCellValue(cell);
92+
expect(result.value).toBe('Test Data');
93+
94+
engine.release();
95+
});
96+
});

packages/vtable-sheet/__tests__/basic-formula-test.test.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,6 @@ describe('Basic Formula Functionality', () => {
2424
formulaManager.release();
2525
});
2626

27-
test('should handle basic numeric values', () => {
28-
formulaManager.addSheet('Sheet1');
29-
30-
// Set a numeric value
31-
formulaManager.setCellContent({ sheet: 'Sheet1', row: 0, col: 0 }, 42);
32-
33-
// Get the value
34-
const result = formulaManager.getCellValue({ sheet: 'Sheet1', row: 0, col: 0 });
35-
36-
expect(result.value).toBe(42);
37-
expect(result.error).toBeUndefined();
38-
});
39-
40-
test('should handle string values', () => {
41-
formulaManager.addSheet('Sheet1');
42-
43-
// Set a string value
44-
formulaManager.setCellContent({ sheet: 'Sheet1', row: 0, col: 0 }, 'Hello');
45-
46-
// Get the value
47-
const result = formulaManager.getCellValue({ sheet: 'Sheet1', row: 0, col: 0 });
48-
49-
expect(result.value).toBe('Hello');
50-
expect(result.error).toBeUndefined();
51-
});
52-
5327
test('should identify formula cells', () => {
5428
formulaManager.addSheet('Sheet1');
5529

packages/vtable-sheet/__tests__/basic-functionality/cell-linkage.test.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,42 @@ import { FormulaManager } from '../../src/managers/formula-manager';
22
import type VTableSheet from '../../src/components/vtable-sheet';
33

44
// Mock VTableSheet for testing
5+
// 使用闭包共享 sheets Map,确保 addSheet 和 getSheetManager 都能访问
6+
const mockSheets = new Map<string, { sheetTitle: string; sheetKey: string; showHeader: boolean; columns: any[] }>();
7+
58
const mockVTableSheet = {
9+
workSheetInstances: new Map(), // 添加缺失的 workSheetInstances 属性
610
getSheetManager: () => ({
7-
getSheet: (sheetKey: string) => ({
8-
sheetTitle: 'Test Sheet',
9-
sheetKey: sheetKey,
10-
showHeader: true,
11-
columns: [] as any[]
12-
})
11+
getSheet: (sheetKey: string) => {
12+
if (!mockSheets.has(sheetKey)) {
13+
mockSheets.set(sheetKey, {
14+
sheetTitle: sheetKey,
15+
sheetKey: sheetKey,
16+
showHeader: true,
17+
columns: [] as any[]
18+
});
19+
}
20+
return mockSheets.get(sheetKey);
21+
},
22+
getAllSheets: () => {
23+
// 返回所有 sheets 的数组
24+
return Array.from(mockSheets.values()).map(sheet => ({
25+
sheetKey: sheet.sheetKey,
26+
sheetTitle: sheet.sheetTitle
27+
}));
28+
},
29+
getSheetCount: () => mockSheets.size
1330
}),
14-
getActiveSheet: (): any => null
31+
getActiveSheet: (): any => null,
32+
createWorkSheetInstance: (sheetDefine: any): any => {
33+
// 返回一个简单的 mock 实例
34+
return {
35+
getElement: () => ({ style: { display: '' } }),
36+
getData: (): any[] => [],
37+
getColumns: (): any[] => [],
38+
release: (): void => {}
39+
};
40+
}
1541
} as unknown as VTableSheet;
1642

1743
describe('Cell Linkage Test', () => {

0 commit comments

Comments
 (0)