@veltra/sheet 2.5.5 → 2.5.7

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.
@@ -1,12 +1,14 @@
1
1
  import _sfc_main$1 from "./formula-suggest-list.js";
2
- import { insertRefText, isRefSelectContext } from "./use-formula-ref-select.js";
3
2
  import { applySuggest, filterFormulaSuggestions, getSuggestContext, moveSuggestIndex } from "./use-formula-suggest.js";
3
+ import _sfc_main$2 from "./popups/functions-popup.js";
4
+ import { insertRefText, isRefSelectContext } from "./use-formula-ref-select.js";
4
5
  import { formatAddress, formatRange, parseRange } from "@veltra/sheet-core/core/address";
5
- import { computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, shallowRef, unref, useTemplateRef, vModelText, watch, withDirectives, withModifiers } from "vue";
6
- import { message } from "@veltra/desktop";
6
+ import { computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, nextTick, normalizeClass, onBeforeUnmount, onMounted, openBlock, ref, shallowRef, unref, useTemplateRef, vModelText, watch, withCtx, withDirectives, withModifiers } from "vue";
7
+ import { UDropdown, message } from "@veltra/desktop";
7
8
  import { bem } from "@veltra/utils";
8
9
  //#region src/components/sheet/formula-bar.vue
9
- const _hoisted_1 = ["disabled", "readonly"];
10
+ const _hoisted_1 = ["disabled"];
11
+ const _hoisted_2 = ["disabled", "readonly"];
10
12
  const _sfc_main = /*@__PURE__*/ defineComponent({
11
13
  name: "UFormulaBar",
12
14
  __name: "formula-bar",
@@ -19,6 +21,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
19
21
  * 公式栏(名称框 + fx 输入栏),USheet 顶部结构之一。
20
22
  *
21
23
  * - 名称框:显示当前选区(单格 A1 / 区域 A1:B2);合法地址回车跳转。
24
+ * - fx 按钮:打开函数弹框(分类导航 + 搜索),选中进入编辑态输出 `=NAME()`。
22
25
  * - fx 输入栏:活动格内容;Enter/✓ 提交、Esc/✗ 取消、失焦提交。
23
26
  * - 函数补全:`=` 开头且光标在函数名 token 上时弹出候选(↑↓ / Tab / Enter / 点击)。
24
27
  * - 引用选择:光标在运算符/`(`/`,` 后时,画布点选/拖选插入引用(blur 抑制防误提交)。
@@ -61,9 +64,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
61
64
  if (!isRefSelecting()) return;
62
65
  suppressBlurCommit = true;
63
66
  }
64
- /** 选区拦截回调:插入 A1 / A1:B2 到光标处 */
67
+ /** 选区拦截回调:插入 A1 / A1:B2 到光标处(仅引用选择模式;弹框打开但非引用上下文时不插入) */
65
68
  function handleRefSelect(range) {
66
- if (!editing.value || mirroring.value) return;
69
+ if (!isRefSelecting()) return;
67
70
  const rangeText = formatRange(range);
68
71
  const selEnd = fxRef.value?.selectionEnd ?? fxCursor.value;
69
72
  const result = insertRefText(fxDraft.value, fxCursor.value, rangeText, selEnd);
@@ -288,7 +291,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
288
291
  editAddr = null;
289
292
  suppressBlurCommit = false;
290
293
  closeSuggest();
291
- if (addr) props.context.setCellValue(addr, fxDraft.value);
294
+ if (addr) {
295
+ props.context.setCellValue(addr, fxDraft.value);
296
+ const active = selection.value.activeCell;
297
+ if (!active || active.row !== addr.row || active.col !== addr.col) props.context.selectCell(addr);
298
+ }
292
299
  refreshFx();
293
300
  }
294
301
  function cancelEdit() {
@@ -336,12 +343,76 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
336
343
  refreshFx();
337
344
  }
338
345
  }
