@veltra/sheet 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/dist/index.d.ts +4 -25
  2. package/dist/index.js +3 -22
  3. package/dist/tools/builtin.js +279 -8
  4. package/dist/tools/context.d.ts +54 -11
  5. package/dist/tools/context.js +28 -5
  6. package/dist/tools/download.js +68 -0
  7. package/dist/tools/export.worker.d.ts +16 -0
  8. package/dist/tools/export.worker.js +29 -0
  9. package/dist/tools/registry.d.ts +7 -4
  10. package/dist/tools/registry.js +1 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/sheet.d.ts +6 -5
  13. package/dist/vue/formula-bar.js +393 -0
  14. package/dist/vue/formula-suggest-list.js +66 -0
  15. package/dist/vue/import-file.js +140 -0
  16. package/dist/vue/insert-count-menu-item.js +68 -0
  17. package/dist/vue/insert-image.js +79 -0
  18. package/dist/vue/popup-helpers.js +73 -0
  19. package/dist/vue/popups/border-popup.js +75 -0
  20. package/dist/vue/popups/border-preset-glyph.js +106 -0
  21. package/dist/vue/popups/export-popup.js +51 -0
  22. package/dist/vue/popups/fill-color-popup.js +37 -0
  23. package/dist/vue/popups/find-popup.js +113 -0
  24. package/dist/vue/popups/font-color-popup.js +37 -0
  25. package/dist/vue/popups/font-size-popup.js +63 -0
  26. package/dist/vue/popups/import.worker.d.ts +21 -0
  27. package/dist/vue/popups/import.worker.js +41 -0
  28. package/dist/vue/popups/insert-image-popup.js +38 -0
  29. package/dist/vue/sheet-context-menu.js +200 -0
  30. package/dist/vue/sheet-tabs.js +162 -0
  31. package/dist/vue/sheet-toolbar.js +106 -0
  32. package/dist/vue/sheet.js +144 -138
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +11 -0
  35. package/dist/vue/style2.css +564 -3
  36. package/dist/vue/use-find-replace.js +161 -0
  37. package/dist/vue/use-formula-ref-select.js +37 -0
  38. package/dist/vue/use-formula-suggest.js +126 -0
  39. package/dist/vue/use-sheet-grid.js +180 -0
  40. package/dist/vue/use-sheet-state.js +122 -0
  41. package/dist/vue/use-sheet-tabs-bar.js +90 -0
  42. package/dist/vue/use-tool-groups.js +42 -0
  43. package/dist/vue/use-tool-popup.js +123 -0
  44. package/dist/vue/use-toolbar-scroll.js +72 -0
  45. package/package.json +12 -6
  46. package/src/env.d.ts +14 -0
  47. package/src/index.ts +203 -13
  48. package/src/tools/__test__/builtin.test.ts +208 -66
  49. package/src/tools/__test__/context.test.ts +62 -2
  50. package/src/tools/builtin.ts +350 -6
  51. package/src/tools/context.ts +89 -11
  52. package/src/tools/download.ts +82 -0
  53. package/src/tools/export.worker.ts +61 -0
  54. package/src/tools/registry.ts +16 -1
  55. package/src/types/index.ts +6 -0
  56. package/src/types/sheet.ts +5 -3
  57. package/src/vue/__test__/formula-bar.test.ts +455 -0
  58. package/src/vue/__test__/import-file.test.ts +183 -0
  59. package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
  60. package/src/vue/__test__/insert-image-popup.test.ts +89 -0
  61. package/src/vue/__test__/insert-image.test.ts +100 -0
  62. package/src/vue/__test__/sheet-component.test.ts +716 -27
  63. package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
  64. package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
  65. package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
  66. package/src/vue/formula-bar.vue +473 -0
  67. package/src/vue/formula-suggest-list.vue +64 -0
  68. package/src/vue/import-file.ts +173 -0
  69. package/src/vue/insert-count-menu-item.vue +69 -0
  70. package/src/vue/insert-image.ts +90 -0
  71. package/src/vue/parsing.ts +11 -0
  72. package/src/vue/popup-helpers.ts +50 -0
  73. package/src/vue/popups/border-popup.vue +93 -0
  74. package/src/vue/popups/border-preset-glyph.vue +111 -0
  75. package/src/vue/popups/export-popup.vue +52 -0
  76. package/src/vue/popups/fill-color-popup.vue +39 -0
  77. package/src/vue/popups/find-popup.vue +111 -0
  78. package/src/vue/popups/font-color-popup.vue +39 -0
  79. package/src/vue/popups/font-size-popup.vue +52 -0
  80. package/src/vue/popups/import.worker.ts +66 -0
  81. package/src/vue/popups/insert-image-popup.vue +39 -0
  82. package/src/vue/sheet-context-menu.ts +229 -0
  83. package/src/vue/sheet-tabs.vue +162 -0
  84. package/src/vue/sheet-toolbar.vue +91 -0
  85. package/src/vue/sheet.vue +163 -148
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +12 -0
  88. package/src/vue/use-find-replace.ts +200 -0
  89. package/src/vue/use-formula-ref-select.ts +34 -0
  90. package/src/vue/use-formula-suggest.ts +150 -0
  91. package/src/vue/use-sheet-grid.ts +243 -0
  92. package/src/vue/use-sheet-state.ts +161 -0
  93. package/src/vue/use-sheet-tabs-bar.ts +115 -0
  94. package/src/vue/use-tool-groups.ts +61 -0
  95. package/src/vue/use-tool-popup.ts +150 -0
  96. package/src/vue/use-toolbar-scroll.ts +95 -0
  97. package/dist/core/address.d.ts +0 -60
  98. package/dist/core/address.js +0 -142
  99. package/dist/core/address.js.map +0 -1
  100. package/dist/core/cell-store.d.ts +0 -61
  101. package/dist/core/cell-store.js +0 -135
  102. package/dist/core/cell-store.js.map +0 -1
  103. package/dist/core/command/default-registry.d.ts +0 -10
  104. package/dist/core/command/default-registry.js +0 -18
  105. package/dist/core/command/default-registry.js.map +0 -1
  106. package/dist/core/command/history.d.ts +0 -64
  107. package/dist/core/command/history.js +0 -127
  108. package/dist/core/command/history.js.map +0 -1
  109. package/dist/core/command/merge-cells.d.ts +0 -21
  110. package/dist/core/command/merge-cells.js +0 -74
  111. package/dist/core/command/merge-cells.js.map +0 -1
  112. package/dist/core/command/registry.d.ts +0 -17
  113. package/dist/core/command/registry.js +0 -26
  114. package/dist/core/command/registry.js.map +0 -1
  115. package/dist/core/command/set-cell-formula.d.ts +0 -17
  116. package/dist/core/command/set-cell-formula.js +0 -29
  117. package/dist/core/command/set-cell-formula.js.map +0 -1
  118. package/dist/core/command/set-cell-value.d.ts +0 -22
  119. package/dist/core/command/set-cell-value.js +0 -35
  120. package/dist/core/command/set-cell-value.js.map +0 -1
  121. package/dist/core/command/types.d.ts +0 -68
  122. package/dist/core/events.js +0 -29
  123. package/dist/core/events.js.map +0 -1
  124. package/dist/core/formula/ast.d.ts +0 -58
  125. package/dist/core/formula/ast.js +0 -38
  126. package/dist/core/formula/ast.js.map +0 -1
  127. package/dist/core/formula/dependency-graph.d.ts +0 -70
  128. package/dist/core/formula/dependency-graph.js +0 -269
  129. package/dist/core/formula/dependency-graph.js.map +0 -1
  130. package/dist/core/formula/errors.d.ts +0 -25
  131. package/dist/core/formula/errors.js +0 -36
  132. package/dist/core/formula/errors.js.map +0 -1
  133. package/dist/core/formula/evaluator.d.ts +0 -41
  134. package/dist/core/formula/evaluator.js +0 -154
  135. package/dist/core/formula/evaluator.js.map +0 -1
  136. package/dist/core/formula/functions.d.ts +0 -30
  137. package/dist/core/formula/functions.js +0 -209
  138. package/dist/core/formula/functions.js.map +0 -1
  139. package/dist/core/formula/parser.d.ts +0 -7
  140. package/dist/core/formula/parser.js +0 -208
  141. package/dist/core/formula/parser.js.map +0 -1
  142. package/dist/core/formula/tokenizer.d.ts +0 -37
  143. package/dist/core/formula/tokenizer.js +0 -146
  144. package/dist/core/formula/tokenizer.js.map +0 -1
  145. package/dist/core/merge-manager.d.ts +0 -64
  146. package/dist/core/merge-manager.js +0 -111
  147. package/dist/core/merge-manager.js.map +0 -1
  148. package/dist/core/selection.d.ts +0 -31
  149. package/dist/core/selection.js +0 -62
  150. package/dist/core/selection.js.map +0 -1
  151. package/dist/core/sheet.d.ts +0 -105
  152. package/dist/core/sheet.js +0 -202
  153. package/dist/core/sheet.js.map +0 -1
  154. package/dist/core/workbook.d.ts +0 -43
  155. package/dist/core/workbook.js +0 -77
  156. package/dist/core/workbook.js.map +0 -1
  157. package/dist/grid/sheet-grid.d.ts +0 -59
  158. package/dist/grid/sheet-grid.js +0 -230
  159. package/dist/grid/sheet-grid.js.map +0 -1
  160. package/dist/tools/builtin.js.map +0 -1
  161. package/dist/tools/context.js.map +0 -1
  162. package/dist/tools/registry.js.map +0 -1
  163. package/dist/vue/index.d.ts +0 -2
  164. package/dist/vue/sheet.js.map +0 -1
  165. package/dist/vue/sheet.vue.d.ts +0 -24
  166. package/src/core/__test__/address.test.ts +0 -113
  167. package/src/core/__test__/cell-store.test.ts +0 -97
  168. package/src/core/__test__/command.test.ts +0 -377
  169. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  170. package/src/core/__test__/formula-engine.test.ts +0 -470
  171. package/src/core/__test__/formula-functions.test.ts +0 -178
  172. package/src/core/__test__/formula-parser.test.ts +0 -160
  173. package/src/core/__test__/history.test.ts +0 -225
  174. package/src/core/__test__/merge-manager.test.ts +0 -106
  175. package/src/core/__test__/sheet.test.ts +0 -142
  176. package/src/core/address.ts +0 -166
  177. package/src/core/cell-store.ts +0 -173
  178. package/src/core/command/default-registry.ts +0 -15
  179. package/src/core/command/history.ts +0 -167
  180. package/src/core/command/index.ts +0 -7
  181. package/src/core/command/merge-cells.ts +0 -74
  182. package/src/core/command/registry.ts +0 -30
  183. package/src/core/command/set-cell-formula.ts +0 -29
  184. package/src/core/command/set-cell-value.ts +0 -38
  185. package/src/core/command/types.ts +0 -73
  186. package/src/core/events.ts +0 -37
  187. package/src/core/formula/ast.ts +0 -66
  188. package/src/core/formula/dependency-graph.ts +0 -329
  189. package/src/core/formula/errors.ts +0 -41
  190. package/src/core/formula/evaluator.ts +0 -219
  191. package/src/core/formula/functions.ts +0 -266
  192. package/src/core/formula/index.ts +0 -7
  193. package/src/core/formula/parser.ts +0 -198
  194. package/src/core/formula/tokenizer.ts +0 -160
  195. package/src/core/merge-manager.ts +0 -158
  196. package/src/core/selection.ts +0 -66
  197. package/src/core/sheet.ts +0 -257
  198. package/src/core/workbook.ts +0 -99
  199. package/src/grid/__test__/canvas-mock.ts +0 -49
  200. package/src/grid/__test__/setup.ts +0 -3
  201. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  202. package/src/grid/__test__/sheet-grid.test.ts +0 -177
  203. package/src/grid/sheet-grid.ts +0 -283
