@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.
Files changed (211) hide show
  1. package/dist/index.d.ts +4 -26
  2. package/dist/index.js +3 -23
  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 -164
  33. package/dist/vue/style.d.ts +11 -1
  34. package/dist/vue/style.js +10 -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 -7
  46. package/src/env.d.ts +11 -0
  47. package/src/index.ts +203 -14
  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 +161 -186
  86. package/src/vue/style.scss +646 -3
  87. package/src/vue/style.ts +11 -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/fill.d.ts +0 -34
  125. package/dist/core/fill.js +0 -320
  126. package/dist/core/fill.js.map +0 -1
  127. package/dist/core/formula/ast.d.ts +0 -58
  128. package/dist/core/formula/ast.js +0 -38
  129. package/dist/core/formula/ast.js.map +0 -1
  130. package/dist/core/formula/dependency-graph.d.ts +0 -70
  131. package/dist/core/formula/dependency-graph.js +0 -269
  132. package/dist/core/formula/dependency-graph.js.map +0 -1
  133. package/dist/core/formula/errors.d.ts +0 -25
  134. package/dist/core/formula/errors.js +0 -36
  135. package/dist/core/formula/errors.js.map +0 -1
  136. package/dist/core/formula/evaluator.d.ts +0 -41
  137. package/dist/core/formula/evaluator.js +0 -154
  138. package/dist/core/formula/evaluator.js.map +0 -1
  139. package/dist/core/formula/functions.d.ts +0 -30
  140. package/dist/core/formula/functions.js +0 -209
  141. package/dist/core/formula/functions.js.map +0 -1
  142. package/dist/core/formula/parser.d.ts +0 -7
  143. package/dist/core/formula/parser.js +0 -208
  144. package/dist/core/formula/parser.js.map +0 -1
  145. package/dist/core/formula/tokenizer.d.ts +0 -37
  146. package/dist/core/formula/tokenizer.js +0 -146
  147. package/dist/core/formula/tokenizer.js.map +0 -1
  148. package/dist/core/merge-manager.d.ts +0 -64
  149. package/dist/core/merge-manager.js +0 -111
  150. package/dist/core/merge-manager.js.map +0 -1
  151. package/dist/core/selection.d.ts +0 -31
  152. package/dist/core/selection.js +0 -62
  153. package/dist/core/selection.js.map +0 -1
  154. package/dist/core/sheet.d.ts +0 -116
  155. package/dist/core/sheet.js +0 -224
  156. package/dist/core/sheet.js.map +0 -1
  157. package/dist/core/workbook.d.ts +0 -43
  158. package/dist/core/workbook.js +0 -77
  159. package/dist/core/workbook.js.map +0 -1
  160. package/dist/grid/sheet-grid.d.ts +0 -81
  161. package/dist/grid/sheet-grid.js +0 -331
  162. package/dist/grid/sheet-grid.js.map +0 -1
  163. package/dist/grid/vtable-theme.js +0 -80
  164. package/dist/grid/vtable-theme.js.map +0 -1
  165. package/dist/tools/builtin.js.map +0 -1
  166. package/dist/tools/context.js.map +0 -1
  167. package/dist/tools/registry.js.map +0 -1
  168. package/dist/vue/index.d.ts +0 -2
  169. package/dist/vue/sheet.js.map +0 -1
  170. package/dist/vue/sheet.vue.d.ts +0 -24
  171. package/src/core/__test__/address.test.ts +0 -113
  172. package/src/core/__test__/cell-store.test.ts +0 -97
  173. package/src/core/__test__/command.test.ts +0 -377
  174. package/src/core/__test__/fill.test.ts +0 -140
  175. package/src/core/__test__/formula-adversarial.test.ts +0 -265
  176. package/src/core/__test__/formula-engine.test.ts +0 -470
  177. package/src/core/__test__/formula-functions.test.ts +0 -178
  178. package/src/core/__test__/formula-parser.test.ts +0 -160
  179. package/src/core/__test__/history.test.ts +0 -225
  180. package/src/core/__test__/merge-manager.test.ts +0 -106
  181. package/src/core/__test__/sheet.test.ts +0 -142
  182. package/src/core/address.ts +0 -166
  183. package/src/core/cell-store.ts +0 -173
  184. package/src/core/command/default-registry.ts +0 -15
  185. package/src/core/command/history.ts +0 -167
  186. package/src/core/command/index.ts +0 -7
  187. package/src/core/command/merge-cells.ts +0 -74
  188. package/src/core/command/registry.ts +0 -30
  189. package/src/core/command/set-cell-formula.ts +0 -29
  190. package/src/core/command/set-cell-value.ts +0 -38
  191. package/src/core/command/types.ts +0 -73
  192. package/src/core/events.ts +0 -37
  193. package/src/core/fill.ts +0 -314
  194. package/src/core/formula/ast.ts +0 -66
  195. package/src/core/formula/dependency-graph.ts +0 -329
  196. package/src/core/formula/errors.ts +0 -41
  197. package/src/core/formula/evaluator.ts +0 -219
  198. package/src/core/formula/functions.ts +0 -266
  199. package/src/core/formula/index.ts +0 -7
  200. package/src/core/formula/parser.ts +0 -198
  201. package/src/core/formula/tokenizer.ts +0 -160
  202. package/src/core/merge-manager.ts +0 -158
  203. package/src/core/selection.ts +0 -66
  204. package/src/core/sheet.ts +0 -282
  205. package/src/core/workbook.ts +0 -99
  206. package/src/grid/__test__/canvas-mock.ts +0 -49
  207. package/src/grid/__test__/setup.ts +0 -3
  208. package/src/grid/__test__/sheet-grid-formula.test.ts +0 -140
  209. package/src/grid/__test__/sheet-grid.test.ts +0 -275
  210. package/src/grid/sheet-grid.ts +0 -407
  211. package/src/grid/vtable-theme.ts +0 -61
@@ -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 };