346
+ const fxLabelRef = useTemplateRef("fxLabelRef");
347
+ const functionsDropdownRef = useTemplateRef("functionsDropdownRef");
348
+ const functionsOpen = ref(false);
349
+ function toggleFunctions() {
350
+ if (fxDisabled.value) return;
351
+ if (functionsOpen.value) {
352
+ functionsOpen.value = false;
353
+ return;
354
+ }
355
+ if (editing.value && fxFocused.value) suppressBlurCommit = true;
356
+ functionsOpen.value = true;
357
+ }
358
+ watch(functionsOpen, () => {
359
+ const dropdown = functionsDropdownRef.value;
360
+ if (!dropdown) return;
361
+ if (functionsOpen.value) dropdown.open({ trigger: fxLabelRef.value ?? void 0 });
362
+ else dropdown.close();
363
+ });
364
+ /** UDropdown 内部关闭(触发元素位移等)→ 同步本地开合状态 */
365
+ function syncFunctionsOpen(visible) {
366
+ if (!visible) functionsOpen.value = false;
367
+ }
368
+ /** 弹框内 Esc 关闭(关闭不还原草稿、不退出挂起的编辑态) */
369
+ function handleFunctionsKeydown(event) {
370
+ if (event.key !== "Escape") return;
371
+ event.preventDefault();
372
+ functionsOpen.value = false;
373
+ }
374
+ /** 点击弹框外关闭(弹框内容与 fx 按钮均 @click.stop,到达这里的必是外部点击) */
375
+ function closeFunctionsOnOuterClick() {
376
+ if (!functionsOpen.value) return;
377
+ functionsOpen.value = false;
378
+ }
379
+ /** 选中函数:关弹框并进入编辑态输出 =NAME()(fx / 工具栏共用路径) */
380
+ function handleFunctionSelect(name) {
381
+ functionsOpen.value = false;
382
+ insertFunction(name);
383
+ }
384
+ /** 插入函数:编辑目标 = 当前活动格,内容覆盖为 =NAME(),光标落在括号内 */
385
+ function insertFunction(name) {
386
+ const addr = selection.value.activeCell;
387
+ if (!addr) return;
388
+ const text = `=${name}()`;
389
+ const cursor = text.length - 1;
390
+ editing.value = true;
391
+ editAddr = addr;
392
+ mirroring.value = false;
393
+ mirrorAddr = null;
394
+ fxDraft.value = text;
395
+ fxCursor.value = cursor;
396
+ suppressBlurCommit = false;
397
+ closeSuggest();
398
+ nextTick(() => {
399
+ const el = fxRef.value;
400
+ if (!el) return;
401
+ el.focus();
402
+ el.setSelectionRange(cursor, cursor);
403
+ fxCursor.value = cursor;
404
+ autosizeFx();
405
+ refreshSuggest();
406
+ });
407
+ }
339
408
  __expose({
340
409
  mirrorGridEdit,
341
410
  exitMirror,
342
411
  isRefSelecting,
412
+ isFunctionPopupOpen: () => functionsOpen.value,
343
413
  beginBlurSuppress,
344
- handleRefSelect
414
+ handleRefSelect,
415
+ insertFunction
345
416
  });
346
417
  let dispose = [];
347
418
  function bindSheet(sheet) {
@@ -355,6 +426,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
355
426
  mirroring.value = false;
356
427
  mirrorAddr = null;
357
428
  suppressBlurCommit = false;
429
+ functionsOpen.value = false;
358
430
  closeSuggest();
359
431
  bindSheet(sheet);
360
432
  selection.value = sheet.getSelection();
@@ -366,10 +438,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
366
438
  selection.value = props.sheet.getSelection();
367
439
  refreshName();
368
440
  refreshFx();
441
+ window.addEventListener("click", closeFunctionsOnOuterClick);
369
442
  });
370
443
  onBeforeUnmount(() => {
371
444
  for (const off of dispose) off();
372
445
  dispose = [];
446
+ window.removeEventListener("click", closeFunctionsOnOuterClick);
373
447
  });
374
448
  return (_ctx, _cache) => {
375
449
  return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).e("formula-bar")) }, [
@@ -383,7 +457,33 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
383
457
  onBlur: handleNameBlur,
384
458
  onKeydown: handleNameKeydown
385
459
  }, null, 34), [[vModelText, nameDraft.value]]),
