@veltra/sheet 2.0.1 → 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.
- package/dist/index.d.ts +4 -26
- package/dist/index.js +3 -23
- package/dist/tools/builtin.js +279 -8
- package/dist/tools/context.d.ts +54 -11
- package/dist/tools/context.js +28 -5
- package/dist/tools/download.js +68 -0
- package/dist/tools/export.worker.d.ts +16 -0
- package/dist/tools/export.worker.js +29 -0
- package/dist/tools/registry.d.ts +7 -4
- package/dist/tools/registry.js +1 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/sheet.d.ts +6 -5
- package/dist/vue/formula-bar.js +393 -0
- package/dist/vue/formula-suggest-list.js +66 -0
- package/dist/vue/import-file.js +140 -0
- package/dist/vue/insert-count-menu-item.js +68 -0
- package/dist/vue/insert-image.js +79 -0
- package/dist/vue/popup-helpers.js +73 -0
- package/dist/vue/popups/border-popup.js +75 -0
- package/dist/vue/popups/border-preset-glyph.js +106 -0
- package/dist/vue/popups/export-popup.js +51 -0
- package/dist/vue/popups/fill-color-popup.js +37 -0
- package/dist/vue/popups/find-popup.js +113 -0
- package/dist/vue/popups/font-color-popup.js +37 -0
- package/dist/vue/popups/font-size-popup.js +63 -0
- package/dist/vue/popups/import.worker.d.ts +21 -0
- package/dist/vue/popups/import.worker.js +41 -0
- package/dist/vue/popups/insert-image-popup.js +38 -0
- package/dist/vue/sheet-context-menu.js +200 -0
- package/dist/vue/sheet-tabs.js +162 -0
- package/dist/vue/sheet-toolbar.js +106 -0
- package/dist/vue/sheet.js +144 -164
- package/dist/vue/style.d.ts +11 -1
- package/dist/vue/style.js +10 -0
- package/dist/vue/style2.css +564 -3
- package/dist/vue/use-find-replace.js +161 -0
- package/dist/vue/use-formula-ref-select.js +37 -0
- package/dist/vue/use-formula-suggest.js +126 -0
- package/dist/vue/use-sheet-grid.js +180 -0
- package/dist/vue/use-sheet-state.js +122 -0
- package/dist/vue/use-sheet-tabs-bar.js +90 -0
- package/dist/vue/use-tool-groups.js +42 -0
- package/dist/vue/use-tool-popup.js +123 -0
- package/dist/vue/use-toolbar-scroll.js +72 -0
- package/package.json +12 -7
- package/src/env.d.ts +11 -0
- package/src/index.ts +203 -14
- package/src/tools/__test__/builtin.test.ts +208 -66
- package/src/tools/__test__/context.test.ts +62 -2
- package/src/tools/builtin.ts +350 -6
- package/src/tools/context.ts +89 -11
- package/src/tools/download.ts +82 -0
- package/src/tools/export.worker.ts +61 -0
- package/src/tools/registry.ts +16 -1
- package/src/types/index.ts +6 -0
- package/src/types/sheet.ts +5 -3
- package/src/vue/__test__/formula-bar.test.ts +455 -0
- package/src/vue/__test__/import-file.test.ts +183 -0
- package/src/vue/__test__/insert-count-menu-item.test.ts +123 -0
- package/src/vue/__test__/insert-image-popup.test.ts +89 -0
- package/src/vue/__test__/insert-image.test.ts +100 -0
- package/src/vue/__test__/sheet-component.test.ts +716 -27
- package/src/vue/__test__/sheet-context-menu.test.ts +206 -0
- package/src/vue/__test__/use-formula-ref-select.test.ts +35 -0
- package/src/vue/__test__/use-formula-suggest.test.ts +83 -0
- package/src/vue/formula-bar.vue +473 -0
- package/src/vue/formula-suggest-list.vue +64 -0
- package/src/vue/import-file.ts +173 -0
- package/src/vue/insert-count-menu-item.vue +69 -0
- package/src/vue/insert-image.ts +90 -0
- package/src/vue/parsing.ts +11 -0
- package/src/vue/popup-helpers.ts +50 -0
- package/src/vue/popups/border-popup.vue +93 -0
- package/src/vue/popups/border-preset-glyph.vue +111 -0
- package/src/vue/popups/export-popup.vue +52 -0
- package/src/vue/popups/fill-color-popup.vue +39 -0
- package/src/vue/popups/find-popup.vue +111 -0
- package/src/vue/popups/font-color-popup.vue +39 -0
- package/src/vue/popups/font-size-popup.vue +52 -0
- package/src/vue/popups/import.worker.ts +66 -0
- package/src/vue/popups/insert-image-popup.vue +39 -0
- package/src/vue/sheet-context-menu.ts +229 -0
- package/src/vue/sheet-tabs.vue +162 -0
- package/src/vue/sheet-toolbar.vue +91 -0
- package/src/vue/sheet.vue +161 -186
- package/src/vue/style.scss +646 -3
- package/src/vue/style.ts +11 -0
- package/src/vue/use-find-replace.ts +200 -0
- package/src/vue/use-formula-ref-select.ts +34 -0
- package/src/vue/use-formula-suggest.ts +150 -0
- package/src/vue/use-sheet-grid.ts +243 -0
- package/src/vue/use-sheet-state.ts +161 -0
- package/src/vue/use-sheet-tabs-bar.ts +115 -0
- package/src/vue/use-tool-groups.ts +61 -0
- package/src/vue/use-tool-popup.ts +150 -0
- package/src/vue/use-toolbar-scroll.ts +95 -0
- package/dist/core/address.d.ts +0 -60
- package/dist/core/address.js +0 -142
- package/dist/core/address.js.map +0 -1
- package/dist/core/cell-store.d.ts +0 -61
- package/dist/core/cell-store.js +0 -135
- package/dist/core/cell-store.js.map +0 -1
- package/dist/core/command/default-registry.d.ts +0 -10
- package/dist/core/command/default-registry.js +0 -18
- package/dist/core/command/default-registry.js.map +0 -1
- package/dist/core/command/history.d.ts +0 -64
- package/dist/core/command/history.js +0 -127
- package/dist/core/command/history.js.map +0 -1
- package/dist/core/command/merge-cells.d.ts +0 -21
- package/dist/core/command/merge-cells.js +0 -74
- package/dist/core/command/merge-cells.js.map +0 -1
- package/dist/core/command/registry.d.ts +0 -17
- package/dist/core/command/registry.js +0 -26
- package/dist/core/command/registry.js.map +0 -1
- package/dist/core/command/set-cell-formula.d.ts +0 -17
- package/dist/core/command/set-cell-formula.js +0 -29
- package/dist/core/command/set-cell-formula.js.map +0 -1
- package/dist/core/command/set-cell-value.d.ts +0 -22
- package/dist/core/command/set-cell-value.js +0 -35
- package/dist/core/command/set-cell-value.js.map +0 -1
- package/dist/core/command/types.d.ts +0 -68
- package/dist/core/events.js +0 -29
- package/dist/core/events.js.map +0 -1
- package/dist/core/fill.d.ts +0 -34
- package/dist/core/fill.js +0 -320
- package/dist/core/fill.js.map +0 -1
- package/dist/core/formula/ast.d.ts +0 -58
- package/dist/core/formula/ast.js +0 -38
- package/dist/core/formula/ast.js.map +0 -1
- package/dist/core/formula/dependency-graph.d.ts +0 -70
- package/dist/core/formula/dependency-graph.js +0 -269
- package/dist/core/formula/dependency-graph.js.map +0 -1
- package/dist/core/formula/errors.d.ts +0 -25
- package/dist/core/formula/errors.js +0 -36
- package/dist/core/formula/errors.js.map +0 -1
- package/dist/core/formula/evaluator.d.ts +0 -41
- package/dist/core/formula/evaluator.js +0 -154
- package/dist/core/formula/evaluator.js.map +0 -1
- package/dist/core/formula/functions.d.ts +0 -30
- package/dist/core/formula/functions.js +0 -209
- package/dist/core/formula/functions.js.map +0 -1
- package/dist/core/formula/parser.d.ts +0 -7
- package/dist/core/formula/parser.js +0 -208
- package/dist/core/formula/parser.js.map +0 -1
- package/dist/core/formula/tokenizer.d.ts +0 -37
- package/dist/core/formula/tokenizer.js +0 -146
- package/dist/core/formula/tokenizer.js.map +0 -1
- package/dist/core/merge-manager.d.ts +0 -64
- package/dist/core/merge-manager.js +0 -111
- package/dist/core/merge-manager.js.map +0 -1
- package/dist/core/selection.d.ts +0 -31
- package/dist/core/selection.js +0 -62
- package/dist/core/selection.js.map +0 -1
- package/dist/core/sheet.d.ts +0 -116
- package/dist/core/sheet.js +0 -224
- package/dist/core/sheet.js.map +0 -1
- package/dist/core/workbook.d.ts +0 -43
- package/dist/core/workbook.js +0 -77
- package/dist/core/workbook.js.map +0 -1
- package/dist/grid/sheet-grid.d.ts +0 -81
- package/dist/grid/sheet-grid.js +0 -331
- package/dist/grid/sheet-grid.js.map +0 -1
- package/dist/grid/vtable-theme.js +0 -80
- package/dist/grid/vtable-theme.js.map +0 -1
- package/dist/tools/builtin.js.map +0 -1
- package/dist/tools/context.js.map +0 -1
- package/dist/tools/registry.js.map +0 -1
- package/dist/vue/index.d.ts +0 -2
- package/dist/vue/sheet.js.map +0 -1
- package/dist/vue/sheet.vue.d.ts +0 -24
- package/src/core/__test__/address.test.ts +0 -113
- package/src/core/__test__/cell-store.test.ts +0 -97
- package/src/core/__test__/command.test.ts +0 -377
- package/src/core/__test__/fill.test.ts +0 -140
- package/src/core/__test__/formula-adversarial.test.ts +0 -265
- package/src/core/__test__/formula-engine.test.ts +0 -470
- package/src/core/__test__/formula-functions.test.ts +0 -178
- package/src/core/__test__/formula-parser.test.ts +0 -160
- package/src/core/__test__/history.test.ts +0 -225
- package/src/core/__test__/merge-manager.test.ts +0 -106
- package/src/core/__test__/sheet.test.ts +0 -142
- package/src/core/address.ts +0 -166
- package/src/core/cell-store.ts +0 -173
- package/src/core/command/default-registry.ts +0 -15
- package/src/core/command/history.ts +0 -167
- package/src/core/command/index.ts +0 -7
- package/src/core/command/merge-cells.ts +0 -74
- package/src/core/command/registry.ts +0 -30
- package/src/core/command/set-cell-formula.ts +0 -29
- package/src/core/command/set-cell-value.ts +0 -38
- package/src/core/command/types.ts +0 -73
- package/src/core/events.ts +0 -37
- package/src/core/fill.ts +0 -314
- package/src/core/formula/ast.ts +0 -66
- package/src/core/formula/dependency-graph.ts +0 -329
- package/src/core/formula/errors.ts +0 -41
- package/src/core/formula/evaluator.ts +0 -219
- package/src/core/formula/functions.ts +0 -266
- package/src/core/formula/index.ts +0 -7
- package/src/core/formula/parser.ts +0 -198
- package/src/core/formula/tokenizer.ts +0 -160
- package/src/core/merge-manager.ts +0 -158
- package/src/core/selection.ts +0 -66
- package/src/core/sheet.ts +0 -282
- package/src/core/workbook.ts +0 -99
- package/src/grid/__test__/canvas-mock.ts +0 -49
- package/src/grid/__test__/setup.ts +0 -3
- package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
- package/src/grid/__test__/sheet-grid.test.ts +0 -275
- package/src/grid/sheet-grid.ts +0 -407
- package/src/grid/vtable-theme.ts +0 -61
package/src/vue/style.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
+
// USheet UI 实际渲染的 desktop 组件样式(#15:缺项会导致仅依赖本入口的宿主拿到无样式组件)
|
|
1
2
|
import '@veltra/desktop/components/contextmenu/style'
|
|
3
|
+
import '@veltra/desktop/components/dropdown/style'
|
|
4
|
+
import '@veltra/desktop/components/number-input/style'
|
|
5
|
+
import '@veltra/desktop/components/palette/style'
|
|
6
|
+
import '@veltra/desktop/components/scroll/style'
|
|
7
|
+
import '@veltra/desktop/components/tip/style'
|
|
8
|
+
import '@veltra/desktop/components/loading/style' // v-loading(sheet.vue grid 容器)
|
|
9
|
+
import '@veltra/desktop/components/message/style' // message / messageConfirm(弹层、tabs、公式栏)
|
|
10
|
+
import '@veltra/desktop/components/input/style' // UInput(find-popup)
|
|
11
|
+
import '@veltra/desktop/components/file-picker/style' // UFilePicker(insert-image-popup)
|
|
12
|
+
import '@veltra/desktop/components/icon/style' // UIcon(sheet-toolbar / sheet-tabs)
|
|
2
13
|
import './style.scss'
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { debounce } from '@cat-kit/core'
|
|
2
|
+
import {
|
|
3
|
+
inferCellType,
|
|
4
|
+
normalizeInputValue,
|
|
5
|
+
type CellData
|
|
6
|
+
} from '@veltra/sheet-core/core/cell-store'
|
|
7
|
+
import {
|
|
8
|
+
findAll,
|
|
9
|
+
findNextFrom,
|
|
10
|
+
findPrevFrom,
|
|
11
|
+
type FindMatch,
|
|
12
|
+
type FindOptions
|
|
13
|
+
} from '@veltra/sheet-core/core/find'
|
|
14
|
+
import type { Sheet } from '@veltra/sheet-core/core/sheet'
|
|
15
|
+
import { computed, ref, shallowRef, watch } from 'vue'
|
|
16
|
+
|
|
17
|
+
import type { SheetContext } from '../tools/context'
|
|
18
|
+
|
|
19
|
+
interface UseFindReplaceOptions {
|
|
20
|
+
/** 目标 sheet(弹层存活期间固定;tab 切换会关闭并销毁弹层) */
|
|
21
|
+
getSheet: () => Sheet
|
|
22
|
+
context: SheetContext
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 查找条逻辑(关键词 / 上一个 / 下一个 / 命中计数 / 替换):
|
|
27
|
+
* - 关键词 / 选项变化 → 重新查找并定位第一个命中
|
|
28
|
+
* - Enter = 下一个、Shift+Enter = 上一个(行主序、到边界循环)
|
|
29
|
+
* - 替换当前 = 一次 setCells(一个 undo 单元);全部替换 = 一次批量 = 单 undo 单元
|
|
30
|
+
* - 公式格不参与替换(写入 {v,t} 会覆盖公式原文 f,导致公式丢失)
|
|
31
|
+
*/
|
|
32
|
+
export function useFindReplace(options: UseFindReplaceOptions) {
|
|
33
|
+
const { getSheet, context } = options
|
|
34
|
+
|
|
35
|
+
const findQuery = ref('')
|
|
36
|
+
const findReplace = ref('')
|
|
37
|
+
const caseSensitive = ref(false)
|
|
38
|
+
const wholeCell = ref(false)
|
|
39
|
+
const searchIn = ref<'value' | 'formula'>('value')
|
|
40
|
+
/** 全部命中(行主序;core/find 纯逻辑,无头可测) */
|
|
41
|
+
const findMatches = shallowRef<FindMatch[]>([])
|
|
42
|
+
/** 当前命中在 findMatches 中的下标;-1 = 无命中 */
|
|
43
|
+
const findCursor = ref(-1)
|
|
44
|
+
|
|
45
|
+
const canFind = computed(() => findMatches.value.length > 0)
|
|
46
|
+
const canReplace = computed(() => canFind.value && findCursor.value >= 0)
|
|
47
|
+
|
|
48
|
+
const findCountText = computed(() => {
|
|
49
|
+
const total = findMatches.value.length
|
|
50
|
+
if (total === 0) return '0 / 0'
|
|
51
|
+
return `${Math.min(findCursor.value + 1, total)} / ${total}`
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
function findOptions(): FindOptions {
|
|
55
|
+
return {
|
|
56
|
+
caseSensitive: caseSensitive.value,
|
|
57
|
+
wholeCell: wholeCell.value,
|
|
58
|
+
searchIn: searchIn.value
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 上次全扫时的关键词 / 选项快照(识别防抖窗口内的脏缓存,见 ensureFreshMatches) */
|
|
63
|
+
let scannedQuery = ''
|
|
64
|
+
let scannedOptions = ''
|
|
65
|
+
|
|
66
|
+
function optionsSnapshot(): string {
|
|
67
|
+
return `${caseSensitive.value}|${wholeCell.value}|${searchIn.value}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 关键词或选项变化:重新查找并定位第一个命中(否则保持当前位置附近) */
|
|
71
|
+
function refreshFind(initial: boolean): void {
|
|
72
|
+
const matches = findAll(getSheet(), findQuery.value, findOptions())
|
|
73
|
+
scannedQuery = findQuery.value
|
|
74
|
+
scannedOptions = optionsSnapshot()
|
|
75
|
+
findMatches.value = matches
|
|
76
|
+
if (matches.length === 0) {
|
|
77
|
+
findCursor.value = -1
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
if (initial) {
|
|
81
|
+
findCursor.value = 0
|
|
82
|
+
context.selectCell(matches[0]!.addr)
|
|
83
|
+
} else {
|
|
84
|
+
findCursor.value = Math.min(findCursor.value, matches.length - 1)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 防抖窗口内(关键词已输入、尚未触发全扫)Enter 导航 / 替换仍持有旧关键词的
|
|
90
|
+
* 命中缓存——先同步补扫一次,保证导航与替换作用于当前输入框里的关键词
|
|
91
|
+
* (否则「全部替换」会把旧关键词的命中集批量改写,与屏幕上看到的查找词不符)
|
|
92
|
+
*/
|
|
93
|
+
function ensureFreshMatches(): void {
|
|
94
|
+
if (findQuery.value !== scannedQuery || optionsSnapshot() !== scannedOptions) {
|
|
95
|
+
refreshFind(false)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 定位到 findMatches 中与目标地址相同的命中(找不到则保持) */
|
|
100
|
+
function locateMatch(match: FindMatch): void {
|
|
101
|
+
const index = findMatches.value.findIndex(
|
|
102
|
+
(item) => item.addr.row === match.addr.row && item.addr.col === match.addr.col
|
|
103
|
+
)
|
|
104
|
+
if (index >= 0) findCursor.value = index
|
|
105
|
+
context.selectCell(match.addr)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findForward(): void {
|
|
109
|
+
ensureFreshMatches()
|
|
110
|
+
const matches = findMatches.value
|
|
111
|
+
if (matches.length === 0) return
|
|
112
|
+
const active = context.getSelection().activeCell
|
|
113
|
+
// 在已缓存的全量命中数组上二分导航——原 findNext 每次重跑 findAll 全表扫描(#9)
|
|
114
|
+
const next = active ? findNextFrom(matches, active) : matches[0]
|
|
115
|
+
if (!next) return
|
|
116
|
+
locateMatch(next)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findPrevious(): void {
|
|
120
|
+
ensureFreshMatches()
|
|
121
|
+
const matches = findMatches.value
|
|
122
|
+
if (matches.length === 0) return
|
|
123
|
+
const active = context.getSelection().activeCell
|
|
124
|
+
const prev = active ? findPrevFrom(matches, active) : matches[matches.length - 1]
|
|
125
|
+
if (!prev) return
|
|
126
|
+
locateMatch(prev)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Enter = 下一个;Shift+Enter = 上一个(UInput 的 keydown 透传到外层容器,冒泡可达) */
|
|
130
|
+
function handleFindKeydown(event: KeyboardEvent): void {
|
|
131
|
+
if (event.key !== 'Enter') return
|
|
132
|
+
event.preventDefault()
|
|
133
|
+
if (event.shiftKey) {
|
|
134
|
+
findPrevious()
|
|
135
|
+
} else {
|
|
136
|
+
findForward()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 替换文本 → CellData(空字符串 = 清除单元格;数字/布尔文本规范化后推断类型) */
|
|
141
|
+
function replaceData(): CellData | undefined {
|
|
142
|
+
if (findReplace.value === '') return undefined
|
|
143
|
+
const normalized = normalizeInputValue(findReplace.value)
|
|
144
|
+
const t = inferCellType(normalized)
|
|
145
|
+
return t ? { v: normalized, t } : { v: normalized }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 公式格不参与替换(写入 {v,t} 会覆盖公式原文 f,导致公式丢失) */
|
|
149
|
+
function isReplaceable(match: FindMatch): boolean {
|
|
150
|
+
return !context.getCellData(match.addr)?.f
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 替换当前命中格(一次 setCells = 一个 undo 单元);替换后重新查找 */
|
|
154
|
+
function replaceCurrent(): void {
|
|
155
|
+
ensureFreshMatches()
|
|
156
|
+
const match = findMatches.value[findCursor.value]
|
|
157
|
+
if (!match || !isReplaceable(match)) return
|
|
158
|
+
context.setCells([{ addr: match.addr, data: replaceData() }])
|
|
159
|
+
refreshFind(false)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 全部替换(一次 setCells 批量 = 单 undo 单元,undo 一次全部还原);公式格自动跳过 */
|
|
163
|
+
function replaceAll(): void {
|
|
164
|
+
ensureFreshMatches()
|
|
165
|
+
const matches = findMatches.value.filter(isReplaceable)
|
|
166
|
+
if (matches.length === 0) return
|
|
167
|
+
context.setCells(matches.map((match) => ({ addr: match.addr, data: replaceData() })))
|
|
168
|
+
refreshFind(false)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 关键词 / 选项变化 → 重新查找(定位第一个命中)。
|
|
172
|
+
// 每击键全扫 440ms 级:连续击键时防抖合并(300ms 停键才扫,immediate=false 即
|
|
173
|
+
// 首键不抢扫,#28),避免每键一次全表扫描把 UI 卡死;若停键前已 Enter / 替换
|
|
174
|
+
// (ensureFreshMatches 同步补扫),快照已新鲜则跳过多余的补扫
|
|
175
|
+
const refreshFindDebounced = debounce(
|
|
176
|
+
() => {
|
|
177
|
+
if (findQuery.value === scannedQuery && optionsSnapshot() === scannedOptions) return
|
|
178
|
+
refreshFind(true)
|
|
179
|
+
},
|
|
180
|
+
300,
|
|
181
|
+
false
|
|
182
|
+
)
|
|
183
|
+
watch([findQuery, caseSensitive, wholeCell, searchIn], refreshFindDebounced)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
findQuery,
|
|
187
|
+
findReplace,
|
|
188
|
+
caseSensitive,
|
|
189
|
+
wholeCell,
|
|
190
|
+
searchIn,
|
|
191
|
+
canFind,
|
|
192
|
+
canReplace,
|
|
193
|
+
findCountText,
|
|
194
|
+
findForward,
|
|
195
|
+
findPrevious,
|
|
196
|
+
handleFindKeydown,
|
|
197
|
+
replaceCurrent,
|
|
198
|
+
replaceAll
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** 引用选择上下文:光标前一个非空字符属于此集合时可插入单元格引用 */
|
|
2
|
+
const REF_TRIGGER_CHARS = new Set(['=', '(', ',', '+', '-', '*', '/', '^', '&', '<', '>'])
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 判定是否处于「可插入引用」位置(无状态:每次按文本 + 光标重算)。
|
|
6
|
+
* `-` 的一元/二元歧义基础版不区分(`A1-` 后框选 → `A1-B2`,合法且误判无害)。
|
|
7
|
+
*/
|
|
8
|
+
export function isRefSelectContext(text: string, cursor: number): boolean {
|
|
9
|
+
if (cursor < 0 || cursor > text.length) return false
|
|
10
|
+
// 非公式编辑不进入引用选择(普通文本失焦应提交)
|
|
11
|
+
if (!text.startsWith('=')) return false
|
|
12
|
+
const before = text.slice(0, cursor).replace(/\s+$/, '')
|
|
13
|
+
if (before.length === 0) return false
|
|
14
|
+
return REF_TRIGGER_CHARS.has(before[before.length - 1]!)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InsertRefResult {
|
|
18
|
+
text: string
|
|
19
|
+
/** 插入后光标位置(落在引用文本之后) */
|
|
20
|
+
cursor: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 在光标处插入引用文本(替换空选区;有选区时覆盖选区) */
|
|
24
|
+
export function insertRefText(
|
|
25
|
+
text: string,
|
|
26
|
+
cursor: number,
|
|
27
|
+
rangeText: string,
|
|
28
|
+
selectionEnd: number = cursor
|
|
29
|
+
): InsertRefResult {
|
|
30
|
+
const start = Math.min(cursor, selectionEnd)
|
|
31
|
+
const end = Math.max(cursor, selectionEnd)
|
|
32
|
+
const next = text.slice(0, start) + rangeText + text.slice(end)
|
|
33
|
+
return { text: next, cursor: start + rangeText.length }
|
|
34
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listFormulaFunctions,
|
|
3
|
+
type FormulaFunctionMeta
|
|
4
|
+
} from '@veltra/sheet-core/core/formula/functions'
|
|
5
|
+
|
|
6
|
+
/** 补全候选上限 */
|
|
7
|
+
export const FORMULA_SUGGEST_LIMIT = 10
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 空前缀时优先展示的常用函数(其余按名称升序补齐至上限)。
|
|
11
|
+
* 纯字典序会把 SUM / OR / ROUND 挤出前 10(ABS…NOT),导致输入 `=` 看不到 SUM。
|
|
12
|
+
*/
|
|
13
|
+
export const COMMON_FORMULA_NAMES = [
|
|
14
|
+
'SUM',
|
|
15
|
+
'AVERAGE',
|
|
16
|
+
'COUNT',
|
|
17
|
+
'MAX',
|
|
18
|
+
'MIN',
|
|
19
|
+
'IF',
|
|
20
|
+
'ROUND',
|
|
21
|
+
'ABS',
|
|
22
|
+
'AND',
|
|
23
|
+
'OR'
|
|
24
|
+
] as const
|
|
25
|
+
|
|
26
|
+
/** 函数名 token 前合法的「触发」字符(= / 运算符 / ( / ,) */
|
|
27
|
+
const SUGGEST_TRIGGER_CHARS = new Set(['=', '(', ',', '+', '-', '*', '/', '^', '&', '<', '>'])
|
|
28
|
+
|
|
29
|
+
export interface FormulaSuggestItem {
|
|
30
|
+
name: string
|
|
31
|
+
params: string[]
|
|
32
|
+
description: string
|
|
33
|
+
/** 签名展示:`SUM(number1, number2, ...)`;无 params 时仅名称 */
|
|
34
|
+
signature: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FormulaSuggestContext {
|
|
38
|
+
/** 当前前缀(可能为空:紧跟 `=` 后) */
|
|
39
|
+
prefix: string
|
|
40
|
+
/** 前缀在文本中的起始下标(含) */
|
|
41
|
+
start: number
|
|
42
|
+
/** 光标位置(= 前缀结束下标) */
|
|
43
|
+
end: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 格式化函数签名;无 params 时仅返回名称 */
|
|
47
|
+
export function formatFunctionSignature(name: string, params: string[]): string {
|
|
48
|
+
if (params.length === 0) return name
|
|
49
|
+
return `${name}(${params.join(', ')})`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 判定补全上下文:光标前向匹配函数名 token(或紧跟 `=` 的空前缀),
|
|
54
|
+
* 且 token 前一个非空字符 ∈ 触发字符集。
|
|
55
|
+
* 镜像只读期由调用方跳过,本函数不做只读判断。
|
|
56
|
+
*/
|
|
57
|
+
export function getSuggestContext(text: string, cursor: number): FormulaSuggestContext | null {
|
|
58
|
+
if (cursor < 0 || cursor > text.length) return null
|
|
59
|
+
const before = text.slice(0, cursor)
|
|
60
|
+
const tokenMatch = /[A-Za-z][A-Za-z0-9_.]*$/.exec(before)
|
|
61
|
+
const prefix = tokenMatch?.[0] ?? ''
|
|
62
|
+
const start = tokenMatch ? cursor - prefix.length : cursor
|
|
63
|
+
|
|
64
|
+
// 空前缀仅在紧跟 `=` 后弹出(验证清单:输入 `=` → 候选出现)。
|
|
65
|
+
// `<=` / `>=` 的尾随 `=` 是比较运算符一部分,不是函数名起点。
|
|
66
|
+
if (!tokenMatch) {
|
|
67
|
+
const trimmed = before.replace(/\s+$/, '')
|
|
68
|
+
if (!trimmed.endsWith('=')) return null
|
|
69
|
+
if (trimmed.endsWith('<=') || trimmed.endsWith('>=')) return null
|
|
70
|
+
return { prefix: '', start, end: cursor }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const beforeToken = before.slice(0, start).replace(/\s+$/, '')
|
|
74
|
+
if (beforeToken.length === 0) return null
|
|
75
|
+
const last = beforeToken[beforeToken.length - 1]!
|
|
76
|
+
if (!SUGGEST_TRIGGER_CHARS.has(last)) return null
|
|
77
|
+
return { prefix, start, end: cursor }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 前缀过滤函数表(大小写不敏感),上限 FORMULA_SUGGEST_LIMIT。
|
|
82
|
+
* 空前缀:常用函数优先,其余名称升序补齐;有前缀:名称升序。
|
|
83
|
+
*/
|
|
84
|
+
export function filterFormulaSuggestions(prefix: string): FormulaSuggestItem[] {
|
|
85
|
+
const upper = prefix.toUpperCase()
|
|
86
|
+
const all = listFormulaFunctions()
|
|
87
|
+
if (!upper) {
|
|
88
|
+
const byName = new Map(all.map((fn) => [fn.name, fn]))
|
|
89
|
+
const items: FormulaSuggestItem[] = []
|
|
90
|
+
const used = new Set<string>()
|
|
91
|
+
for (const name of COMMON_FORMULA_NAMES) {
|
|
92
|
+
const fn = byName.get(name)
|
|
93
|
+
if (!fn) continue
|
|
94
|
+
items.push(toSuggestItem(fn))
|
|
95
|
+
used.add(name)
|
|
96
|
+
if (items.length >= FORMULA_SUGGEST_LIMIT) return items
|
|
97
|
+
}
|
|
98
|
+
for (const fn of all) {
|
|
99
|
+
if (used.has(fn.name)) continue
|
|
100
|
+
items.push(toSuggestItem(fn))
|
|
101
|
+
if (items.length >= FORMULA_SUGGEST_LIMIT) break
|
|
102
|
+
}
|
|
103
|
+
return items
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const items: FormulaSuggestItem[] = []
|
|
107
|
+
for (const fn of all) {
|
|
108
|
+
if (!fn.name.startsWith(upper)) continue
|
|
109
|
+
items.push(toSuggestItem(fn))
|
|
110
|
+
if (items.length >= FORMULA_SUGGEST_LIMIT) break
|
|
111
|
+
}
|
|
112
|
+
return items
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toSuggestItem(fn: { name: string } & FormulaFunctionMeta): FormulaSuggestItem {
|
|
116
|
+
return {
|
|
117
|
+
name: fn.name,
|
|
118
|
+
params: fn.params,
|
|
119
|
+
description: fn.description,
|
|
120
|
+
signature: formatFunctionSignature(fn.name, fn.params)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ApplySuggestResult {
|
|
125
|
+
text: string
|
|
126
|
+
/** 替换后光标位置(落在 `NAME()` 的括号内) */
|
|
127
|
+
cursor: number
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 将 [start, end) 的 token 替换为 `NAME()`,光标入括号内(免手动补右括号) */
|
|
131
|
+
export function applySuggest(
|
|
132
|
+
text: string,
|
|
133
|
+
start: number,
|
|
134
|
+
end: number,
|
|
135
|
+
name: string
|
|
136
|
+
): ApplySuggestResult {
|
|
137
|
+
const insert = `${name}()`
|
|
138
|
+
const next = text.slice(0, start) + insert + text.slice(end)
|
|
139
|
+
return { text: next, cursor: start + name.length + 1 }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 键盘导航:↑↓ 循环;返回新的高亮下标。
|
|
144
|
+
* activeIndex 越界时按 0 处理。
|
|
145
|
+
*/
|
|
146
|
+
export function moveSuggestIndex(activeIndex: number, length: number, delta: 1 | -1): number {
|
|
147
|
+
if (length <= 0) return 0
|
|
148
|
+
const cur = ((activeIndex % length) + length) % length
|
|
149
|
+
return (cur + delta + length) % length
|
|
150
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { contextmenu } from '@veltra/desktop'
|
|
2
|
+
import type { CellAddress, CellRange } from '@veltra/sheet-core/core/address'
|
|
3
|
+
import type { Sheet } from '@veltra/sheet-core/core/sheet'
|
|
4
|
+
import { SheetGrid, type SheetGridContextMenuInfo } from '@veltra/sheet-core/grid/sheet-grid'
|
|
5
|
+
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
|
6
|
+
|
|
7
|
+
import type { SheetContext } from '../tools/context'
|
|
8
|
+
import type { SheetProps } from '../types'
|
|
9
|
+
import {
|
|
10
|
+
buildBodyMenus,
|
|
11
|
+
buildColHeaderMenus,
|
|
12
|
+
buildRowHeaderMenus,
|
|
13
|
+
ensureContextMenuSelection,
|
|
14
|
+
resolveRenderSize
|
|
15
|
+
} from './sheet-context-menu'
|
|
16
|
+
|
|
17
|
+
/** 模板 ref(useTemplateRef 的只读形态) */
|
|
18
|
+
type ElRef<T> = { readonly value: T | null | undefined }
|
|
19
|
+
|
|
20
|
+
/** 公式栏暴露接口(镜像 + 引用选择) */
|
|
21
|
+
export interface FormulaBarMirror {
|
|
22
|
+
mirrorGridEdit: (addr: CellAddress) => void
|
|
23
|
+
exitMirror: (addr: CellAddress) => void
|
|
24
|
+
/** fx 编辑中且处于可插入引用位置 */
|
|
25
|
+
isRefSelecting: () => boolean
|
|
26
|
+
/** 网格 pointerdown → 挂起 blur 提交 */
|
|
27
|
+
beginBlurSuppress: () => void
|
|
28
|
+
/** 画布选区 → 插入引用文本 */
|
|
29
|
+
handleRefSelect: (range: CellRange) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface UseSheetGridOptions {
|
|
33
|
+
props: SheetProps
|
|
34
|
+
gridRef: ElRef<HTMLElement>
|
|
35
|
+
getActiveSheet: () => Sheet
|
|
36
|
+
context: SheetContext
|
|
37
|
+
formulaBarRef: ElRef<FormulaBarMirror>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 缓存实例(LRU 淘汰):每个 sheet 一个独立容器 div,非激活容器 visibility:hidden 堆叠 */
|
|
41
|
+
interface CachedGrid {
|
|
42
|
+
sheet: Sheet
|
|
43
|
+
grid: SheetGrid
|
|
44
|
+
el: HTMLDivElement
|
|
45
|
+
lastUsed: number
|
|
46
|
+
/** 释放实例:取消 structure-change 订阅 + grid.release + 移除容器 */
|
|
47
|
+
release: () => void
|
|
48
|
+
/** 隐藏期间发生 structure-change(程序化行列变更)→ 切回时需重建 */
|
|
49
|
+
dirty: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 缓存容量:最近 N 个 sheet 的 SheetGrid 实例常驻(tab 来回切换零重建)。
|
|
54
|
+
* 大文件多 sheet 场景下切换卡顿的主因是每次重建 VTable(实例创建 + scenegraph
|
|
55
|
+
* 首屏构建 ~300ms);缓存命中时仅切换容器可见性 + 选区回驱(≈0)。
|
|
56
|
+
* 每实例持有一个 canvas + scenegraph,容量即内存上限(含隐藏实例的渲染开销)。
|
|
57
|
+
*/
|
|
58
|
+
const GRID_CACHE_CAPACITY = 3
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* SheetGrid 生命周期与网格右键菜单(LRU 实例缓存):
|
|
62
|
+
* - activateGrid:tab 切换入口——命中缓存 → 显示 + 模型同步(零重建);
|
|
63
|
+
* 未命中 → 新建实例入缓存,超容量淘汰最久未用(release + 移除容器)
|
|
64
|
+
* - rebuildGrid:强制重建激活实例并清空缓存(structure-change / props 尺寸
|
|
65
|
+
* 变化 / 工作簿切换 / 导入替换——这些场景下缓存实例的 columns / 数据源失效)
|
|
66
|
+
* - 右键菜单:body / 行号 / 列头
|
|
67
|
+
* - 公式栏引用选择:interceptSelection 拦截选区回写 + pointerdown 挂起 blur
|
|
68
|
+
*/
|
|
69
|
+
export function useSheetGrid(options: UseSheetGridOptions) {
|
|
70
|
+
const { props, gridRef, getActiveSheet, context, formulaBarRef } = options
|
|
71
|
+
const cache = new Map<Sheet, CachedGrid>()
|
|
72
|
+
let active: CachedGrid | undefined
|
|
73
|
+
let seq = 0
|
|
74
|
+
let detachPointerDown: (() => void) | undefined
|
|
75
|
+
|
|
76
|
+
function handleContextMenu(info: SheetGridContextMenuInfo): void {
|
|
77
|
+
const sheet = getActiveSheet()
|
|
78
|
+
const renderSize = resolveRenderSize(props.rows, props.cols, sheet.rows, sheet.cols)
|
|
79
|
+
ensureContextMenuSelection(context, info, renderSize)
|
|
80
|
+
|
|
81
|
+
const menus =
|
|
82
|
+
info.kind === 'row-header'
|
|
83
|
+
? buildRowHeaderMenus(context)
|
|
84
|
+
: info.kind === 'col-header'
|
|
85
|
+
? buildColHeaderMenus(context)
|
|
86
|
+
: buildBodyMenus(context)
|
|
87
|
+
|
|
88
|
+
// 最小宽度兜底;实际宽度由最长菜单项撑开(勿固定 240 导致空白过大)
|
|
89
|
+
contextmenu.pop({ mousePosition: { x: info.x, y: info.y }, width: 'max-content', menus })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function bindGridPointerDown(container: HTMLElement): void {
|
|
93
|
+
detachPointerDown?.()
|
|
94
|
+
const onPointerDown = () => {
|
|
95
|
+
formulaBarRef.value?.beginBlurSuppress()
|
|
96
|
+
}
|
|
97
|
+
// capture:赶在 textarea blur 之前挂起提交
|
|
98
|
+
container.addEventListener('pointerdown', onPointerDown, true)
|
|
99
|
+
detachPointerDown = () => {
|
|
100
|
+
container.removeEventListener('pointerdown', onPointerDown, true)
|
|
101
|
+
detachPointerDown = undefined
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 新建实例:创建独立容器 div(absolute 堆叠于 grid 区内)并构造 SheetGrid */
|
|
106
|
+
function createGrid(sheet: Sheet): CachedGrid {
|
|
107
|
+
const host = gridRef.value
|
|
108
|
+
if (!host) throw new Error('useSheetGrid: grid 容器未挂载')
|
|
109
|
+
const el = document.createElement('div')
|
|
110
|
+
el.className = 'u-sheet__grid-instance'
|
|
111
|
+
host.appendChild(el)
|
|
112
|
+
const grid = new SheetGrid({
|
|
113
|
+
container: el,
|
|
114
|
+
sheet,
|
|
115
|
+
rows: props.rows,
|
|
116
|
+
cols: props.cols,
|
|
117
|
+
onContextMenu: handleContextMenu,
|
|
118
|
+
onEditStart: (addr) => formulaBarRef.value?.mirrorGridEdit(addr),
|
|
119
|
+
onEditEnd: (addr) => formulaBarRef.value?.exitMirror(addr),
|
|
120
|
+
// 引用选择:不回写模型选区,序列化为 A1 / A1:B2 交给公式栏
|
|
121
|
+
interceptSelection: () => formulaBarRef.value?.isRefSelecting() ?? false,
|
|
122
|
+
onSelectionIntercept: (range) => formulaBarRef.value?.handleRefSelect(range)
|
|
123
|
+
})
|
|
124
|
+
// 结构变更订阅(vue 层只绑定激活 sheet;隐藏实例必须自持标记,切回时判定过期)
|
|
125
|
+
let dirty = false
|
|
126
|
+
const offStructureChange = sheet.on('structure-change', () => {
|
|
127
|
+
dirty = true
|
|
128
|
+
})
|
|
129
|
+
const release = (): void => {
|
|
130
|
+
offStructureChange()
|
|
131
|
+
grid.release()
|
|
132
|
+
el.remove()
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
sheet,
|
|
136
|
+
grid,
|
|
137
|
+
el,
|
|
138
|
+
lastUsed: ++seq,
|
|
139
|
+
release,
|
|
140
|
+
get dirty() {
|
|
141
|
+
return dirty
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 切换激活实例:显示目标、隐藏其余(visibility:hidden 保持尺寸,canvas 不重建) */
|
|
147
|
+
function showOnly(target: CachedGrid): void {
|
|
148
|
+
for (const item of cache.values()) {
|
|
149
|
+
const on = item === target
|
|
150
|
+
item.el.style.visibility = on ? 'visible' : 'hidden'
|
|
151
|
+
// 隐藏实例停用视图同步(只置脏),激活实例立即同步挂起变更(#12)
|
|
152
|
+
item.grid.setVisible(on)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 淘汰最久未用的缓存实例(保留激活实例) */
|
|
157
|
+
function evict(): void {
|
|
158
|
+
while (cache.size > GRID_CACHE_CAPACITY) {
|
|
159
|
+
let oldest: CachedGrid | undefined
|
|
160
|
+
for (const item of cache.values()) {
|
|
161
|
+
if (item === active) continue
|
|
162
|
+
if (!oldest || item.lastUsed < oldest.lastUsed) oldest = item
|
|
163
|
+
}
|
|
164
|
+
if (!oldest) break
|
|
165
|
+
cache.delete(oldest.sheet)
|
|
166
|
+
oldest.release()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 清空全部缓存实例(workbook 切换 / 导入替换 / 渲染尺寸变化 / 结构变更) */
|
|
171
|
+
function invalidateAll(): void {
|
|
172
|
+
for (const item of cache.values()) {
|
|
173
|
+
item.release()
|
|
174
|
+
}
|
|
175
|
+
cache.clear()
|
|
176
|
+
active = undefined
|
|
177
|
+
detachPointerDown?.()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* tab 切换:优先复用缓存实例(零重建)。
|
|
182
|
+
* 命中时校验实例是否过期(隐藏期间程序化 structure-change——插入/删除/undo
|
|
183
|
+
* 行列——vue 层只绑定激活 sheet 的 structure-change,隐藏实例靠自持订阅标记
|
|
184
|
+
* dirty);过期则释放重建,否则同步模型状态(选区回驱 + 冻结校正;数据/样式/
|
|
185
|
+
* 行高由实例常驻的 sheet 事件订阅持续同步,隐藏期间跨表重算等模型变更不丢)。
|
|
186
|
+
*/
|
|
187
|
+
function activateGrid(): void {
|
|
188
|
+
const sheet = getActiveSheet()
|
|
189
|
+
const host = gridRef.value
|
|
190
|
+
if (!host) return
|
|
191
|
+
const cached = cache.get(sheet)
|
|
192
|
+
if (cached && !cached.dirty) {
|
|
193
|
+
cached.lastUsed = ++seq
|
|
194
|
+
cached.grid.syncFromModel()
|
|
195
|
+
showOnly(cached)
|
|
196
|
+
active = cached
|
|
197
|
+
bindGridPointerDown(cached.el)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
if (cached) {
|
|
201
|
+
// 隐藏期间结构变更:实例过期,释放后重建(走下方新建路径)
|
|
202
|
+
cache.delete(sheet)
|
|
203
|
+
cached.release()
|
|
204
|
+
}
|
|
205
|
+
const created = createGrid(sheet)
|
|
206
|
+
cache.set(sheet, created)
|
|
207
|
+
created.lastUsed = ++seq
|
|
208
|
+
showOnly(created)
|
|
209
|
+
active = created
|
|
210
|
+
bindGridPointerDown(created.el)
|
|
211
|
+
evict()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 清理已不存在的 sheet 的缓存实例(workbook 删除 sheet 后及时释放,
|
|
216
|
+
* 避免条目与事件订阅残留到容量淘汰)。
|
|
217
|
+
*/
|
|
218
|
+
function pruneCache(sheets: ReadonlyArray<Sheet>): void {
|
|
219
|
+
const alive = new Set(sheets)
|
|
220
|
+
for (const [sheet, item] of cache) {
|
|
221
|
+
// 存活或激活的实例保留;已删除的 sheet 释放(含事件订阅)
|
|
222
|
+
if (alive.has(sheet) || item === active) continue
|
|
223
|
+
cache.delete(sheet)
|
|
224
|
+
item.release()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 强制重建激活实例并清空缓存(structure-change / props / 工作簿替换) */
|
|
229
|
+
function rebuildGrid(): void {
|
|
230
|
+
invalidateAll()
|
|
231
|
+
activateGrid()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onMounted(activateGrid)
|
|
235
|
+
|
|
236
|
+
watch(() => [props.rows, props.cols], rebuildGrid)
|
|
237
|
+
|
|
238
|
+
onBeforeUnmount(() => {
|
|
239
|
+
invalidateAll()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return { rebuildGrid, activateGrid, pruneCache, getGrid: () => active?.grid }
|
|
243
|
+
}
|