@@ -0,0 +1,61 @@
1
+ /// <reference lib="webworker" />
2
+ /**
3
+ * XLSX 导出 Worker:exportWorkbookXlsx(模型 → hucre 序列化 + ZIP 压缩,
4
+ * 大工作簿为秒级同步重活)移到独立线程,主线程保持空闲。
5
+ *
6
+ * 协议:主线程 postMessage({ sheets: [{ name, snapshot }], activeIndex })
7
+ * (快照为纯数据,可结构化克隆,行高随 SheetSnapshot.rowHeights 携带)→
8
+ * worker 返回 { ok: true, buffer }(精确拷贝后 transfer 回主线程);
9
+ * 失败返回 { ok: false, error }。
10
+ *
11
+ * 与 import.worker.ts 同一约束:必须用**运行时动态 import**——worker 顶层
12
+ * 静态 import 在 vite dev 的 worker 上下文中会因模块图加载顺序导致
13
+ * `Workbook is not defined`(实测)。
14
+ */
15
+ import type { SheetSnapshot } from '@veltra/sheet-core/core/sheet'
16
+
17
+ export interface ExportWorkerPayload {
18
+ sheets: { name: string; snapshot: SheetSnapshot }[]
19
+ activeIndex: number
20
+ }
21
+
22
+ export interface ExportWorkerResponse {
23
+ ok: boolean
24
+ buffer?: Uint8Array
25
+ error?: string
26
+ }
27
+
28
+ self.onmessage = (e: MessageEvent<ExportWorkerPayload>): void => {
29
+ void Promise.all([
30
+ import('@veltra/sheet-core/core/io/export'),
31
+ import('@veltra/sheet-core/core/workbook')
32
+ ])
33
+ .then(([{ exportWorkbookXlsx }, { Workbook }]) => {
34
+ const wb = new Workbook()
35
+ const sheets = e.data.sheets
36
+ for (let i = 0; i < sheets.length; i++) {
37
+ const { name, snapshot } = sheets[i]!
38
+ // 首个 sheet 复用 Workbook 自带的默认表:先改名对齐(空名/重名保持原名,
39
+ // 导出侧 hucre 1.0 会校验表名合法性)
40
+ const sheet = i === 0 ? wb.activeSheet : wb.addSheet(name)
41
+ if (i === 0 && sheet.name !== name) wb.renameSheet(sheet.name, name)
42
+ sheet.restore(snapshot)
43
+ }
44
+ wb.activateSheet(wb.getSheets()[Math.min(e.data.activeIndex, wb.sheetCount - 1)]!.name)
45
+ return exportWorkbookXlsx(wb)
46
+ })
47
+ .then((buffer) => {
48
+ // transfer 精确视图(writeXlsx 返回的 Uint8Array 理论上独占底层 buffer,
49
+ // slice 一份拷贝兜底偏移情形,避免 transfer 整个底层 buffer 带出冗余字节)
50
+ const exact = buffer.slice()
51
+ const response: ExportWorkerResponse = { ok: true, buffer: exact }
52
+ ;(self as unknown as Worker).postMessage(response, [exact.buffer])
53
+ })
54
+ .catch((err: unknown) => {
55
+ const response: ExportWorkerResponse = {
56
+ ok: false,
57
+ error: err instanceof Error ? err.message : String(err)
58
+ }
59
+ ;(self as unknown as Worker).postMessage(response)
60
+ })
61
+ }
@@ -1,4 +1,5 @@
1
- import { TypedEventEmitter } from '../core/events'
1
+ import { TypedEventEmitter } from '@veltra/sheet-core/core/events'
2
+
2
3
  import type { SheetContext } from './context'