386
- createElementVNode("span", { class: normalizeClass(unref(cls).e("fx-label")) }, "fx", 2),
460
+ createElementVNode("button", {
461
+ ref_key: "fxLabelRef",
462
+ ref: fxLabelRef,
463
+ type: "button",
464
+ class: normalizeClass(unref(cls).e("fx-label")),
465
+ disabled: fxDisabled.value,
466
+ title: "插入函数",
467
+ onMousedown: _cache[2] || (_cache[2] = withModifiers(() => {}, ["prevent"])),
468
+ onClick: withModifiers(toggleFunctions, ["stop"])
469
+ }, " fx ", 42, _hoisted_1),
470
+ createCommentVNode(" 函数弹框:锚定 fx 按钮下方(UDropdown Teleport 到 #pop-container,\n 同工具栏弹层机制);打开 / 关闭不触碰选区与草稿 "),
471
+ createVNode(unref(UDropdown), {
472
+ ref_key: "functionsDropdownRef",
473
+ ref: functionsDropdownRef,
474
+ trigger: "custom",
475
+ visible: functionsOpen.value,
476
+ width: "auto",
477
+ onKeydown: handleFunctionsKeydown,
478
+ "onUpdate:visible": syncFunctionsOpen
479
+ }, {
480
+ content: withCtx(() => [functionsOpen.value ? (openBlock(), createElementBlock("div", {
481
+ key: 0,
482
+ class: normalizeClass(unref(cls).e("functions-popover")),
483
+ onClick: _cache[3] || (_cache[3] = withModifiers(() => {}, ["stop"]))
484
+ }, [createVNode(_sfc_main$2, { onSelect: handleFunctionSelect })], 2)) : createCommentVNode("v-if", true)]),
485
+ _: 1
486
+ }, 8, ["visible"]),
387
487
  createElementVNode("div", {
388
488
  class: normalizeClass(unref(cls).e("fx-editor")),
389
489
  onMouseenter: handleFxMouseEnter,
@@ -391,7 +491,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
391
491
  }, [createCommentVNode(" 文档流只占单行;面板绝对定位向下浮起,不撑开公式栏 "), createElementVNode("div", { class: normalizeClass([unref(cls).e("fx-input-panel"), unref(bem).is("expanded", fxExpanded.value)]) }, [withDirectives(createElementVNode("textarea", {
392
492
  ref_key: "fxRef",
393
493
  ref: fxRef,
394
- "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => fxDraft.value = $event),
494
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => fxDraft.value = $event),
395
495
  class: normalizeClass(unref(cls).e("fx-input")),
396
496
  disabled: fxDisabled.value,
397
497
  readonly: mirroring.value,
@@ -406,18 +506,18 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
406
506
  onClick: syncFxCursor,
407
507
  onKeyup: syncFxCursor,
408
508
  onSelect: syncFxCursor
409
- }, null, 42, _hoisted_1), [[vModelText, fxDraft.value]]), createVNode(_sfc_main$1, {
509
+ }, null, 42, _hoisted_2), [[vModelText, fxDraft.value]]), createVNode(_sfc_main$1, {
410
510
  items: suggestItems.value,
411
511
  "active-index": suggestIndex.value,
412
512
  onSelect: confirmSuggest,
413
- onHover: _cache[3] || (_cache[3] = (i) => suggestIndex.value = i)
513
+ onHover: _cache[5] || (_cache[5] = (i) => suggestIndex.value = i)
414
514
  }, null, 8, ["items", "active-index"])], 2)], 34),
415
515
  editing.value ? (openBlock(), createElementBlock("button", {
416
516
  key: 0,
417
517
  type: "button",
418
518
  class: normalizeClass(unref(cls).e("fx-btn")),
419
519
  title: "提交(Enter)",
420
- onMousedown: _cache[4] || (_cache[4] = withModifiers(() => {}, ["prevent"])),
520
+ onMousedown: _cache[6] || (_cache[6] = withModifiers(() => {}, ["prevent"])),
421
521
  onClick: commitEdit
422
522
  }, " ✓ ", 34)) : createCommentVNode("v-if", true),
423
523
  editing.value ? (openBlock(), createElementBlock("button", {
@@ -425,7 +525,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
425
525
  type: "button",
426
526
  class: normalizeClass(unref(cls).e("fx-btn")),
427
527
  title: "取消(Esc)",
428
- onMousedown: _cache[5] || (_cache[5] = withModifiers(() => {}, ["prevent"])),
528
+ onMousedown: _cache[7] || (_cache[7] = withModifiers(() => {}, ["prevent"])),
429
529
  onClick: cancelEdit
430
530
  }, " ✗ ", 34)) : createCommentVNode("v-if", true)
431
531
  ], 2);
