@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- 下拉排列:每行一个字号;滚动用 desktop UScroll(自定义滚动条) -->
|
|
3
|
+
<u-scroll :height="280" :class="cls.e('popup-size-scroll')">
|
|
4
|
+
<div :class="cls.e('popup-size-list')">
|
|
5
|
+
<button
|
|
6
|
+
v-for="size in FONT_SIZES"
|
|
7
|
+
:key="size"
|
|
8
|
+
type="button"
|
|
9
|
+
:class="[cls.e('popup-size-item'), bem.is('active', currentSize === size)]"
|
|
10
|
+
@click="applyFontSize(size)"
|
|
11
|
+
>
|
|
12
|
+
{{ size }}
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
</u-scroll>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script lang="ts" setup>
|
|
19
|
+
import { UScroll } from '@veltra/desktop'
|
|
20
|
+
import { bem } from '@veltra/utils'
|
|
21
|
+
import { onMounted, ref } from 'vue'
|
|
22
|
+
|
|
23
|
+
import type { SheetContext } from '../../tools/context'
|
|
24
|
+
import { currentRange } from '../popup-helpers'
|
|
25
|
+
|
|
26
|
+
defineOptions({ name: 'USheetFontSizePopup' })
|
|
27
|
+
|
|
28
|
+
/** 预设字号列表(pt,与 Excel 常用档对齐) */
|
|
29
|
+
const FONT_SIZES = [9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32] as const
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 字号面板(预设列表;点击写入 font.size)。
|
|
33
|
+
* 面板打开期间的写入由 use-tool-popup 事务包裹为一个 undo 单元。
|
|
34
|
+
*/
|
|
35
|
+
const props = defineProps<{ context: SheetContext }>()
|
|
36
|
+
|
|
37
|
+
const cls = bem('sheet')
|
|
38
|
+
const currentSize = ref<number | undefined>(undefined)
|
|
39
|
+
|
|
40
|
+
onMounted(() => {
|
|
41
|
+
const active = props.context.getSelection().activeCell
|
|
42
|
+
const style = active ? props.context.getCellStyle(active) : undefined
|
|
43
|
+
currentSize.value = style?.font?.size
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function applyFontSize(size: number): void {
|
|
47
|
+
const range = currentRange(props.context)
|
|
48
|
+
if (!range) return
|
|
49
|
+
currentSize.value = size
|
|
50
|
+
props.context.setCellStyle(range, { font: { size } })
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/// <reference lib="webworker" />
|
|
2
|
+
/**
|
|
3
|
+
* XLSX 解析 Worker:hucre 解析 + 模型构建(大文件 3~4s 同步重活)
|
|
4
|
+
* 移到独立线程,主线程保持空闲(loading 动画正常、交互不冻结)。
|
|
5
|
+
*
|
|
6
|
+
* 协议:主线程 postMessage({ buffer: ArrayBuffer }) → worker 返回
|
|
7
|
+
* - { type: 'progress', done, total }(模型分片构建进度,readXlsx 同步解析段无进度)
|
|
8
|
+
* - { type: 'done', ok: true, sheets: [{ name, snapshot }], activeIndex }(快照为
|
|
9
|
+
* 纯数据,可结构化克隆);失败 { type: 'done', ok: false, error }。
|
|
10
|
+
*
|
|
11
|
+
* 名字唯一化与活动表对齐由 buildWorkbookFromHucre 完成(worker 内),主线程
|
|
12
|
+
* 直接把快照替换进目标(无 undo 历史——替换语义由确认后的 replaceWorkbook 负责)。
|
|
13
|
+
*
|
|
14
|
+
* 注意:必须用**运行时动态 import**——worker 顶层静态 import 在 vite dev 的
|
|
15
|
+
* worker 上下文中会因模块图加载顺序导致 `Workbook is not defined`(实测)。
|
|
16
|
+
*/
|
|
17
|
+
import type { SheetSnapshot } from '@veltra/sheet-core/core/sheet'
|
|
18
|
+
|
|
19
|
+
export type ImportWorkerResponse =
|
|
20
|
+
| { type: 'progress'; done: number; total: number }
|
|
21
|
+
| {
|
|
22
|
+
type: 'done'
|
|
23
|
+
ok: true
|
|
24
|
+
sheets: { name: string; snapshot: SheetSnapshot }[]
|
|
25
|
+
activeIndex: number
|
|
26
|
+
}
|
|
27
|
+
| { type: 'done'; ok: false; error: string }
|
|
28
|
+
|
|
29
|
+
self.onmessage = (e: MessageEvent<{ buffer: ArrayBuffer }>): void => {
|
|
30
|
+
const { buffer } = e.data
|
|
31
|
+
void (async () => {
|
|
32
|
+
try {
|
|
33
|
+
const [{ readXlsx }, { buildWorkbookFromHucre }] = await Promise.all([
|
|
34
|
+
import('hucre/xlsx'),
|
|
35
|
+
import('@veltra/sheet-core/core/io/import')
|
|
36
|
+
])
|
|
37
|
+
const hucreWb = await readXlsx(new Uint8Array(buffer), { readStyles: true })
|
|
38
|
+
// 分片构建:按 10% 粒度回报进度(避免 196 条/秒的消息风暴与文字跳变过快;
|
|
39
|
+
// 模型构建段约 1s,10 次更新肉眼可见数字推进)
|
|
40
|
+
let lastPercent = -1
|
|
41
|
+
const wb = buildWorkbookFromHucre(hucreWb, (done, total) => {
|
|
42
|
+
const percent = total > 0 ? Math.floor((done / total) * 10) : 10
|
|
43
|
+
if (percent !== lastPercent || done === total) {
|
|
44
|
+
lastPercent = percent
|
|
45
|
+
const progress: ImportWorkerResponse = { type: 'progress', done, total }
|
|
46
|
+
;(self as unknown as Worker).postMessage(progress)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
const sheets = wb.getSheets().map((s) => ({ name: s.name, snapshot: s.snapshot() }))
|
|
50
|
+
const response: ImportWorkerResponse = {
|
|
51
|
+
type: 'done',
|
|
52
|
+
ok: true,
|
|
53
|
+
sheets,
|
|
54
|
+
activeIndex: wb.activeSheetIndex
|
|
55
|
+
}
|
|
56
|
+
;(self as unknown as Worker).postMessage(response)
|
|
57
|
+
} catch (err: unknown) {
|
|
58
|
+
const response: ImportWorkerResponse = {
|
|
59
|
+
type: 'done',
|
|
60
|
+
ok: false,
|
|
61
|
+
error: err instanceof Error ? err.message : String(err)
|
|
62
|
+
}
|
|
63
|
+
;(self as unknown as Worker).postMessage(response)
|
|
64
|
+
}
|
|
65
|
+
})()
|
|
66
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<u-file-picker
|
|
3
|
+
:accept="INSERT_IMAGE_ACCEPT"
|
|
4
|
+
:class="cls.e('insert-image-picker')"
|
|
5
|
+
@pick="handlePick"
|
|
6
|
+
>
|
|
7
|
+
<div :class="cls.e('insert-image-hint')">
|
|
8
|
+
选择图片文件
|
|
9
|
+
<div :class="cls.e('insert-image-sub')">支持 png / jpeg / gif / svg / webp</div>
|
|
10
|
+
</div>
|
|
11
|
+
</u-file-picker>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script lang="ts" setup>
|
|
15
|
+
import { UFilePicker } from '@veltra/desktop'
|
|
16
|
+
import { bem } from '@veltra/utils'
|
|
17
|
+
|
|
18
|
+
import type { SheetContext } from '../../tools/context'
|
|
19
|
+
import { INSERT_IMAGE_ACCEPT, insertImageFromFile } from '../insert-image'
|
|
20
|
+
|
|
21
|
+
defineOptions({ name: 'USheetInsertImagePopup' })
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 插入图片面板(UFilePicker):选文件后走共享 insertImageFromFile,锚定当前选区活动格。
|
|
25
|
+
* 不参与面板事务(单次插入 = 一个 undo 单元)。
|
|
26
|
+
*/
|
|
27
|
+
const props = defineProps<{ context: SheetContext }>()
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{ close: [] }>()
|
|
30
|
+
|
|
31
|
+
const cls = bem('sheet')
|
|
32
|
+
|
|
33
|
+
function handlePick(files: File[]): void {
|
|
34
|
+
const file = files[0]
|
|
35
|
+
if (!file) return
|
|
36
|
+
emit('close')
|
|
37
|
+
void insertImageFromFile(props.context, file)
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { ContextmenuItem } from '@veltra/desktop'
|
|
2
|
+
import { createRange, type CellAddress, type CellRange } from '@veltra/sheet-core/core/address'
|
|
3
|
+
import { defineComponent, h } from 'vue'
|
|
4
|
+
|
|
5
|
+
import type { SheetContext } from '../tools/context'
|
|
6
|
+
import { defaultToolRegistry } from '../tools/registry'
|
|
7
|
+
import InsertCountMenuItem from './insert-count-menu-item.vue'
|
|
8
|
+
import { pickAndInsertImage } from './insert-image'
|
|
9
|
+
|
|
10
|
+
/** 插入数量钳制范围(对齐 univer) */
|
|
11
|
+
export const INSERT_COUNT_MIN = 1
|
|
12
|
+
export const INSERT_COUNT_MAX = 1000
|
|
13
|
+
|
|
14
|
+
/** 选区覆盖行/列数 → 插入默认 N(min 1 / max 1000) */
|
|
15
|
+
export function defaultInsertCount(range: CellRange | null, axis: 'row' | 'col'): number {
|
|
16
|
+
if (!range) return INSERT_COUNT_MIN
|
|
17
|
+
const n =
|
|
18
|
+
axis === 'row' ? range.end.row - range.start.row + 1 : range.end.col - range.start.col + 1
|
|
19
|
+
return Math.min(INSERT_COUNT_MAX, Math.max(INSERT_COUNT_MIN, n))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 点击行是否落在任一选区内(按行号覆盖) */
|
|
23
|
+
export function isRowInSelection(ranges: CellRange[], row: number): boolean {
|
|
24
|
+
return ranges.some((r) => r.start.row <= row && row <= r.end.row)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 点击列是否落在任一选区内(按列号覆盖) */
|
|
28
|
+
export function isColInSelection(ranges: CellRange[], col: number): boolean {
|
|
29
|
+
return ranges.some((r) => r.start.col <= col && col <= r.end.col)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 整行选区(列跨渲染宽度) */
|
|
33
|
+
export function wholeRowRange(row: number, colCount: number): CellRange {
|
|
34
|
+
return createRange({ row, col: 0 }, { row, col: Math.max(colCount - 1, 0) })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 整列选区(行跨渲染高度) */
|
|
38
|
+
export function wholeColRange(col: number, rowCount: number): CellRange {
|
|
39
|
+
return createRange({ row: 0, col }, { row: Math.max(rowCount - 1, 0), col })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 渲染尺寸 = max(props, sheet.rows/cols),与 SheetGrid 一致 */
|
|
43
|
+
export function resolveRenderSize(
|
|
44
|
+
propsRows: number | undefined,
|
|
45
|
+
propsCols: number | undefined,
|
|
46
|
+
sheetRows: number,
|
|
47
|
+
sheetCols: number
|
|
48
|
+
): { rows: number; cols: number } {
|
|
49
|
+
return { rows: Math.max(propsRows ?? 100, sheetRows), cols: Math.max(propsCols ?? 26, sheetCols) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function insertCountRender(options: {
|
|
53
|
+
prefix: string
|
|
54
|
+
suffix: string
|
|
55
|
+
defaultValue: number
|
|
56
|
+
onConfirm: (n: number) => void
|
|
57
|
+
}): ContextmenuItem['render'] {
|
|
58
|
+
return defineComponent({
|
|
59
|
+
name: 'SheetInsertCountMenuRender',
|
|
60
|
+
setup() {
|
|
61
|
+
return () =>
|
|
62
|
+
h(InsertCountMenuItem, {
|
|
63
|
+
prefix: options.prefix,
|
|
64
|
+
suffix: options.suffix,
|
|
65
|
+
defaultValue: options.defaultValue,
|
|
66
|
+
min: INSERT_COUNT_MIN,
|
|
67
|
+
max: INSERT_COUNT_MAX,
|
|
68
|
+
onConfirm: options.onConfirm
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function primaryRange(ctx: SheetContext): CellRange | null {
|
|
75
|
+
return ctx.getSelection().ranges[0] ?? null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 行号右键菜单:插入×2 / 删除 / divider / 冻结 / 取消冻结 */
|
|
79
|
+
export function buildRowHeaderMenus(ctx: SheetContext): ContextmenuItem[] {
|
|
80
|
+
const range = primaryRange(ctx)
|
|
81
|
+
const startRow = range?.start.row ?? 0
|
|
82
|
+
const endRow = range?.end.row ?? startRow
|
|
83
|
+
const rowCount = defaultInsertCount(range, 'row')
|
|
84
|
+
const deleteCount = range ? endRow - startRow + 1 : 1
|
|
85
|
+
const frozen = ctx.frozen
|
|
86
|
+
const freezeActive = frozen.rows === endRow + 1
|
|
87
|
+
|
|
88
|
+
return [
|
|
89
|
+
{
|
|
90
|
+
label: '在上方插入行',
|
|
91
|
+
keepOpen: true,
|
|
92
|
+
render: insertCountRender({
|
|
93
|
+
prefix: '在上方插入',
|
|
94
|
+
suffix: '行',
|
|
95
|
+
defaultValue: rowCount,
|
|
96
|
+
onConfirm: (n) => ctx.insertRows(startRow, n)
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: '在下方插入行',
|
|
101
|
+
keepOpen: true,
|
|
102
|
+
render: insertCountRender({
|
|
103
|
+
prefix: '在下方插入',
|
|
104
|
+
suffix: '行',
|
|
105
|
+
defaultValue: rowCount,
|
|
106
|
+
onConfirm: (n) => ctx.insertRows(endRow + 1, n)
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
{ label: '删除行', callback: () => ctx.deleteRows(startRow, deleteCount) },
|
|
110
|
+
{ divider: true },
|
|
111
|
+
{
|
|
112
|
+
label: freezeActive ? '✓ 冻结到当前行' : '冻结到当前行',
|
|
113
|
+
callback: () => ctx.setFrozen(endRow + 1, frozen.cols)
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
label: '取消冻结',
|
|
117
|
+
disabled: frozen.rows === 0 && frozen.cols === 0,
|
|
118
|
+
callback: () => ctx.setFrozen(0, 0)
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 列头右键菜单:插入×2 / 删除 / divider / 冻结 / 取消冻结 */
|
|
124
|
+
export function buildColHeaderMenus(ctx: SheetContext): ContextmenuItem[] {
|
|
125
|
+
const range = primaryRange(ctx)
|
|
126
|
+
const startCol = range?.start.col ?? 0
|
|
127
|
+
const endCol = range?.end.col ?? startCol
|
|
128
|
+
const colCount = defaultInsertCount(range, 'col')
|
|
129
|
+
const deleteCount = range ? endCol - startCol + 1 : 1
|
|
130
|
+
const frozen = ctx.frozen
|
|
131
|
+
const freezeActive = frozen.cols === endCol + 1
|
|
132
|
+
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
label: '在左侧插入列',
|
|
136
|
+
keepOpen: true,
|
|
137
|
+
render: insertCountRender({
|
|
138
|
+
prefix: '在左侧插入',
|
|
139
|
+
suffix: '列',
|
|
140
|
+
defaultValue: colCount,
|
|
141
|
+
onConfirm: (n) => ctx.insertCols(startCol, n)
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
label: '在右侧插入列',
|
|
146
|
+
keepOpen: true,
|
|
147
|
+
render: insertCountRender({
|
|
148
|
+
prefix: '在右侧插入',
|
|
149
|
+
suffix: '列',
|
|
150
|
+
defaultValue: colCount,
|
|
151
|
+
onConfirm: (n) => ctx.insertCols(endCol + 1, n)
|
|
152
|
+
})
|
|
153
|
+
},
|
|
154
|
+
{ label: '删除列', callback: () => ctx.deleteCols(startCol, deleteCount) },
|
|
155
|
+
{ divider: true },
|
|
156
|
+
{
|
|
157
|
+
label: freezeActive ? '✓ 冻结到当前列' : '冻结到当前列',
|
|
158
|
+
callback: () => ctx.setFrozen(frozen.rows, endCol + 1)
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
label: '取消冻结',
|
|
162
|
+
disabled: frozen.rows === 0 && frozen.cols === 0,
|
|
163
|
+
callback: () => ctx.setFrozen(0, 0)
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** body 格右键菜单:合并/取消合并 + 插入图片(行列插入/删除仅行号/列头菜单) */
|
|
169
|
+
export function buildBodyMenus(ctx: SheetContext): ContextmenuItem[] {
|
|
170
|
+
const mergeTool = defaultToolRegistry.get('merge')
|
|
171
|
+
const unmergeTool = defaultToolRegistry.get('unmerge')
|
|
172
|
+
const active = ctx.getSelection().activeCell
|
|
173
|
+
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
label: '合并单元格',
|
|
177
|
+
disabled: mergeTool?.disabled?.(ctx) ?? true,
|
|
178
|
+
callback: () => mergeTool?.onClick(ctx)
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
label: '取消合并单元格',
|
|
182
|
+
disabled: unmergeTool?.disabled?.(ctx) ?? true,
|
|
183
|
+
callback: () => unmergeTool?.onClick(ctx)
|
|
184
|
+
},
|
|
185
|
+
{ label: '插入图片', disabled: !active, callback: () => pickAndInsertImage(ctx) }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 右键落点预处理:body 格落在选区外 → 选中该格;
|
|
191
|
+
* 行号/列头落在选区外 → 选中整行/整列(渲染尺寸)。
|
|
192
|
+
*/
|
|
193
|
+
export function ensureContextMenuSelection(
|
|
194
|
+
ctx: SheetContext,
|
|
195
|
+
info: {
|
|
196
|
+
kind: 'body' | 'row-header' | 'col-header'
|
|
197
|
+
addr: CellAddress | null
|
|
198
|
+
row?: number
|
|
199
|
+
col?: number
|
|
200
|
+
},
|
|
201
|
+
renderSize: { rows: number; cols: number }
|
|
202
|
+
): void {
|
|
203
|
+
const ranges = ctx.getSelection().ranges
|
|
204
|
+
|
|
205
|
+
if (info.kind === 'body' && info.addr) {
|
|
206
|
+
const inSelection = ranges.some(
|
|
207
|
+
(range) =>
|
|
208
|
+
range.start.row <= info.addr!.row &&
|
|
209
|
+
info.addr!.row <= range.end.row &&
|
|
210
|
+
range.start.col <= info.addr!.col &&
|
|
211
|
+
info.addr!.col <= range.end.col
|
|
212
|
+
)
|
|
213
|
+
if (!inSelection) ctx.selectCell(info.addr)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (info.kind === 'row-header' && info.row != null) {
|
|
218
|
+
if (!isRowInSelection(ranges, info.row)) {
|
|
219
|
+
ctx.selectRange(wholeRowRange(info.row, renderSize.cols))
|
|
220
|
+
}
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (info.kind === 'col-header' && info.col != null) {
|
|
225
|
+
if (!isColInSelection(ranges, info.col)) {
|
|
226
|
+
ctx.selectRange(wholeColRange(info.col, renderSize.rows))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cls.e('tabs')">
|
|
3
|
+
<button
|
|
4
|
+
v-show="showNav"
|
|
5
|
+
type="button"
|
|
6
|
+
:class="cls.e('tabs-nav')"
|
|
7
|
+
:disabled="!canPrev"
|
|
8
|
+
title="向左滚动"
|
|
9
|
+
@click="scrollByStep(-1)"
|
|
10
|
+
>
|
|
11
|
+
<u-icon><ArrowLeft /></u-icon>
|
|
12
|
+
</button>
|
|
13
|
+
<div ref="tabsViewportRef" :class="cls.e('tabs-viewport')">
|
|
14
|
+
<div ref="tabsListRef" :class="cls.e('tabs-list')">
|
|
15
|
+
<div
|
|
16
|
+
v-for="(sheet, index) in sheetList"
|
|
17
|
+
:key="sheet"
|
|
18
|
+
:class="[cls.e('tab'), bem.is('active', index === activeIndex)]"
|
|
19
|
+
title="右键重命名 / 删除"
|
|
20
|
+
@click="handleTabClick(index)"
|
|
21
|
+
@contextmenu.prevent="handleTabContextMenu($event, sheet, index)"
|
|
22
|
+
>
|
|
23
|
+
<input
|
|
24
|
+
v-if="renamingIndex === index"
|
|
25
|
+
ref="renameInputRef"
|
|
26
|
+
v-model="renameDraft"
|
|
27
|
+
:class="cls.e('tab-rename-input')"
|
|
28
|
+
:maxlength="31"
|
|
29
|
+
@click.stop
|
|
30
|
+
@keydown.enter.prevent="commitRename"
|
|
31
|
+
@keydown.esc.prevent="cancelRename"
|
|
32
|
+
@blur="commitRename"
|
|
33
|
+
/>
|
|
34
|
+
<span v-else>{{ sheet.name }}</span>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<button
|
|
39
|
+
v-show="showNav"
|
|
40
|
+
type="button"
|
|
41
|
+
:class="cls.e('tabs-nav')"
|
|
42
|
+
:disabled="!canNext"
|
|
43
|
+
title="向右滚动"
|
|
44
|
+
@click="scrollByStep(1)"
|
|
45
|
+
>
|
|
46
|
+
<u-icon><ArrowRight /></u-icon>
|
|
47
|
+
</button>
|
|
48
|
+
<button type="button" :class="cls.e('tab-add')" title="添加工作表" @click="handleAddSheet">
|
|
49
|
+
+
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script lang="ts" setup>
|
|
55
|
+
import { contextmenu, message, messageConfirm, UIcon } from '@veltra/desktop'
|
|
56
|
+
import { ArrowLeft, ArrowRight } from '@veltra/icons/normal'
|
|
57
|
+
import type { Sheet } from '@veltra/sheet-core/core/sheet'
|
|
58
|
+
import type { Workbook } from '@veltra/sheet-core/core/workbook'
|
|
59
|
+
import { bem } from '@veltra/utils'
|
|
60
|
+
import { nextTick, ref, toRef, useTemplateRef } from 'vue'
|
|
61
|
+
|
|
62
|
+
import { useSheetTabsBar } from './use-sheet-tabs-bar'
|
|
63
|
+
|
|
64
|
+
defineOptions({ name: 'USheetTabs' })
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 底部 sheet tabs(可滚动视口 + 左右导航;「+」钉在视口外):
|
|
68
|
+
* - tab 点击切换;末尾「+」addSheet + 自动激活新表(激活事件驱动 grid 重建)
|
|
69
|
+
* - tab 右键菜单「重命名 / 删除」:重命名进入行内输入(Enter 提交、Esc 取消、失焦提交;
|
|
70
|
+
* 冲突提示且不写入),删除二次确认(最后一个 sheet 的删除项禁用)
|
|
71
|
+
* - sheet 增删改名是工作簿级结构操作(不走 undo),直接调用 Workbook——
|
|
72
|
+
* 不经 SheetContext(见 AGENTS.md 门面边界);UI 刷新由 workbook 事件驱动
|
|
73
|
+
*/
|
|
74
|
+
const props = defineProps<{ workbook: Workbook; sheetList: Sheet[]; activeIndex: number }>()
|
|
75
|
+
|
|
76
|
+
const cls = bem('sheet')
|
|
77
|
+
|
|
78
|
+
// ─── 可滚动视口(导航显隐 / 步进滚动 / 活动 tab 滚入视野)────────
|
|
79
|
+
|
|
80
|
+
const tabsViewportRef = useTemplateRef<HTMLElement>('tabsViewportRef')
|
|
81
|
+
const tabsListRef = useTemplateRef<HTMLElement>('tabsListRef')
|
|
82
|
+
const { showNav, canPrev, canNext, scrollByStep } = useSheetTabsBar({
|
|
83
|
+
viewportRef: tabsViewportRef,
|
|
84
|
+
listRef: tabsListRef,
|
|
85
|
+
sheetList: toRef(props, 'sheetList'),
|
|
86
|
+
activeIndex: toRef(props, 'activeIndex')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
function handleTabClick(index: number): void {
|
|
90
|
+
if (index === props.activeIndex) return
|
|
91
|
+
const sheet = props.sheetList[index]
|
|
92
|
+
if (sheet) props.workbook.activateSheet(sheet.name)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 「+」添加 sheet:自动激活新表(激活事件驱动 grid 重建) */
|
|
96
|
+
function handleAddSheet(): void {
|
|
97
|
+
const sheet = props.workbook.addSheet()
|
|
98
|
+
props.workbook.activateSheet(sheet.name)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── tab 右键菜单(重命名 / 删除)────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** 正在行内重命名的 tab 下标(-1 = 无) */
|
|
104
|
+
const renamingIndex = ref(-1)
|
|
105
|
+
const renameDraft = ref('')
|
|
106
|
+
// 输入框在 v-for 内:useTemplateRef 收集为数组(Vue 3.5 v-for ref 语义)
|
|
107
|
+
const renameInputRef = useTemplateRef<HTMLInputElement[]>('renameInputRef')
|
|
108
|
+
|
|
109
|
+
function handleTabContextMenu(event: MouseEvent, sheet: Sheet, index: number): void {
|
|
110
|
+
// 右键菜单操作的是该 tab,不切换激活(与 Excel 一致:菜单动作不影响激活)
|
|
111
|
+
contextmenu.pop({
|
|
112
|
+
mousePosition: { x: event.clientX, y: event.clientY },
|
|
113
|
+
width: 160,
|
|
114
|
+
menus: [
|
|
115
|
+
{ label: '重命名', callback: () => startRename(index) },
|
|
116
|
+
{
|
|
117
|
+
label: '删除',
|
|
118
|
+
disabled: props.sheetList.length <= 1,
|
|
119
|
+
callback: () => confirmRemoveSheet(sheet)
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 进入行内重命名(预填当前名并全选) */
|
|
126
|
+
function startRename(index: number): void {
|
|
127
|
+
renamingIndex.value = index
|
|
128
|
+
renameDraft.value = props.sheetList[index]?.name ?? ''
|
|
129
|
+
// 等输入框渲染后聚焦全选
|
|
130
|
+
nextTick(() => renameInputRef.value?.[0]?.select())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 提交重命名:空名 / 重名(含大小写变体)由 Workbook 拒绝,提示且不写入 */
|
|
134
|
+
function commitRename(): void {
|
|
135
|
+
const index = renamingIndex.value
|
|
136
|
+
if (index < 0) return
|
|
137
|
+
renamingIndex.value = -1
|
|
138
|
+
const sheet = props.sheetList[index]
|
|
139
|
+
if (!sheet) return
|
|
140
|
+
const next = renameDraft.value.trim()
|
|
141
|
+
if (next === '' || next === sheet.name) return
|
|
142
|
+
if (!props.workbook.renameSheet(sheet.name, next)) {
|
|
143
|
+
message.warn(`无法重命名:名称“${next}”无效或已被占用`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function cancelRename(): void {
|
|
148
|
+
renamingIndex.value = -1
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 删除确认(message-confirm);最后一个 sheet 在菜单层已禁用,这里再兜底 */
|
|
152
|
+
function confirmRemoveSheet(sheet: Sheet): void {
|
|
153
|
+
if (props.workbook.sheetCount <= 1) return
|
|
154
|
+
messageConfirm.danger(`确定删除工作表“${sheet.name}”吗?删除后不可恢复。`, {
|
|
155
|
+
confirmButtonText: '删除',
|
|
156
|
+
onClosed: (action) => {
|
|
157
|
+
if (action !== 'confirm') return
|
|
158
|
+
props.workbook.removeSheet(sheet.name)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cls.e('toolbar')">
|
|
3
|
+
<button
|
|
4
|
+
v-if="showNav"
|
|
5
|
+
type="button"
|
|
6
|
+
:class="cls.e('toolbar-nav')"
|
|
7
|
+
:disabled="!canPrev"
|
|
8
|
+
:aria-label="'向左滚动工具栏'"
|
|
9
|
+
@click="scrollByStep(-1)"
|
|
10
|
+
>
|
|
11
|
+
<u-icon><ArrowLeft /></u-icon>
|
|
12
|
+
</button>
|
|
13
|
+
|
|
14
|
+
<!-- 单行滚动视口:内容超出时由左右箭头导航(use-toolbar-scroll),不再换行 -->
|
|
15
|
+
<div ref="scrollRef" :class="cls.e('toolbar-scroll')">
|
|
16
|
+
<div ref="listRef" :class="cls.e('toolbar-list')">
|
|
17
|
+
<template v-for="(group, groupIndex) in groups" :key="group.name">
|
|
18
|
+
<span v-if="groupIndex > 0" :class="cls.e('toolbar-divider')" />
|
|
19
|
+
<u-tip
|
|
20
|
+
v-for="item in group.tools"
|
|
21
|
+
:key="item.tool.id"
|
|
22
|
+
:content="item.tool.tooltip ?? item.tool.title"
|
|
23
|
+
trigger="hover"
|
|
24
|
+
direction="top"
|
|
25
|
+
alignment="center"
|
|
26
|
+
:show-delay="400"
|
|
27
|
+
:disabled="item.disabled"
|
|
28
|
+
>
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
:class="[cls.e('tool'), bem.is('active', item.active)]"
|
|
32
|
+
:data-tool-id="item.tool.id"
|
|
33
|
+
:disabled="item.disabled"
|
|
34
|
+
:title="item.tool.tooltip ?? item.tool.title"
|
|
35
|
+
@click="emit('toolClick', item.tool, $event)"
|
|
36
|
+
>
|
|
37
|
+
<component :is="item.tool.icon" v-if="item.tool.icon" :class="cls.e('tool-icon')" />
|
|
38
|
+
<span v-else>{{ item.tool.title }}</span>
|
|
39
|
+
</button>
|
|
40
|
+
</u-tip>
|
|
41
|
+
</template>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<button
|
|
46
|
+
v-if="showNav"
|
|
47
|
+
type="button"
|
|
48
|
+
:class="cls.e('toolbar-nav')"
|
|
49
|
+
:disabled="!canNext"
|
|
50
|
+
:aria-label="'向右滚动工具栏'"
|
|
51
|
+
@click="scrollByStep(1)"
|
|
52
|
+
>
|
|
53
|
+
<u-icon><ArrowRight /></u-icon>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script lang="ts" setup>
|
|
59
|
+
import { UIcon } from '@veltra/desktop'
|
|
60
|
+
import { ArrowLeft, ArrowRight } from '@veltra/icons/normal'
|
|
61
|
+
import { bem } from '@veltra/utils'
|
|
62
|
+
import { toRef, useTemplateRef } from 'vue'
|
|
63
|
+
|
|
64
|
+
import type { SheetTool } from '../tools/registry'
|
|
65
|
+
import type { ToolGroupView } from '../use-tool-groups'
|
|
66
|
+
import { useToolbarScroll } from './use-toolbar-scroll'
|
|
67
|
+
|
|
68
|
+
defineOptions({ name: 'USheetToolbar' })
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 工具栏(渲染 defaultToolRegistry 分组工具 + 分隔符,单行溢出滚动)。
|
|
72
|
+
* 纯展示:有 icon 时只渲染图标(title/tooltip 经 UTip + 原生 title 兜底);
|
|
73
|
+
* 无 icon 时回落文字按钮(自定义工具兼容)。
|
|
74
|
+
* 内容超出视口时显示左右箭头(点击步进滚动、滚轮横滚),不换行挤压 grid。
|
|
75
|
+
*/
|
|
76
|
+
const props = defineProps<{ groups: ToolGroupView[] }>()
|
|
77
|
+
|
|
78
|
+
const emit = defineEmits<{ toolClick: [tool: SheetTool, event: MouseEvent] }>()
|
|
79
|
+
|
|
80
|
+
const cls = bem('sheet')
|
|
81
|
+
|
|
82
|
+
// ─── 溢出滚动(箭头显隐 / 步进 / 滚轮横滚)────────────────────
|
|
83
|
+
|
|
84
|
+
const scrollRef = useTemplateRef<HTMLElement>('scrollRef')
|
|
85
|
+
const listRef = useTemplateRef<HTMLElement>('listRef')
|
|
86
|
+
const { showNav, canPrev, canNext, scrollByStep } = useToolbarScroll({
|
|
87
|
+
viewportRef: scrollRef,
|
|
88
|
+
listRef,
|
|
89
|
+
content: toRef(props, 'groups')
|
|
90
|
+
})
|
|
91
|
+
</script>
|