3
4
 
4
5
  /**
@@ -12,6 +13,16 @@ import type { SheetContext } from './context'
12
13
  * (与 defaultCommandRegistry 同语义;每个实例的 SheetContext 各自绑定当前 sheet)
13
14
  */
14
15
 
16
+ /** 弹层型工具类型:vue 层按类型渲染弹层面板(面板交互走 SheetContext 命令入口) */
17
+ export type SheetToolPopupType =
18
+ | 'fill-color'
19
+ | 'border'
20
+ | 'font-color'
21
+ | 'font-size'
22
+ | 'find'
23
+ | 'insert-image'
24
+ | 'export'
25
+
15
26
  /** 工具定义 */
16
27
  export interface SheetTool {
17
28
  /** 唯一 id;同 id 重复注册视为替换(保留原位置) */
@@ -30,6 +41,10 @@ export interface SheetTool {
30
41
  visible?(ctx: SheetContext): boolean
31
42
  /** 是否禁用(工具栏状态刷新时求值);缺省可用 */
32
43
  disabled?(ctx: SheetContext): boolean
44
+ /** 是否处于激活/高亮状态(工具栏状态刷新时求值,vue 层渲染 is-active);缺省 false */
45
+ active?(ctx: SheetContext): boolean
46
+ /** 弹层型工具:vue 层渲染弹层面板,onClick 不执行(面板交互走 SheetContext) */
47
+ popup?: SheetToolPopupType
33
48
  /** 点击执行;ctx 是唯一操作入口(写操作全走命令系统,天然可撤销) */
34
49
  onClick(ctx: SheetContext): void
35
50
  }
@@ -1 +1,7 @@
1
1
  export * from './sheet'
2
+ export type {
3
+ SheetImageType,
4
+ SheetImageAnchor,
5
+ SheetImage,
6
+ ImageInput
7
+ } from '@veltra/sheet-core/core/image'
@@ -1,9 +1,9 @@
1
+ import type { Sheet } from '@veltra/sheet-core/core/sheet'
2
+ import type { Workbook } from '@veltra/sheet-core/core/workbook'
3
+ import type { SheetGrid } from '@veltra/sheet-core/grid/sheet-grid'
1
4
  import type { DeconstructValue } from '@veltra/utils'
2
5
  import type { ComputedRef } from 'vue'
3
6
 
4
- import type { Sheet } from '../core/sheet'
5
- import type { Workbook } from '../core/workbook'
6
- import type { SheetGrid } from '../grid/sheet-grid'
7
7
  import type { SheetContext } from '../tools/context'
8
8
 
9
9
  /** 电子表格组件属性 */
@@ -16,6 +16,8 @@ export interface SheetProps {
16
16
  cols?: number
17
17
  /** 是否显示工具栏,默认 true */
18
18
  showToolbar?: boolean
19
+ /** 是否显示顶部公式栏(名称框 + fx 输入栏),默认 true */
20
+ showFormulaBar?: boolean
19
21
  /** 是否显示底部 sheet 标签栏,默认 true */
20
22
  showTabs?: boolean
21
23
  }
@@ -0,0 +1,455 @@
1
+ import { Workbook } from '@veltra/sheet-core/core/workbook'
2
+ import { ListTable } from '@visactor/vtable'
3
+ import { afterEach, describe, expect, it } from 'vitest'
4
+ import { createApp, h, nextTick, type App } from 'vue'
5
+
6
+ import { USheet } from '../../index'
7
+ import type { SheetExposed } from '../../types'
8
+
9
+ const apps: App[] = []
10
+ const containers: HTMLElement[] = []
11
+
12
+ function mount(
13
+ props: () => Record<string, unknown> = () => ({}),
14
+ exposedRef?: { value: SheetExposed | undefined }
15
+ ) {
16
+ const el = document.createElement('div')
17
+ el.style.width = '800px'
18
+ el.style.height = '600px'
19
+ document.body.appendChild(el)
20
+ containers.push(el)
21
+ const app = createApp({
22
+ render: () =>
23
+ h(USheet, {
24
+ ...props(),
25
+ ref: (value: unknown) => {
26
+ if (exposedRef) exposedRef.value = value as SheetExposed | undefined
27
+ }
28
+ })
29
+ })
30
+ app.mount(el)
31
+ apps.push(app)
32
+ return { app, el }
33
+ }
34
+
35
+ function createWorkbook() {
36
+ const workbook = new Workbook()
37
+ workbook.addSheet('Sheet2')
38
+ return workbook
39
+ }
40
+
41
+ afterEach(() => {
42
+ while (apps.length) apps.pop()!.unmount()
43
+ while (containers.length) containers.pop()!.remove()
44
+ })
45
+
46
+ function nameBox(el: HTMLElement): HTMLInputElement {
47
+ return el.querySelector<HTMLInputElement>('.u-sheet__name-box')!
48
+ }
49
+
50
+ function fxInput(el: HTMLElement): HTMLTextAreaElement {
51
+ return el.querySelector<HTMLTextAreaElement>('.u-sheet__fx-input')!
52
+ }
53
+
54
+ /** 名称框输入并按键(默认 Enter) */
55
+ function typeName(el: HTMLElement, text: string, key = 'Enter'): void {
56
+ const input = nameBox(el)
57
+ input.value = text
58
+ input.dispatchEvent(new Event('input', { bubbles: true }))
59
+ if (key) input.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
60
+ }
61
+
62
+ /** fx 输入栏聚焦 → 输入 → 按键(key = null 时不按键,保持编辑态) */
63
+ function typeFx(el: HTMLElement, text: string, key: string | null = null): void {
64
+ const input = fxInput(el)
65
+ input.dispatchEvent(new FocusEvent('focus'))
66
+ input.value = text
67
+ input.dispatchEvent(new Event('input', { bubbles: true }))
68
+ if (key) input.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
69
+ }
70
+
71
+ describe('USheet 公式栏(名称框 + fx 输入栏)', () => {
72
+ it('选区变化 → 名称框 / 输入栏内容正确(普通值 / 公式原文 / 空 / 合并格锚点)', async () => {
73
+ const workbook = new Workbook()
74
+ const sheet = workbook.activeSheet
75
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
76
+ await nextTick()
77
+
78
+ // 默认选区 A1:名称框显示 A1,fx 输入栏可用
79
+ expect(nameBox(el).value).toBe('A1')
80
+ expect(fxInput(el).disabled).toBe(false)
81
+ expect(fxInput(el).value).toBe('')
82
+
83
+ // 普通值 → 原始值文本
84
+ sheet.setCellValue({ row: 0, col: 0 }, 'hello')
85
+ await nextTick()
86
+ expect(fxInput(el).value).toBe('hello')
87
+
88
+ // 公式格 → '=' + f 原文
89
+ sheet.setCellFormula({ row: 0, col: 0 }, '=SUM(1,2)')
90
+ await nextTick()
91
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
92
+
93
+ // 区域选区 → A1:B2 序列化;活动格(区域起点)为空 → 输入栏空
94
+ sheet.selectRange({ start: { row: 1, col: 1 }, end: { row: 3, col: 3 } })
95
+ await nextTick()
96
+ expect(nameBox(el).value).toBe('B2:D4')
97
+ expect(fxInput(el).value).toBe('')
98
+
99
+ // 合并格锚点:点被覆盖格 → 名称框显示锚点地址(B2)
100
+ sheet.mergeCells({ start: { row: 1, col: 1 }, end: { row: 2, col: 2 } })
101
+ sheet.selectCell({ row: 2, col: 2 })
102
+ await nextTick()
103
+ expect(sheet.getSelection().activeCell).toEqual({ row: 1, col: 1 })
104
+ expect(nameBox(el).value).toBe('B2')
105
+ })
106
+
107
+ it('输入 =SUM(A1:A2) 提交 → 模型公式与计算值正确;普通文本原样存储;Esc 不改模型', async () => {
108
+ const workbook = new Workbook()
109
+ const sheet = workbook.activeSheet
110
+ sheet.setCellValue({ row: 0, col: 0 }, 1)
111
+ sheet.setCellValue({ row: 1, col: 0 }, 2)
112
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
113
+ await nextTick()
114
+ sheet.selectCell({ row: 2, col: 0 })
115
+ await nextTick()
116
+
117
+ // 公式提交:'=' 前缀自动公式路径;计算值正确;提交后显示公式原文;选区保持
118
+ typeFx(el, '=SUM(A1:A2)', 'Enter')
119
+ await nextTick()
120
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: 'SUM(A1:A2)', v: 3, t: 'n' })
121
+ expect(sheet.getDisplayValue({ row: 2, col: 0 })).toBe(3)
122
+ expect(fxInput(el).value).toBe('=SUM(A1:A2)')
123
+ expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
124
+
125
+ // 普通文本原样存储(首尾空格不 trim)
126
+ typeFx(el, ' hello ', 'Enter')
127
+ await nextTick()
128
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: ' hello ', t: 's' })
129
+
130
+ // Esc 取消:不改模型,输入栏还原为模型内容
131
+ typeFx(el, 'zzz', 'Escape')
132
+ await nextTick()
133
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: ' hello ' })
134
+ expect(fxInput(el).value).toBe(' hello ')
135
+
136
+ // 失焦提交(Excel 语义:点击网格等场景自然提交)
137
+ typeFx(el, 'blur-submit')
138
+ fxInput(el).dispatchEvent(new FocusEvent('blur'))
139
+ await nextTick()
140
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: 'blur-submit' })
141
+ })
142
+
143
+ it('名称框输入 B3 / B3:D5 跳转正确;非法输入被拒绝', async () => {
144
+ const workbook = new Workbook()
145
+ const sheet = workbook.activeSheet
146
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
147
+ await nextTick()
148
+
149
+ // 单格地址 → selectCell 跳转
150
+ typeName(el, 'B3')
151
+ await nextTick()
152
+ expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 1 })
153
+ expect(nameBox(el).value).toBe('B3')
154
+
155
+ // 区域 → selectRange 跳转
156
+ typeName(el, 'B3:D5')
157
+ await nextTick()
158
+ expect(sheet.getSelection().ranges[0]).toEqual({
159
+ start: { row: 2, col: 1 },
160
+ end: { row: 4, col: 3 }
161
+ })
162
+ expect(nameBox(el).value).toBe('B3:D5')
163
+
164
+ // 非法:不跳转、不写入,名称框还原为当前选区地址
165
+ typeName(el, 'INVALID!')
166
+ await nextTick()
167
+ expect(sheet.getSelection().ranges[0]).toEqual({
168
+ start: { row: 2, col: 1 },
169
+ end: { row: 4, col: 3 }
170
+ })
171
+ expect(nameBox(el).value).toBe('B3:D5')
172
+
173
+ // Esc:还原显示
174
+ typeName(el, 'X99', 'Escape')
175
+ await nextTick()
176
+ expect(sheet.getSelection().ranges[0]).toEqual({
177
+ start: { row: 2, col: 1 },
178
+ end: { row: 4, col: 3 }
179
+ })
180
+ expect(nameBox(el).value).toBe('B3:D5')
181
+ })
182
+
183
+ it('公式栏编辑期间网格事件不打断输入;提交后网格渲染计算值', async () => {
184
+ const workbook = new Workbook()
185
+ const sheet = workbook.activeSheet
186
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
187
+ sheet.setCellValue({ row: 0, col: 0 }, 1)
188
+ sheet.setCellValue({ row: 1, col: 0 }, 2)
189
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
190
+ await nextTick()
191
+ sheet.selectCell({ row: 2, col: 0 })
192
+ await nextTick()
193
+
194
+ // 聚焦进入编辑态并输入(不按键)
195
+ typeFx(el, '=SUM(A1:A2)')
196
+ // 网格侧选区变化(编辑态锁:输入内容不被覆盖)
197
+ sheet.selectCell({ row: 5, col: 5 })
198
+ await nextTick()
199
+ expect(fxInput(el).value).toBe('=SUM(A1:A2)')
200
+
201
+ // 提交 → 写进入编辑时的格(A3),而非当前选区格(F6)
202
+ fxInput(el).dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
203
+ await nextTick()
204
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ f: 'SUM(A1:A2)', v: 3 })
205
+
206
+ // 提交后网格渲染计算值(模型 A3 → 表格坐标 (1,3))
207
+ expect(exposed.value?.getGrid()?.getTable().getCellValue(1, 3)).toBe(3)
208
+ })
209
+
210
+ it('网格侧编辑提交(change_cell_value)后公式栏同步显示', async () => {
211
+ const workbook = new Workbook()
212
+ const sheet = workbook.activeSheet
213
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
214
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
215
+ await nextTick()
216
+ sheet.selectCell({ row: 0, col: 0 })
217
+ await nextTick()
218
+
219
+ // 模拟网格双击编辑提交(与 grid 测试同一路径:change_cell_value → 模型回写)
220
+ exposed.value?.getGrid()?.getTable().changeCellValue(1, 1, 'grid-edited', false, true)
221
+ await nextTick()
222
+ expect(fxInput(el).value).toBe('grid-edited')
223
+ })
224
+
225
+ it('网格编辑器打开(startEditCell)→ 公式栏镜像显示公式原文,提交后退出镜像', async () => {
226
+ const workbook = new Workbook()
227
+ const sheet = workbook.activeSheet
228
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
229
+ sheet.setCellFormula({ row: 0, col: 0 }, '=SUM(1,2)')
230
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
231
+ await nextTick()
232
+ sheet.selectCell({ row: 0, col: 0 })
233
+ await nextTick()
234
+
235
+ // 程序化进入网格编辑(表格坐标 (1,1) = 模型 A1)
236
+ exposed.value?.getGrid()?.getTable().startEditCell(1, 1)
237
+ await nextTick()
238
+ expect(fxInput(el).readOnly).toBe(true)
239
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
240
+
241
+ // 提交 → 编辑器 onEnd → 公式栏退出镜像(显示模型内容)
242
+ exposed.value?.getGrid()?.getTable().completeEditCell()
243
+ await nextTick()
244
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ f: 'SUM(1,2)' })
245
+ expect(fxInput(el).readOnly).toBe(false)
246
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
247
+ })
248
+
249
+ it('tab 切换:重绑订阅并刷新(新 sheet 的选区 / 单元格内容回显)', async () => {
250
+ const workbook = createWorkbook()
251
+ const sheet1 = workbook.activeSheet
252
+ const sheet2 = workbook.getSheet('Sheet2')!
253
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
254
+ await nextTick()
255
+ // Sheet1 默认选区 A1
256
+ expect(nameBox(el).value).toBe('A1')
257
+
258
+ // Sheet2 的选区不影响公式栏(订阅的是 Sheet1)
259
+ sheet2.selectCell({ row: 1, col: 1 })
260
+ await nextTick()
261
+ expect(nameBox(el).value).toBe('A1')
262
+
263
+ workbook.activateSheet('Sheet2')
264
+ await nextTick()
265
+ expect(nameBox(el).value).toBe('B2')
266
+
267
+ sheet2.setCellValue({ row: 1, col: 1 }, 's2')
268
+ await nextTick()
269
+ expect(fxInput(el).value).toBe('s2')
270
+
271
+ // 切回 Sheet1:公式栏跟随
272
+ workbook.activateSheet('Sheet1')
273
+ await nextTick()
274
+ expect(nameBox(el).value).toBe('A1')
275
+ })
276
+
277
+ it('fx 编辑中切换 tab:编辑态重置,草稿不写入任何 sheet', async () => {
278
+ const workbook = createWorkbook()
279
+ const sheet1 = workbook.activeSheet
280
+ const sheet2 = workbook.getSheet('Sheet2')!
281
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
282
+ await nextTick()
283
+ sheet1.selectCell({ row: 0, col: 0 })
284
+ await nextTick()
285
+
286
+ // 进入 fx 编辑态并输入草稿
287
+ const input = fxInput(el)
288
+ input.dispatchEvent(new Event('focus', { bubbles: true }))
289
+ input.value = 'draft'
290
+ input.dispatchEvent(new Event('input', { bubbles: true }))
291
+ await nextTick()
292
+
293
+ // 切 tab → 编辑态重置(editing=false、editAddr=null)
294
+ workbook.activateSheet('Sheet2')
295
+ await nextTick()
296
+ // fx 显示新 sheet 内容(空),不再显示草稿
297
+ expect(input.value).toBe('')
298
+ // Enter 提交:编辑态已重置 → 不写入
299
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
300
+ await nextTick()
301
+ expect(sheet2.getCellData({ row: 0, col: 0 })).toBeUndefined()
302
+ expect(sheet1.getCellData({ row: 0, col: 0 })).toBeUndefined()
303
+ })
304
+
305
+ it('showFormulaBar=false 隐藏公式栏', async () => {
306
+ const { el } = mount(() => ({ showFormulaBar: false }))
307
+ await nextTick()
308
+ expect(el.querySelector('.u-sheet__formula-bar')).toBeNull()
309
+ })
310
+ })
311
+
312
+ /** 设置 fx 文本并同步光标到末尾(触发补全 / 引用选择上下文) */
313
+ function setFxText(el: HTMLElement, text: string): HTMLTextAreaElement {
314
+ const input = fxInput(el)
315
+ input.dispatchEvent(new FocusEvent('focus'))
316
+ input.value = text
317
+ input.dispatchEvent(new Event('input', { bubbles: true }))
318
+ input.setSelectionRange(text.length, text.length)
319
+ input.dispatchEvent(new Event('keyup', { bubbles: true }))
320
+ return input
321
+ }
322
+
323
+ function suggestList(el: HTMLElement): HTMLElement | null {
324
+ return el.querySelector('.u-sheet__fx-suggest')
325
+ }
326
+
327
+ describe('USheet 公式栏:函数补全与引用选择', () => {
328
+ it('输入 = 弹出候选含 SUM;SU 过滤到 SUM;↓+Enter 替换为 =SUM() 且光标在括号内', async () => {
329
+ const workbook = new Workbook()
330
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
331
+ await nextTick()
332
+
333
+ setFxText(el, '=')
334
+ await nextTick()
335
+ expect(suggestList(el)).not.toBeNull()
336
+ const emptyPrefixNames = [...el.querySelectorAll('.u-sheet__fx-suggest-item')].map(
337
+ (n) => n.querySelector('.u-sheet__fx-suggest-sig')!.textContent
338
+ )
339
+ expect(emptyPrefixNames.length).toBeGreaterThan(0)
340
+ expect(emptyPrefixNames.some((t) => t?.startsWith('SUM('))).toBe(true)
341
+
342
+ setFxText(el, '=SU')
343
+ await nextTick()
344
+ const items = [...el.querySelectorAll('.u-sheet__fx-suggest-item')].map(
345
+ (n) => n.querySelector('.u-sheet__fx-suggest-sig')!.textContent
346
+ )
347
+ expect(items.some((t) => t?.startsWith('SUM('))).toBe(true)
348
+
349
+ const input = fxInput(el)
350
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
351
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
352
+ await nextTick()
353
+ expect(input.value).toBe('=SUM()')
354
+ expect(input.selectionStart).toBe(5)
355
+ expect(suggestList(el)).toBeNull()
356
+ })
357
+
358
+ it('Esc 分层:先关候选再取消编辑;候选开着时 Enter 确认而非提交', async () => {
359
+ const workbook = new Workbook()
360
+ const sheet = workbook.activeSheet
361
+ sheet.setCellValue({ row: 0, col: 0 }, 'keep')
362
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }))
363
+ await nextTick()
364
+
365
+ const input = setFxText(el, '=S')
366
+ await nextTick()
367
+ expect(suggestList(el)).not.toBeNull()
368
+
369
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
370
+ await nextTick()
371
+ expect(suggestList(el)).toBeNull()
372
+ expect(input.value).toBe('=S') // 仍在编辑
373
+
374
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
375
+ await nextTick()
376
+ expect(input.value).toBe('keep') // 取消还原
377
+ expect(sheet.getCellData({ row: 0, col: 0 })).toMatchObject({ v: 'keep' })
378
+ })
379
+
380
+ it('引用插入文本与光标位置;blur 抑制;非引用选择失焦仍提交', async () => {
381
+ const workbook = new Workbook()
382
+ const sheet = workbook.activeSheet
383
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
384
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
385
+ await nextTick()
386
+ sheet.selectCell({ row: 2, col: 0 })
387
+ await nextTick()
388
+
389
+ const input = setFxText(el, '=SUM(')
390
+ await nextTick()
391
+
392
+ // pointerdown on grid → blur 不提交(引用选择);监听绑定在实例容器(LRU 缓存)
393
+ const gridEl = el.querySelector('.u-sheet__grid-instance')!
394
+ gridEl.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
395
+ input.dispatchEvent(new FocusEvent('blur'))
396
+ await nextTick()
397
+ expect(sheet.getCellData({ row: 2, col: 0 })).toBeUndefined()
398
+ expect(input.value).toBe('=SUM(')
399
+
400
+ // 与 grid 选区测试同路径:selectCells 设视觉选区 + fire SELECTED_CELL
401
+ const table = exposed.value!.getGrid()!.getTable()
402
+ table.selectCells([{ start: { col: 1, row: 1 }, end: { col: 2, row: 2 } }])
403
+ table.fireListeners(ListTable.EVENT_TYPE.SELECTED_CELL, { col: 2, row: 2 })
404
+ await nextTick()
405
+ expect(input.value).toBe('=SUM(A1:B2')
406
+ expect(input.selectionStart).toBe('=SUM(A1:B2'.length)
407
+ // intercept 不写模型选区
408
+ expect(sheet.getSelection().activeCell).toEqual({ row: 2, col: 0 })
409
+
410
+ // 非引用选择场景失焦提交
411
+ setFxText(el, 'plain')
412
+ await nextTick()
413
+ fxInput(el).dispatchEvent(new FocusEvent('blur'))
414
+ await nextTick()
415
+ expect(sheet.getCellData({ row: 2, col: 0 })).toMatchObject({ v: 'plain' })
416
+ })
417
+
418
+ it('网格镜像期不弹补全', async () => {
419
+ const workbook = new Workbook()
420
+ const sheet = workbook.activeSheet
421
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
422
+ sheet.setCellFormula({ row: 0, col: 0 }, '=SUM(1,2)')
423
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
424
+ await nextTick()
425
+ sheet.selectCell({ row: 0, col: 0 })
426
+ await nextTick()
427
+
428
+ exposed.value?.getGrid()?.getTable().startEditCell(1, 1)
429
+ await nextTick()
430
+ expect(fxInput(el).readOnly).toBe(true)
431
+ expect(fxInput(el).value).toBe('=SUM(1,2)')
432
+ expect(suggestList(el)).toBeNull()
433
+
434
+ exposed.value?.getGrid()?.getTable().completeEditCell()
435
+ await nextTick()
436
+ })
437
+
438
+ it('连续引用选择:=A1+ 后再插入 B1', async () => {
439
+ const workbook = new Workbook()
440
+ const sheet = workbook.activeSheet
441
+ const exposed: { value: SheetExposed | undefined } = { value: undefined }
442
+ const { el } = mount(() => ({ workbook, rows: 20, cols: 8 }), exposed)
443
+ await nextTick()
444
+ sheet.selectCell({ row: 2, col: 0 })
445
+ await nextTick()
446
+
447
+ setFxText(el, '=A1+')
448
+ await nextTick()
449
+ const table = exposed.value!.getGrid()!.getTable()
450
+ table.selectCells([{ start: { col: 2, row: 1 }, end: { col: 2, row: 1 } }])
451
+ table.fireListeners(ListTable.EVENT_TYPE.SELECTED_CELL, { col: 2, row: 1 })
452
+ await nextTick()
453
+ expect(fxInput(el).value).toBe('=A1+B1')
454
+ })
455
+ })