@@ -0,0 +1,117 @@
1
+ import { COMMON_FORMULA_NAMES, FUNCTION_POPUP_CATEGORIES, formatFunctionSignature, moveSuggestIndex } from "../use-formula-suggest.js";
2
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, onMounted, openBlock, ref, renderList, toDisplayString, unref, useTemplateRef, watch, withCtx } from "vue";
3
+ import { UInput, UScroll } from "@veltra/desktop";
4
+ import { bem } from "@veltra/utils";
5
+ import { listFormulaFunctions } from "@veltra/sheet-core/core/formula/functions";
6
+ //#region src/components/sheet/popups/functions-popup.vue
7
+ const _hoisted_1 = ["onClick"];
8
+ const _hoisted_2 = ["onClick", "onMouseenter"];
9
+ const _sfc_main = /*@__PURE__*/ defineComponent({
10
+ name: "USheetFunctionsPopup",
11
+ __name: "functions-popup",
12
+ emits: ["select"],
13
+ setup(__props, { emit: __emit }) {
14
+ /**
15
+ * 函数弹框(分类导航 + 搜索 + 可选择):fx 按钮与工具栏「函数」共用。
16
+ * 列出全部已注册函数(内置 + 宿主经 registerFormulaFunction 注册的自定义函数),
17
+ * 选中仅 emit select(name),由公式栏接管(进入编辑态输出 =NAME()),不写模型。
18
+ * 弹框随 v-if 挂载 / 销毁,每次打开都是全新状态(关键词清空、回到「常用」)。
19
+ */
20
+ const emit = __emit;
21
+ const cls = bem("sheet");
22
+ const panelRef = useTemplateRef("panelRef");
23
+ const keyword = ref("");
24
+ const activeCategory = ref("常用");
25
+ /** 键盘 / 悬停高亮下标(点击即确认不高亮) */
26
+ const activeIndex = ref(0);
27
+ /** 打开时快照一次注册表(弹框存活期间注册表变化不追踪,重开即刷新) */
28
+ const all = listFormulaFunctions();
29
+ /** 关键词大小写不敏感子串匹配:名称或中文描述;跨分类(未分类函数也可搜到) */
30
+ function searchResult(key) {
31
+ return all.filter((fn) => fn.name.toLowerCase().includes(key) || fn.description.toLowerCase().includes(key));
32
+ }
33
+ /** 当前展示列表:搜索优先;否则按分类导航过滤 */
34
+ const visible = computed(() => {
35
+ const key = keyword.value.trim().toLowerCase();
36
+ if (key) return searchResult(key);
37
+ if (activeCategory.value === "全部") return all;
38
+ if (activeCategory.value === "常用") {
39
+ const byName = new Map(all.map((fn) => [fn.name, fn]));
40
+ return COMMON_FORMULA_NAMES.map((name) => byName.get(name)).filter((fn) => fn != null);
41
+ }
42
+ return all.filter((fn) => fn.category === activeCategory.value);
43
+ });
44
+ watch(visible, () => {
45
+ activeIndex.value = 0;
46
+ });
47
+ function selectCategory(cat) {
48
+ activeCategory.value = cat;
49
+ }
50
+ /** 键盘路径:↑↓ 移动高亮(循环),Enter 确认选中(Esc 交由外层弹框关闭) */
51
+ function handleKeydown(event) {
52
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
53
+ event.preventDefault();
54
+ activeIndex.value = moveSuggestIndex(activeIndex.value, visible.value.length, event.key === "ArrowDown" ? 1 : -1);
55
+ return;
56
+ }
57
+ if (event.key === "Enter") {
58
+ const fn = visible.value[activeIndex.value];
59
+ if (!fn) return;
60
+ event.preventDefault();
61
+ emit("select", fn.name);
62
+ }
63
+ }
64
+ onMounted(() => {
65
+ panelRef.value?.querySelector("input")?.focus();
66
+ });
67
+ return (_ctx, _cache) => {
68
+ return openBlock(), createElementBlock("div", {
69
+ ref_key: "panelRef",
70
+ ref: panelRef,
71
+ class: normalizeClass(unref(cls).e("functions-panel")),
72
+ onKeydown: handleKeydown
73
+ }, [createElementVNode("nav", { class: normalizeClass(unref(cls).e("functions-nav")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(FUNCTION_POPUP_CATEGORIES), (cat) => {
74
+ return openBlock(), createElementBlock("button", {
75
+ key: cat,
76
+ type: "button",
77
+ class: normalizeClass([unref(cls).e("functions-nav-item"), unref(bem).is("active", cat === activeCategory.value)]),
78
+ onClick: ($event) => selectCategory(cat)
79
+ }, toDisplayString(cat), 11, _hoisted_1);
80
+ }), 128))], 2), createElementVNode("div", { class: normalizeClass(unref(cls).e("functions-main")) }, [
81
+ createVNode(unref(UInput), {
82
+ modelValue: keyword.value,
83
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => keyword.value = $event),
84
+ placeholder: "搜索函数名或描述",
85
+ size: "small",
86
+ class: normalizeClass(unref(cls).e("functions-search"))
87
+ }, null, 8, ["modelValue", "class"]),
88
+ createCommentVNode(" 滚动用 desktop UScroll(自定义滚动条),同字号面板 "),
89
+ createVNode(unref(UScroll), {
90
+ height: 320,
91
+ class: normalizeClass(unref(cls).e("functions-scroll"))
92
+ }, {
93
+ default: withCtx(() => [visible.value.length > 0 ? (openBlock(), createElementBlock("div", {
94
+ key: 0,
95
+ class: normalizeClass(unref(cls).e("functions-list"))
96
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(visible.value, (fn, index) => {
97
+ return openBlock(), createElementBlock("div", {
98
+ key: fn.name,
99
+ class: normalizeClass([unref(cls).e("functions-item"), unref(bem).is("active", index === activeIndex.value)]),
100
+ onClick: ($event) => emit("select", fn.name),
101
+ onMouseenter: ($event) => activeIndex.value = index
102
+ }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("functions-signature")) }, toDisplayString(unref(formatFunctionSignature)(fn.name, fn.params)), 3), fn.description ? (openBlock(), createElementBlock("span", {
103
+ key: 0,
104
+ class: normalizeClass(unref(cls).e("functions-description"))
105
+ }, toDisplayString(fn.description), 3)) : createCommentVNode("v-if", true)], 42, _hoisted_2);
106
+ }), 128))], 2)) : (openBlock(), createElementBlock("div", {
107
+ key: 1,
108
+ class: normalizeClass(unref(cls).e("functions-empty"))
109
+ }, "无匹配函数", 2))]),
110
+ _: 1
111
+ }, 8, ["class"])
112
+ ], 2)], 34);
113
+ };
114
+ }
115
+ });
116
+ //#endregion
117
+ export { _sfc_main as default };
@@ -1,6 +1,6 @@
1
1
  import { SheetSnapshot } from "@veltra/sheet-core/core/sheet";
