@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,140 @@
1
+ import { message, messageConfirm } from "@veltra/desktop";
2
+ import { nextFrame } from "@veltra/utils";
3
+ import { importCsv, importXlsx, replaceWorkbookWithSnapshots } from "@veltra/sheet-core/core/io/import";
4
+ //#region src/vue/import-file.ts
5
+ /** 导入文件 accept(工具栏直接选文件) */
6
+ const IMPORT_FILE_ACCEPT = ".xlsx,.csv";
7
+ /**
8
+ * xlsx 解析(worker 优先):importXlsx 是同步重活,主线程直接跑会冻结 UI。
9
+ * worker 返回纯数据快照;不可用时回退主线程解析。
10
+ */
11
+ async function parseXlsxAsync(buffer, parseProgress) {
12
+ let worker;
13
+ try {
14
+ const url = import.meta.env?.DEV ? new URL("./popups/import.worker.ts", import.meta.url) : new URL("./popups/import.worker.js", import.meta.url);
15
+ worker = new Worker(url, { type: "module" });
16
+ } catch {
17
+ return toSnapshotArray(await importXlsx(new Uint8Array(buffer)));
18
+ }
19
+ return await new Promise((resolve, reject) => {
20
+ let settled = false;
21
+ const fallback = () => {
22
+ if (settled) return;
23
+ settled = true;
24
+ worker.terminate();
25
+ importXlsx(new Uint8Array(buffer)).then((wb) => resolve(toSnapshotArray(wb))).catch((err) => reject(err));
26
+ };
27
+ worker.onmessage = (e) => {
28
+ const data = e.data;
29
+ if (data.type === "progress") {
30
+ if (parseProgress) parseProgress.value = {
31
+ done: data.done,
32
+ total: data.total
33
+ };
34
+ return;
35
+ }
36
+ if (settled) return;
37
+ settled = true;
38
+ if (!data.ok) {
39
+ worker.terminate();
40
+ reject(new Error(data.error ?? "worker 解析失败"));
41
+ return;
42
+ }
43
+ worker.terminate();
44
+ resolve({
45
+ sheets: data.sheets ?? [],
46
+ activeIndex: data.activeIndex ?? 0
47
+ });
48
+ };
49
+ worker.onerror = () => fallback();
50
+ const transfer = buffer.slice(0);
51
+ worker.postMessage({ buffer: transfer }, [transfer]);
52
+ });
53
+ }
54
+ function toSnapshotArray(wb) {
55
+ return {
56
+ sheets: wb.getSheets().map((s) => ({
57
+ name: s.name,
58
+ snapshot: s.snapshot()
59
+ })),
60
+ activeIndex: wb.activeSheetIndex
61
+ };
62
+ }
63
+ /**
64
+ * 从本地 File 导入:
65
+ * - .csv → importCsv 写入当前活动表
66
+ * - .xlsx → 解析后经 messageConfirm 确认再 replaceWorkbookWithSnapshots
67
+ */
68
+ function importFromFile(file, options) {
69
+ const { workbook, activeSheet, parsing, parseProgress, onCsvImported, onWorkbookReplaced } = options;
70
+ if (file.name.toLowerCase().endsWith(".csv")) {
71
+ file.text().then((text) => {
72
+ importCsv(text, activeSheet);
73
+ onCsvImported?.();
74
+ message.success(`已从 ${file.name} 导入到工作表「${activeSheet.name}」`);
75
+ });
76
+ return;
77
+ }
78
+ file.arrayBuffer().then((buffer) => {
79
+ if (parsing) parsing.value = true;
80
+ if (parseProgress) parseProgress.value = {
81
+ done: 0,
82
+ total: 0
83
+ };
84
+ parseXlsxAsync(buffer, parseProgress).then((imported) => {
85
+ if (parsing) parsing.value = false;
86
+ messageConfirm.danger(`导入将替换当前工作簿(共 ${imported.sheets.length} 个工作表),确定吗?`, {
87
+ confirmButtonText: "导入",
88
+ onClosed: (action) => {
89
+ if (action !== "confirm") return;
90
+ const loading = message({
91
+ message: "正在导入…",
92
+ duration: 0
93
+ });
94
+ try {
95
+ replaceWorkbookWithSnapshots(workbook, imported.sheets, imported.activeIndex);
96
+ onWorkbookReplaced?.();
97
+ nextFrame(() => {
98
+ message.success("导入完成");
99
+ loading.close();
100
+ });
101
+ } catch (err) {
102
+ console.error("[sheet] 导入失败:", err);
103
+ message.error(`导入失败:${err instanceof Error ? err.message : String(err)}`);
104
+ loading.close();
105
+ }
106
+ }
107
+ });
108
+ }).catch((err) => {
109
+ if (parsing) parsing.value = false;
110
+ message.error(`文件解析失败:${err instanceof Error ? err.message : String(err)}`);
111
+ });
112
+ });
113
+ }
114
+ /**
115
+ * 拉起系统文件选择框(工具栏导入用;不用 UFilePicker——无编程式打开 API)。
116
+ * 选中文件后走 importFromFile。
117
+ */
118
+ function pickAndImportFile(options) {
119
+ const input = document.createElement("input");
120
+ input.type = "file";
121
+ input.accept = IMPORT_FILE_ACCEPT;
122
+ input.hidden = true;
123
+ const cleanup = () => {
124
+ input.remove();
125
+ };
126
+ input.addEventListener("change", () => {
127
+ const file = input.files?.[0];
128
+ cleanup();
129
+ if (file) importFromFile(file, options);
130
+ });
131
+ window.addEventListener("focus", () => {
132
+ setTimeout(() => {
133
+ if (document.body.contains(input) && !input.files?.length) cleanup();
134
+ }, 300);
135
+ }, { once: true });
136
+ document.body.appendChild(input);
137
+ input.click();
138
+ }
139
+ //#endregion
140
+ export { IMPORT_FILE_ACCEPT, importFromFile, pickAndImportFile };
@@ -0,0 +1,68 @@
1
+ import { createElementBlock, createElementVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, toDisplayString, unref, withKeys, withModifiers } from "vue";
2
+ import { ContextmenuRootDIKey, UNumberInput } from "@veltra/desktop";
3
+ import { bem } from "@veltra/utils";
4
+ //#region src/vue/insert-count-menu-item.vue
5
+ const _hoisted_1 = ["onKeydownCapture"];
6
+ const _sfc_main = /*@__PURE__*/ defineComponent({
7
+ name: "USheetInsertCountMenuItem",
8
+ __name: "insert-count-menu-item",
9
+ props: {
10
+ prefix: {},
11
+ suffix: {},
12
+ defaultValue: { default: 1 },
13
+ min: { default: 1 },
14
+ max: { default: 1e3 }
15
+ },
16
+ emits: ["confirm"],
17
+ setup(__props, { emit: __emit }) {
18
+ /**
19
+ * 右键菜单内嵌数量输入项:文案 + UNumberInput。
20
+ * 点击菜单项本体按当前值执行;输入框区域点击不冒泡。
21
+ * Enter 提交、Esc 关闭;提交/取消均经 ContextmenuRootDIKey 关闭菜单。
22
+ * Enter/Esc 使用 capture:UNumberInput 内部 `@keydown.stop`,冒泡阶段收不到。
23
+ */
24
+ const props = __props;
25
+ const emit = __emit;
26
+ const cls = bem("sheet");
27
+ const root = inject(ContextmenuRootDIKey, null);
28
+ const count = ref(props.defaultValue);
29
+ function clamp(n) {
30
+ return Math.min(props.max, Math.max(props.min, Math.floor(n) || props.min));
31
+ }
32
+ function close() {
33
+ root?.onItemClickEnd();
34
+ }
35
+ function confirm() {
36
+ emit("confirm", clamp(count.value ?? props.defaultValue));
37
+ close();
38
+ }
39
+ return (_ctx, _cache) => {
40
+ return openBlock(), createElementBlock("div", {
41
+ class: normalizeClass(unref(cls).e("insert-count-menu")),
42
+ onClick: confirm,
43
+ onKeydownCapture: [withKeys(withModifiers(confirm, ["prevent"]), ["enter"]), withKeys(withModifiers(close, ["prevent"]), ["escape"])]
44
+ }, [
45
+ createElementVNode("span", { class: normalizeClass(unref(cls).e("insert-count-prefix")) }, toDisplayString(__props.prefix), 3),
46
+ createVNode(unref(UNumberInput), {
47
+ modelValue: count.value,
48
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => count.value = $event),
49
+ size: "small",
50
+ min: __props.min,
51
+ max: __props.max,
52
+ step: 1,
53
+ clearable: false,
54
+ class: normalizeClass(unref(cls).e("insert-count-input")),
55
+ onClick: _cache[1] || (_cache[1] = withModifiers(() => {}, ["stop"]))
56
+ }, null, 8, [
57
+ "modelValue",
58
+ "min",
59
+ "max",
60
+ "class"
61
+ ]),
62
+ createElementVNode("span", { class: normalizeClass(unref(cls).e("insert-count-suffix")) }, toDisplayString(__props.suffix), 3)
63
+ ], 42, _hoisted_1);
64
+ };
65
+ }
66
+ });
67
+ //#endregion
68
+ export { _sfc_main as default };
@@ -0,0 +1,79 @@
1
+ import { message } from "@veltra/desktop";
2
+ //#region src/vue/insert-image.ts
3
+ /** 文件选择 accept(工具栏弹层与右键菜单共用;不用裸 image/*) */
4
+ const INSERT_IMAGE_ACCEPT = ".png,.jpg,.jpeg,.gif,.svg,.webp";
5
+ const MIME_TO_TYPE = {
6
+ "image/png": "png",
7
+ "image/jpeg": "jpeg",
8
+ "image/jpg": "jpeg",
9
+ "image/gif": "gif",
10
+ "image/svg+xml": "svg",
11
+ "image/webp": "webp"
12
+ };
13
+ const EXT_TO_TYPE = {
14
+ png: "png",
15
+ jpg: "jpeg",
16
+ jpeg: "jpeg",
17
+ gif: "gif",
18
+ svg: "svg",
19
+ webp: "webp"
20
+ };
21
+ /** MIME → hucre 图片类型;MIME 为空时回落扩展名 */
22
+ function resolveImageType(file) {
23
+ const mime = file.type.trim().toLowerCase();
24
+ if (mime && MIME_TO_TYPE[mime]) return MIME_TO_TYPE[mime];
25
+ const ext = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".") + 1) : "";
26
+ return EXT_TO_TYPE[ext.toLowerCase()];
27
+ }
28
+ /**
29
+ * 从本地 File 插入浮动图片:读字节 → 映射类型 → 锚定活动格 → ctx.insertImage。
30
+ * 不支持的格式 / 无活动格时 message.error 提示并返回 undefined。
31
+ */
32
+ async function insertImageFromFile(ctx, file) {
33
+ const type = resolveImageType(file);
34
+ if (!type) {
35
+ message.error("不支持的图片格式,请选择 png / jpeg / gif / svg / webp");
36
+ return;
37
+ }
38
+ const active = ctx.getSelection().activeCell;
39
+ if (!active) {
40
+ message.error("请先选择单元格");
41
+ return;
42
+ }
43
+ const buffer = await file.arrayBuffer();
44
+ return ctx.insertImage({
45
+ data: new Uint8Array(buffer),
46
+ type,
47
+ anchor: { from: {
48
+ row: active.row,
49
+ col: active.col
50
+ } }
51
+ });
52
+ }
53
+ /**
54
+ * 拉起系统文件选择框(右键菜单用;不用 UFilePicker——无编程式打开 API)。
55
+ * 选中文件后走 insertImageFromFile。
56
+ */
57
+ function pickAndInsertImage(ctx) {
58
+ const input = document.createElement("input");
59
+ input.type = "file";
60
+ input.accept = INSERT_IMAGE_ACCEPT;
61
+ input.hidden = true;
62
+ const cleanup = () => {
63
+ input.remove();
64
+ };
65
+ input.addEventListener("change", () => {
66
+ const file = input.files?.[0];
67
+ cleanup();
68
+ if (file) insertImageFromFile(ctx, file);
69
+ });
70
+ window.addEventListener("focus", () => {
71
+ setTimeout(() => {
72
+ if (document.body.contains(input) && !input.files?.length) cleanup();
73
+ }, 300);
74
+ }, { once: true });
75
+ document.body.appendChild(input);
76
+ input.click();
77
+ }
78
+ //#endregion
79
+ export { INSERT_IMAGE_ACCEPT, insertImageFromFile, pickAndInsertImage, resolveImageType };
@@ -0,0 +1,73 @@
1
+ import { BORDER_STYLE_WIDTH } from "@veltra/sheet-core/core/style/types";
2
+ //#region src/vue/popup-helpers.ts
3
+ /** 当前选区(ranges[0] 优先;无区域选区时用活动格单格) */
4
+ function currentRange(context) {
5
+ const { activeCell, ranges } = context.getSelection();
6
+ return ranges[0] ?? (activeCell ? {
7
+ start: activeCell,
8
+ end: activeCell
9
+ } : null);
10
+ }
11
+ const BORDER_LINE_STYLES = [
12
+ "thin",
13
+ "medium",
14
+ "thick",
15
+ "dashed",
16
+ "dotted"
17
+ ];
18
+ const BORDER_LINE_TITLES = {
19
+ thin: "细线",
20
+ medium: "中粗线",
21
+ thick: "粗线",
22
+ dashed: "虚线",
23
+ dotted: "点线"
24
+ };
25
+ const BORDER_PRESETS = [
26
+ {
27
+ id: "outer",
28
+ title: "外边框",
29
+ glyph: "outer"
30
+ },
31
+ {
32
+ id: "inner",
33
+ title: "内边框",
34
+ glyph: "inner"
35
+ },
36
+ {
37
+ id: "all",
38
+ title: "所有边框",
39
+ glyph: "all"
40
+ },
41
+ {
42
+ id: "top",
43
+ title: "上边框",
44
+ glyph: "top"
45
+ },
46
+ {
47
+ id: "bottom",
48
+ title: "下边框",
49
+ glyph: "bottom"
50
+ },
51
+ {
52
+ id: "left",
53
+ title: "左边框",
54
+ glyph: "left"
55
+ },
56
+ {
57
+ id: "right",
58
+ title: "右边框",
59
+ glyph: "right"
60
+ },
61
+ {
62
+ id: "none",
63
+ title: "无边框",
64
+ glyph: "none"
65
+ }
66
+ ];
67
+ /** 线型示例条样式(边框面板的线型按钮 swatch) */
68
+ function lineSwatchStyle(line) {
69
+ const dash = line === "dashed" ? "dashed" : line === "dotted" ? "dotted" : "solid";
70
+ return { borderBottom: `${BORDER_STYLE_WIDTH[line]}px ${dash} #000` };
71
+ }
72
+ //#endregion
73
+ export { BORDER_LINE_STYLES, BORDER_LINE_TITLES, BORDER_PRESETS, currentRange, lineSwatchStyle };
@@ -0,0 +1,75 @@
1
+ import { BORDER_LINE_STYLES, BORDER_LINE_TITLES, BORDER_PRESETS, currentRange, lineSwatchStyle } from "../popup-helpers.js";
2
+ import _sfc_main$1 from "./border-preset-glyph.js";
3
+ import { Fragment, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, ref, renderList, unref } from "vue";
4
+ import { UPalette } from "@veltra/desktop";
5
+ import { bem } from "@veltra/utils";
6
+ import { buildBorderPresetItems } from "@veltra/sheet-core/core/style/border-presets";
7
+ import { BORDER_STYLE_WIDTH } from "@veltra/sheet-core/core/style/types";
8
+ //#region src/vue/popups/border-popup.vue
9
+ const _hoisted_1 = ["title", "onClick"];
10
+ const _hoisted_2 = ["title", "onClick"];
11
+ const _sfc_main = /*@__PURE__*/ defineComponent({
12
+ name: "USheetBorderPopup",
13
+ __name: "border-popup",
14
+ props: { context: {} },
15
+ setup(__props) {
16
+ /**
17
+ * 边框面板(外边框 / 内边框 / 所有边框 / 单侧边框 / 无边框预设 + 线型 / 颜色)。
18
+ * 预设补丁由 core/style/border-presets 生成(含邻居格共享边同步,对齐
19
+ * Excel「写入时同步、最近设置生效」);面板打开期间的写入由
20
+ * use-tool-popup 事务包裹为一个 undo 单元。
21
+ */
22
+ const props = __props;
23
+ const cls = bem("sheet");
24
+ /** 当前线型 / 颜色 */
25
+ const borderLineStyle = ref("thin");
26
+ const borderColor = ref("#000000");
27
+ /** 应用边框预设:一次 executeCommand(items 批量,含邻居同步补丁)= 一个 undo 单元 */
28
+ function applyBorderPreset(preset) {
29
+ const range = currentRange(props.context);
30
+ if (!range) return;
31
+ const edge = {
32
+ style: borderLineStyle.value,
33
+ width: BORDER_STYLE_WIDTH[borderLineStyle.value],
34
+ color: borderColor.value
35
+ };
36
+ const items = buildBorderPresetItems(range, preset, edge, (addr) => props.context.getCellStyle(addr)).map(({ addr, patch }) => ({
37
+ addr,
38
+ partial: patch
39
+ }));
40
+ if (items.length === 0) return;
41
+ props.context.executeCommand("sheet.command.set-cell-style", { items });
42
+ }
43
+ return (_ctx, _cache) => {
44
+ return openBlock(), createElementBlock(Fragment, null, [
45
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("popup-row")) }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("popup-label")) }, "线型", 2), (openBlock(true), createElementBlock(Fragment, null, renderList(unref(BORDER_LINE_STYLES), (line) => {
46
+ return openBlock(), createElementBlock("button", {
47
+ key: line,
48
+ type: "button",
49
+ class: normalizeClass([unref(cls).e("popup-line"), unref(bem).is("active", borderLineStyle.value === line)]),
50
+ title: unref(BORDER_LINE_TITLES)[line],
51
+ onClick: ($event) => borderLineStyle.value = line
52
+ }, [createElementVNode("span", {
53
+ class: normalizeClass(unref(cls).e("popup-line-swatch")),
54
+ style: normalizeStyle(unref(lineSwatchStyle)(line))
55
+ }, null, 6)], 10, _hoisted_1);
56
+ }), 128))], 2),
57
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("popup-row")) }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("popup-label")) }, "颜色", 2), createVNode(unref(UPalette), {
58
+ "model-value": borderColor.value,
59
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = (value) => borderColor.value = value || borderColor.value)
60
+ }, null, 8, ["model-value"])], 2),
61
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("popup-row")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(BORDER_PRESETS), (preset) => {
62
+ return openBlock(), createElementBlock("button", {
63
+ key: preset.id,
64
+ type: "button",
65
+ class: normalizeClass(unref(cls).e("popup-preset")),
66
+ title: preset.title,
67
+ onClick: ($event) => applyBorderPreset(preset.id)
68
+ }, [createVNode(_sfc_main$1, { glyph: preset.glyph }, null, 8, ["glyph"])], 10, _hoisted_2);
69
+ }), 128))], 2)
70
+ ], 64);
71
+ };
72
+ }
73
+ });
74
+ //#endregion
75
+ export { _sfc_main as default };
@@ -0,0 +1,106 @@
1
+ import { computed, createCommentVNode, createElementBlock, createElementVNode, defineComponent, normalizeClass, openBlock, unref } from "vue";
2
+ import { bem } from "@veltra/utils";
3
+ //#region src/vue/popups/border-preset-glyph.vue
4
+ const _hoisted_1 = ["stroke"];
5
+ const _hoisted_2 = ["stroke"];
6
+ const _hoisted_3 = ["stroke"];
7
+ const _hoisted_4 = ["stroke"];
8
+ const _hoisted_5 = ["stroke"];
9
+ const _hoisted_6 = ["stroke"];
10
+ const MUTED = "var(--u-border-muted-color, #c0c4cc)";
11
+ const ACTIVE = "currentColor";
12
+ const _sfc_main = /*@__PURE__*/ defineComponent({
13
+ name: "USheetBorderPresetGlyph",
14
+ __name: "border-preset-glyph",
15
+ props: { glyph: {} },
16
+ setup(__props) {
17
+ /**
18
+ * 边框预设田字格 glyph(包内联 SVG,不进 @veltra/icons)。
19
+ * 3×3 网格(外框 + 十字),按 glyph 高亮对应边。
20
+ */
21
+ const props = __props;
22
+ const cls = bem("sheet");
23
+ /** 田字格各线段是否高亮 */
24
+ function stroke(active) {
25
+ return active ? ACTIVE : MUTED;
26
+ }
27
+ const edges = computed(() => {
28
+ const g = props.glyph;
29
+ return {
30
+ top: g === "outer" || g === "all" || g === "top",
31
+ bottom: g === "outer" || g === "all" || g === "bottom",
32
+ left: g === "outer" || g === "all" || g === "left",
33
+ right: g === "outer" || g === "all" || g === "right",
34
+ innerH: g === "inner" || g === "all",
35
+ innerV: g === "inner" || g === "all"
36
+ };
37
+ });
38
+ return (_ctx, _cache) => {
39
+ return openBlock(), createElementBlock("svg", {
40
+ class: normalizeClass(unref(cls).e("popup-preset-glyph")),
41
+ viewBox: "0 0 16 16",
42
+ width: "16",
43
+ height: "16",
44
+ "aria-hidden": "true"
45
+ }, [
46
+ createCommentVNode(" 田字格:外框 + 十字;高亮边用 currentColor,其余用 muted "),
47
+ createElementVNode("line", {
48
+ x1: "2",
49
+ y1: "2",
50
+ x2: "14",
51
+ y2: "2",
52
+ stroke: stroke(edges.value.top),
53
+ "stroke-width": "1.5",
54
+ "stroke-linecap": "square"
55
+ }, null, 8, _hoisted_1),
56
+ createElementVNode("line", {
57
+ x1: "2",
58
+ y1: "14",
59
+ x2: "14",
60
+ y2: "14",
61
+ stroke: stroke(edges.value.bottom),
62
+ "stroke-width": "1.5",
63
+ "stroke-linecap": "square"
64
+ }, null, 8, _hoisted_2),
65
+ createElementVNode("line", {
66
+ x1: "2",
67
+ y1: "2",
68
+ x2: "2",
69
+ y2: "14",
70
+ stroke: stroke(edges.value.left),
71
+ "stroke-width": "1.5",
72
+ "stroke-linecap": "square"
73
+ }, null, 8, _hoisted_3),
74
+ createElementVNode("line", {
75
+ x1: "14",
76
+ y1: "2",
77
+ x2: "14",
78
+ y2: "14",
79
+ stroke: stroke(edges.value.right),
80
+ "stroke-width": "1.5",
81
+ "stroke-linecap": "square"
82
+ }, null, 8, _hoisted_4),
83
+ createElementVNode("line", {
84
+ x1: "2",
85
+ y1: "8",
86
+ x2: "14",
87
+ y2: "8",
88
+ stroke: stroke(edges.value.innerH),
89
+ "stroke-width": "1.5",
90
+ "stroke-linecap": "square"
91
+ }, null, 8, _hoisted_5),
92
+ createElementVNode("line", {
93
+ x1: "8",
94
+ y1: "2",
95
+ x2: "8",
96
+ y2: "14",
97
+ stroke: stroke(edges.value.innerV),
98
+ "stroke-width": "1.5",
99
+ "stroke-linecap": "square"
100
+ }, null, 8, _hoisted_6)
101
+ ], 2);
102
+ };
103
+ }
104
+ });
105
+ //#endregion
106
+ export { _sfc_main as default };
@@ -0,0 +1,51 @@
1
+ import { exportSheetCsvFile, exportWorkbookFile } from "../../tools/download.js";
2
+ import { createElementBlock, createElementVNode, defineComponent, normalizeClass, openBlock, unref } from "vue";
3
+ import { message } from "@veltra/desktop";
4
+ import { bem } from "@veltra/utils";
5
+ //#region src/vue/popups/export-popup.vue
6
+ const _sfc_main = /*@__PURE__*/ defineComponent({
7
+ name: "USheetExportPopup",
8
+ __name: "export-popup",
9
+ props: { context: {} },
10
+ emits: ["close"],
11
+ setup(__props, { emit: __emit }) {
12
+ /**
13
+ * 导出选择面板(xlsx / csv)。不参与面板事务:点击即下载并关闭。
14
+ */
15
+ const props = __props;
16
+ const emit = __emit;
17
+ const cls = bem("sheet");
18
+ function exportXlsx() {
19
+ emit("close");
20
+ const loading = message({
21
+ message: "正在导出…",
22
+ duration: 0
23
+ });
24
+ exportWorkbookFile(props.context).then(() => loading.close()).catch((err) => {
25
+ loading.close();
26
+ message.error(`导出失败:${err instanceof Error ? err.message : String(err)}`);
27
+ });
28
+ }
29
+ function exportCsv() {
30
+ emit("close");
31
+ try {
32
+ exportSheetCsvFile(props.context);
33
+ } catch (err) {
34
+ message.error(`导出失败:${err instanceof Error ? err.message : String(err)}`);
35
+ }
36
+ }
37
+ return (_ctx, _cache) => {
38
+ return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("export-menu")) }, [createElementVNode("button", {
39
+ type: "button",
40
+ class: normalizeClass(unref(cls).e("export-option")),
41
+ onClick: exportXlsx
42
+ }, " 导出 Excel (.xlsx) ", 2), createElementVNode("button", {
43
+ type: "button",
44
+ class: normalizeClass(unref(cls).e("export-option")),
45
+ onClick: exportCsv
46
+ }, " 导出 CSV (.csv) ", 2)], 2);
47
+ };
48
+ }
49
+ });
50
+ //#endregion
51
+ export { _sfc_main as default };
@@ -0,0 +1,37 @@
1
+ import { currentRange } from "../popup-helpers.js";
2
+ import { createBlock, defineComponent, onMounted, openBlock, ref, unref } from "vue";
3
+ import { UPalette } from "@veltra/desktop";
4
+ //#region src/vue/popups/fill-color-popup.vue
5
+ const _sfc_main = /*@__PURE__*/ defineComponent({
6
+ name: "USheetFillColorPopup",
7
+ __name: "fill-color-popup",
8
+ props: { context: {} },
9
+ setup(__props) {
10
+ /**
11
+ * 填充颜色面板(UPalette 调色板 + 无填充 = 清除 fill 保留边框)。
12
+ * 面板打开期间的写入由 use-tool-popup 事务包裹为一个 undo 单元。
13
+ */
14
+ const props = __props;
15
+ /** 填充颜色('' = 无填充) */
16
+ const fillColor = ref("");
17
+ onMounted(() => {
18
+ const active = props.context.getSelection().activeCell;
19
+ const style = active ? props.context.getCellStyle(active) : void 0;
20
+ fillColor.value = style?.fill?.color ?? "";
21
+ });
22
+ /** 填充颜色变化('' = 无填充:清除 fill 保留边框) */
23
+ function applyFillColor(color) {
24
+ const range = currentRange(props.context);
25
+ if (!range) return;
26
+ props.context.setCellStyle(range, color ? { fill: { color } } : { fill: {} });
27
+ }
28
+ return (_ctx, _cache) => {
29
+ return openBlock(), createBlock(unref(UPalette), {
30
+ "model-value": fillColor.value,
31
+ "onUpdate:modelValue": applyFillColor
32
+ }, null, 8, ["model-value"]);
33
+ };
34
+ }
35
+ });
36
+ //#endregion
37
+ export { _sfc_main as default };