2
2
  //#region src/components/sheet/popups/import.worker.d.ts
3
- type ImportWorkerResponse = {
3
+ export type ImportWorkerResponse = {
4
4
  type: 'progress';
5
5
  done: number;
6
6
  total: number;
@@ -17,5 +17,4 @@ type ImportWorkerResponse = {
17
17
  ok: false;
18
18
  error: string;
19
19
  };
20
- //#endregion
21
- export { ImportWorkerResponse };
20
+ //#endregion
@@ -1,14 +1,15 @@
1
- import _sfc_main$1 from "./formula-bar.js";
1
+ import _sfc_main$1 from "./popups/functions-popup.js";
2
+ import _sfc_main$2 from "./formula-bar.js";
2
3
  import { pickAndImportFile } from "./import-file.js";
3
- import _sfc_main$2 from "./popups/border-popup.js";
4
- import _sfc_main$3 from "./popups/export-popup.js";
5
- import _sfc_main$4 from "./popups/fill-color-popup.js";
6
- import _sfc_main$5 from "./popups/find-popup.js";
7
- import _sfc_main$6 from "./popups/font-color-popup.js";
8
- import _sfc_main$7 from "./popups/font-size-popup.js";
9
- import _sfc_main$8 from "./popups/insert-image-popup.js";
10
- import _sfc_main$9 from "./sheet-tabs.js";
11
- import _sfc_main$10 from "./sheet-toolbar.js";
4
+ import _sfc_main$3 from "./popups/border-popup.js";
5
+ import _sfc_main$4 from "./popups/export-popup.js";
6
+ import _sfc_main$5 from "./popups/fill-color-popup.js";
7
+ import _sfc_main$6 from "./popups/find-popup.js";
8
+ import _sfc_main$7 from "./popups/font-color-popup.js";
9
+ import _sfc_main$8 from "./popups/font-size-popup.js";
10
+ import _sfc_main$9 from "./popups/insert-image-popup.js";
11
+ import _sfc_main$10 from "./sheet-tabs.js";
12
+ import _sfc_main$11 from "./sheet-toolbar.js";
12
13
  import { useSheetGrid } from "./use-sheet-grid.js";
13
14
  import { useSheetState } from "./use-sheet-state.js";
14
15
  import { useToolGroups } from "./use-tool-groups.js";
@@ -88,6 +89,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
88
89
  }
89
90
  openOrRunTool(tool, event);
90
91
  }
92
+ /** 工具栏「函数」弹框选中 → 关弹层并走公式栏统一插入路径(与 fx 按钮一致) */
93
+ function handleFunctionSelect(name) {
94
+ closePopup();
95
+ formulaBarRef.value?.insertFunction(name);
96
+ }
91
97
  const popupDropdownRef = useTemplateRef("popupDropdownRef");
92
98
  watch(popupTool, () => {
93
99
  const dropdown = popupDropdownRef.value;
@@ -107,7 +113,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
107
113
  gridRef,
108
114
  getActiveSheet: () => activeSheet.value,
109
115
  context,
110
- formulaBarRef
116
+ formulaBarRef,
117
+ isFunctionPopupOpen: () => popupTool.value?.popup === "functions"
111
118
  });
112
119
  __expose({
113
120
  workbook,
@@ -122,12 +129,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
122
129
  class: normalizeClass(unref(cls).b)
123
130
  }, [
124
131
  createElementVNode("div", { class: normalizeClass(unref(cls).e("toolbar-wrap")) }, [
125
- __props.showToolbar ? (openBlock(), createBlock(_sfc_main$10, {
132
+ __props.showToolbar ? (openBlock(), createBlock(_sfc_main$11, {
126
133
  key: 0,
127
134
  groups: unref(toolGroups),
128
135
  onToolClick: handleToolClick
129
136
  }, null, 8, ["groups"])) : createCommentVNode("v-if", true),
130
- createCommentVNode(" 弹层型工具面板(填充/边框/字体色/字号/查找/插入图片/导出):UDropdown(Teleport 到\n #pop-container + floating-ui 定位:锚点跟随触发按钮、自动翻转/边界位移;\n 触发元素滚动/窗口缩放时自动关闭)。面板交互走 SheetContext 命令入口。\n 导入无弹层:点击直接系统文件选择(见 handleToolClick → pickAndImportFile) "),
137
+ createCommentVNode(" 弹层型工具面板(填充/边框/字体色/字号/查找/函数/插入图片/导出):UDropdown(Teleport 到\n #pop-container + floating-ui 定位:锚点跟随触发按钮、自动翻转/边界位移;\n 触发元素滚动/窗口缩放时自动关闭)。面板交互走 SheetContext 命令入口。\n 导入无弹层:点击直接系统文件选择(见 handleToolClick → pickAndImportFile) "),
131
138
  createVNode(unref(UDropdown), {
132
139
  ref_key: "popupDropdownRef",
133
140
  ref: popupDropdownRef,
@@ -140,19 +147,19 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
140
147
  key: 0,
141
148
  class: normalizeClass(unref(cls).e("popup")),
142
149
  onClick: _cache[0] || (_cache[0] = withModifiers(() => {}, ["stop"]))
143
- }, [unref(popupTool).popup === "fill-color" ? (openBlock(), createBlock(_sfc_main$4, {
150
+ }, [unref(popupTool).popup === "fill-color" ? (openBlock(), createBlock(_sfc_main$5, {
144
151
  key: 0,
145
152
  context: unref(context)
146
- }, null, 8, ["context"])) : unref(popupTool).popup === "border" ? (openBlock(), createBlock(_sfc_main$2, {
153
+ }, null, 8, ["context"])) : unref(popupTool).popup === "border" ? (openBlock(), createBlock(_sfc_main$3, {
147
154
  key: 1,
148
155
  context: unref(context)
149
- }, null, 8, ["context"])) : unref(popupTool).popup === "font-color" ? (openBlock(), createBlock(_sfc_main$6, {
156
+ }, null, 8, ["context"])) : unref(popupTool).popup === "font-color" ? (openBlock(), createBlock(_sfc_main$7, {
150
157
  key: 2,
151
158
  context: unref(context)
152
- }, null, 8, ["context"])) : unref(popupTool).popup === "font-size" ? (openBlock(), createBlock(_sfc_main$7, {
159
+ }, null, 8, ["context"])) : unref(popupTool).popup === "font-size" ? (openBlock(), createBlock(_sfc_main$8, {
153
160
  key: 3,
154
161
  context: unref(context)
155
- }, null, 8, ["context"])) : unref(popupTool).popup === "find" ? (openBlock(), createBlock(_sfc_main$5, {
162
+ }, null, 8, ["context"])) : unref(popupTool).popup === "find" ? (openBlock(), createBlock(_sfc_main$6, {
156
163
  key: 4,
157
164
  sheet: unref(activeSheet),
158
165
  context: unref(context),
@@ -161,19 +168,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
161
168
  "sheet",
162
169
  "context",
163
170
  "onClose"
164
- ])) : unref(popupTool).popup === "insert-image" ? (openBlock(), createBlock(_sfc_main$8, {
171
+ ])) : unref(popupTool).popup === "functions" ? (openBlock(), createBlock(_sfc_main$1, {
165
172
  key: 5,
173
+ onSelect: handleFunctionSelect
174
+ })) : unref(popupTool).popup === "insert-image" ? (openBlock(), createBlock(_sfc_main$9, {
175
+ key: 6,
166
176
  context: unref(context),
167
177
  onClose: unref(closePopup)
168
- }, null, 8, ["context", "onClose"])) : unref(popupTool).popup === "export" ? (openBlock(), createBlock(_sfc_main$3, {
169
- key: 6,
178
+ }, null, 8, ["context", "onClose"])) : unref(popupTool).popup === "export" ? (openBlock(), createBlock(_sfc_main$4, {
179
+ key: 7,
170
180
  context: unref(context),
171
181
  onClose: unref(closePopup)
172
182
  }, null, 8, ["context", "onClose"])) : createCommentVNode("v-if", true)], 2)) : createCommentVNode("v-if", true)]),
173
183
  _: 1
174
184
  }, 8, ["visible"])
175
185
  ], 2),
176
- __props.showFormulaBar ? (openBlock(), createBlock(_sfc_main$1, {
186
+ __props.showFormulaBar ? (openBlock(), createBlock(_sfc_main$2, {
177
187
  key: 0,
178
188
  ref_key: "formulaBarRef",
179
189
  ref: formulaBarRef,
@@ -188,7 +198,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
188
198
  key: 0,
189
199
  class: normalizeClass(unref(cls).e("loading-mask"))
190
200
  }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("loading-spinner")) }, null, 2), createElementVNode("div", { class: normalizeClass(unref(cls).e("loading-text")) }, toDisplayString(parseText.value), 3)], 2)) : createCommentVNode("v-if", true)], 2),
191
- __props.showTabs ? (openBlock(), createBlock(_sfc_main$9, {
201
+ __props.showTabs ? (openBlock(), createBlock(_sfc_main$10, {
192
202
  key: 1,
193
203
  workbook: unref(workbook),
194
204
  "sheet-list": unref(sheetList),
@@ -226,10 +226,23 @@
226
226
  flex-shrink: 0;
227
227
  align-items: center;
228
228
  height: var(--u-form-component-height-small);
229
+ padding: 0 4px;
230
+ border: none;
231
+ border-radius: var(--u-radius-small);
232
+ background: transparent;
229
233
  color: var(--u-text-color-second);
230
234
  font-size: var(--u-font-size-main-small);
231
235
  font-style: italic;
232
236
  user-select: none;
237
+ cursor: pointer;
238
+ }
239
+ .u-sheet__fx-label:hover:not(:disabled) {
240
+ background-color: var(--u-bg-color-hover);
241
+ color: var(--u-text-color-main);
242
+ }
243
+ .u-sheet__fx-label:disabled {
244
+ color: var(--u-text-color-disabled);
245
+ cursor: not-allowed;
233
246
  }
234
247
  .u-sheet__fx-input {
235
248
  display: block;
@@ -469,6 +482,93 @@
469
482
  .u-sheet__insert-image-picker:hover {
470
483
  color: var(--u-color-primary);
471
484
  }
485
+ .u-sheet {
486
+ /* ─── 函数弹框(分类导航 + 搜索 + 可选择;fx 按钮与工具栏共用)── */
487
+ }
488
+ .u-sheet__functions-popover {
489
+ display: flex;
490
+ flex-direction: column;
491
+ }
492
+ .u-sheet__functions-panel {
493
+ display: flex;
494
+ gap: var(--u-gap-small);
495
+ }
496
+ .u-sheet__functions-nav {
497
+ display: flex;
498
+ flex-direction: column;
499
+ flex-shrink: 0;
500
+ gap: 2px;
501
+ padding: 4px;
502
+ border-right: 1px solid var(--u-border-muted-color);
503
+ min-width: 96px;
504
+ }
505
+ .u-sheet__functions-nav-item {
506
+ padding: 4px 8px;
507
+ border: none;
508
+ border-radius: var(--u-radius-small);
509
+ background: transparent;
510
+ color: var(--u-text-color-second);
511
+ font-size: var(--u-font-size-main-small);
512
+ text-align: left;
513
+ white-space: nowrap;
514
+ cursor: pointer;
515
+ }
516
+ .u-sheet__functions-nav-item:hover {
517
+ background-color: var(--u-bg-color-hover);
518
+ }
519
+ .u-sheet__functions-nav-item.is-active {
520
+ background-color: var(--u-bg-color-hover);
521
+ color: var(--u-color-primary);
522
+ font-weight: 600;
523
+ }
524
+ .u-sheet__functions-main {
525
+ display: flex;
526
+ flex-direction: column;
527
+ gap: 6px;
528
+ flex: 1;
529
+ min-width: 0;
530
+ }
531
+ .u-sheet__functions-search {
532
+ width: 320px;
533
+ }
534
+ .u-sheet__functions-scroll {
535
+ width: 320px;
536
+ }
537
+ .u-sheet__functions-list {
538
+ display: flex;
539
+ flex-direction: column;
540
+ gap: 2px;
541
+ }
542
+ .u-sheet__functions-item {
543
+ display: flex;
544
+ flex-direction: column;
545
+ gap: 2px;
546
+ padding: 6px 8px;
547
+ border-radius: var(--u-radius-small);
548
+ cursor: pointer;
549
+ }
550
+ .u-sheet__functions-item:hover {
551
+ background-color: var(--u-bg-color-hover);
552
+ }
553
+ .u-sheet__functions-item.is-active {
554
+ background-color: var(--u-bg-color-hover);
555
+ }
556
+ .u-sheet__functions-signature {
557
+ color: var(--u-text-color-main);
558
+ font-size: var(--u-font-size-main-small);
559
+ font-weight: 600;
560
+ font-family: "JetBrains Mono", Consolas, "Courier New", monospace;
561
+ }
562
+ .u-sheet__functions-description {
563
+ color: var(--u-text-color-second);
564
+ font-size: var(--u-font-size-assist-small);
565
+ }
566
+ .u-sheet__functions-empty {
567
+ padding: 16px 0;
568
+ color: var(--u-text-color-second);
569
+ font-size: var(--u-font-size-main-small);
570
+ text-align: center;
571
+ }
472
572
  .u-sheet__insert-image-hint {
473
573
  padding: 8px 10px;
474
574
  border: 1px dashed var(--u-border-muted-color);
@@ -15,6 +15,22 @@ const COMMON_FORMULA_NAMES = [
15
15
  "AND",
16
16
  "OR"
17
17
  ];
18
+ /**
19
+ * 函数弹框分类导航固定集合(顺序即展示顺序)。
20
+ * 「常用」「全部」是导航概念(非注册表分类):常用 = COMMON_FORMULA_NAMES 固定清单,
21
+ * 全部 = 全部已注册函数;其余与 sheet-core `FormulaFunctionCategory` 对应。
22
+ */
23
+ const FUNCTION_POPUP_CATEGORIES = [
24
+ "常用",
25
+ "全部",
26
+ "财务",
27
+ "日期与时间",
28
+ "数学",
29
+ "统计",
30
+ "查找与引用",
31
+ "文本",
32
+ "逻辑"
33
+ ];
18
34
  /** 函数名 token 前合法的「触发」字符(= / 运算符 / ( / ,) */
19
35
  const SUGGEST_TRIGGER_CHARS = /* @__PURE__ */ new Set([
20
36
  "=",
@@ -123,4 +139,4 @@ function moveSuggestIndex(activeIndex, length, delta) {
123
139
  return ((activeIndex % length + length) % length + delta + length) % length;
124
140
  }
125
141
  //#endregion
126
- export { COMMON_FORMULA_NAMES, applySuggest, filterFormulaSuggestions, formatFunctionSignature, getSuggestContext, moveSuggestIndex };
142
+ export { COMMON_FORMULA_NAMES, FUNCTION_POPUP_CATEGORIES, applySuggest, filterFormulaSuggestions, formatFunctionSignature, getSuggestContext, moveSuggestIndex };
@@ -72,7 +72,7 @@ function useSheetGrid(options) {
72
72
  onContextMenu: handleContextMenu,
73
73
  onEditStart: (addr) => formulaBarRef.value?.mirrorGridEdit(addr),
74
74
  onEditEnd: (addr) => formulaBarRef.value?.exitMirror(addr),
75
- interceptSelection: () => formulaBarRef.value?.isRefSelecting() ?? false,
75
+ interceptSelection: () => (formulaBarRef.value?.isRefSelecting() ?? false) || (formulaBarRef.value?.isFunctionPopupOpen() ?? false) || (options.isFunctionPopupOpen?.() ?? false),
76
76
  onSelectionIntercept: (range) => formulaBarRef.value?.handleRefSelect(range)
77
77
  });
78
78
  let dirty = false;
@@ -4,18 +4,20 @@ import { onBeforeUnmount, onMounted, shallowRef } from "vue";
4
4
  /**
5
5
  * 面板打开期间的写入是否合并为一个 undo 单元(关闭时提交):
6
6
  * 填充颜色 / 边框 / 字体颜色 / 字号参与事务;查找(每次替换独立 undo)、
7
- * 导出(下载侧效应,无模型写入)不参与。导入无弹层,不经此路径。
7
+ * 函数(选中仅进入公式栏编辑态,模型写入待 fx 提交)、导出(下载侧效应,
8
+ * 无模型写入)不参与。导入无弹层,不经此路径。
8
9
  */
9
10
  function joinsTransaction(tool) {
10
11
  return tool.popup === "fill-color" || tool.popup === "border" || tool.popup === "font-color" || tool.popup === "font-size";
11
12
  }
12
13
  /**
13
- * 弹层型工具编排(填充颜色 / 边框 / 查找 / 插入图片 / 导出):
14
+ * 弹层型工具编排(填充颜色 / 边框 / 查找 / 函数 / 插入图片 / 导出):
14
15
  * - popupTool:当前打开的弹层工具(null = 未打开)
15
16
  * - popupAnchor:触发按钮元素(面板定位参照,见 sheet.vue 的 left 计算)
16
17
  * - 打开 / 关闭时的事务包裹(面板期间写入合并为一个 undo 单元,关闭时提交)
17
18
  * - 点击面板外关闭(面板内 @click.stop 不冒泡到 window)
18
19
  * - Ctrl/Cmd+F 开合查找条(与工具按钮同一 toggle 逻辑)
20
+ * 函数面板选中只进入公式栏编辑态(无直接模型写入),与查找 / 导出一样不参与事务。
19
21
  * 导入在 sheet.vue 特殊处理(直接系统文件选择),不经弹层。
20
22
  */
21
23
  function useToolPopup(context, rootEl) {