@veltra/sheet 2.5.6 → 2.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/sheet/formula-bar.js +115 -15
- package/dist/components/sheet/import-file.js +1 -1
- package/dist/components/sheet/popup-helpers.js +1 -1
- package/dist/components/sheet/popups/border-popup.js +2 -2
- package/dist/components/sheet/popups/functions-popup.js +117 -0
- package/dist/components/sheet/popups/import.worker.d.ts +3 -4
- package/dist/components/sheet/popups/import.worker.js +1 -1
- package/dist/components/sheet/sheet-context-menu.js +1 -1
- package/dist/components/sheet/sheet.js +33 -23
- package/dist/components/sheet/style2.css +100 -0
- package/dist/components/sheet/use-find-replace.js +2 -2
- package/dist/components/sheet/use-formula-suggest.js +18 -2
- package/dist/components/sheet/use-sheet-grid.js +1 -1
- package/dist/components/sheet/use-sheet-state.js +1 -1
- package/dist/components/sheet/use-tool-popup.js +4 -2
- package/dist/tools/builtin.js +12 -2
- package/dist/tools/context.d.ts +15 -16
- package/dist/tools/download.js +1 -1
- package/dist/tools/export.worker.d.ts +4 -5
- package/dist/tools/export.worker.js +1 -1
- package/dist/tools/registry.d.ts +8 -9
- package/dist/tools/registry.js +1 -1
- package/dist/types/sheet.d.ts +7 -8
- package/package.json +5 -5
- package/src/components/sheet/__test__/formula-bar.test.ts +314 -2
- package/src/components/sheet/__test__/functions-popup.test.ts +182 -0
- package/src/components/sheet/__test__/import-file.test.ts +3 -3
- package/src/components/sheet/__test__/insert-image-popup.test.ts +1 -1
- package/src/components/sheet/__test__/insert-image.test.ts +1 -1
- package/src/components/sheet/__test__/sheet-component.test.ts +2 -1
- package/src/components/sheet/__test__/sheet-context-menu.test.ts +2 -2
- package/src/components/sheet/__test__/use-formula-suggest.test.ts +5 -4
- package/src/components/sheet/formula-bar.vue +134 -9
- package/src/components/sheet/import-file.ts +3 -3
- package/src/components/sheet/insert-image.ts +1 -1
- package/src/components/sheet/popup-helpers.ts +3 -3
- package/src/components/sheet/popups/border-popup.vue +3 -3
- package/src/components/sheet/popups/find-popup.vue +1 -1
- package/src/components/sheet/popups/functions-popup.vue +141 -0
- package/src/components/sheet/popups/import.worker.ts +2 -2
- package/src/components/sheet/sheet-context-menu.ts +2 -2
- package/src/components/sheet/sheet-tabs.vue +2 -2
- package/src/components/sheet/sheet.vue +15 -2
- package/src/components/sheet/style.scss +117 -0
- package/src/components/sheet/use-find-replace.ts +3 -3
- package/src/components/sheet/use-formula-suggest.ts +20 -1
- package/src/components/sheet/use-sheet-grid.ts +12 -4
- package/src/components/sheet/use-sheet-state.ts +2 -2
- package/src/components/sheet/use-tool-popup.ts +4 -2
- package/src/tools/__test__/apply-style.test.ts +2 -2
- package/src/tools/__test__/builtin.test.ts +12 -4
- package/src/tools/__test__/context.test.ts +2 -2
- package/src/tools/apply-style.ts +3 -3
- package/src/tools/builtin.ts +15 -3
- package/src/tools/context.ts +11 -11
- package/src/tools/download.ts +2 -2
- package/src/tools/export.worker.ts +3 -3
- package/src/tools/registry.ts +2 -1
- package/src/types/sheet.ts +2 -2
|
@@ -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";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import _sfc_main$2 from "./popups/functions-popup.js";
|
|
4
|
+
import { insertRefText, isRefSelectContext } from "./use-formula-ref-select.js";
|
|
5
|
+
import { formatAddress, formatRange, parseRange } from "@veltra/sheet-core/core/address.js";
|
|
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"
|
|
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 (!
|
|
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)
|
|
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("
|
|
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[
|
|
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,
|
|
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[
|
|
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[
|
|
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[
|
|
528
|
+
onMousedown: _cache[7] || (_cache[7] = withModifiers(() => {}, ["prevent"])),
|
|
429
529
|
onClick: cancelEdit
|
|
430
530
|
}, " ✗ ", 34)) : createCommentVNode("v-if", true)
|
|
431
531
|
], 2);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { message, messageConfirm } from "@veltra/desktop";
|
|
2
2
|
import { nextFrame } from "@veltra/utils";
|
|
3
|
-
import { importCsv, importXlsx, replaceWorkbookWithSnapshots } from "@veltra/sheet-core/core/io/import";
|
|
3
|
+
import { importCsv, importXlsx, replaceWorkbookWithSnapshots } from "@veltra/sheet-core/core/io/import.js";
|
|
4
4
|
//#region src/components/sheet/import-file.ts
|
|
5
5
|
/** 导入文件 accept(工具栏直接选文件) */
|
|
6
6
|
const IMPORT_FILE_ACCEPT = ".xlsx,.csv";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BORDER_STYLE_WIDTH } from "@veltra/sheet-core/core/style/types";
|
|
1
|
+
import { BORDER_STYLE_WIDTH } from "@veltra/sheet-core/core/style/types.js";
|
|
2
2
|
//#region src/components/sheet/popup-helpers.ts
|
|
3
3
|
/** 当前选区(ranges[0] 优先;无区域选区时用活动格单格) */
|
|
4
4
|
function currentRange(context) {
|
|
@@ -3,8 +3,8 @@ import _sfc_main$1 from "./border-preset-glyph.js";
|
|
|
3
3
|
import { Fragment, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, ref, renderList, unref } from "vue";
|
|
4
4
|
import { UPalette } from "@veltra/desktop";
|
|
5
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";
|
|
6
|
+
import { buildBorderPresetItems } from "@veltra/sheet-core/core/style/border-presets.js";
|
|
7
|
+
import { BORDER_STYLE_WIDTH } from "@veltra/sheet-core/core/style/types.js";
|
|
8
8
|
//#region src/components/sheet/popups/border-popup.vue
|
|
9
9
|
const _hoisted_1 = ["title", "onClick"];
|
|
10
10
|
const _hoisted_2 = ["title", "onClick"];
|
|
@@ -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.js";
|
|
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
|
-
import { SheetSnapshot } from "@veltra/sheet-core/core/sheet";
|
|
1
|
+
import { SheetSnapshot } from "@veltra/sheet-core/core/sheet.js";
|
|
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
|
|
@@ -3,7 +3,7 @@ self.onmessage = (e) => {
|
|
|
3
3
|
const { buffer } = e.data;
|
|
4
4
|
(async () => {
|
|
5
5
|
try {
|
|
6
|
-
const { importXlsx } = await import("@veltra/sheet-core/core/io/import");
|
|
6
|
+
const { importXlsx } = await import("@veltra/sheet-core/core/io/import.js");
|
|
7
7
|
let lastPercent = -1;
|
|
8
8
|
const wb = await importXlsx(new Uint8Array(buffer), (done, total) => {
|
|
9
9
|
const percent = total > 0 ? Math.floor(done / total * 10) : 10;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defaultToolRegistry } from "../../tools/registry.js";
|
|
2
2
|
import { pickAndInsertImage } from "./insert-image.js";
|
|
3
3
|
import _sfc_main from "./insert-count-menu-item.js";
|
|
4
|
-
import { createRange } from "@veltra/sheet-core/core/address";
|
|
4
|
+
import { createRange } from "@veltra/sheet-core/core/address.js";
|
|
5
5
|
import { defineComponent, h } from "vue";
|
|
6
6
|
const INSERT_COUNT_MAX = 1e3;
|
|
7
7
|
/** 选区覆盖行/列数 → 插入默认 N(min 1 / max 1000) */
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import _sfc_main$1 from "./
|
|
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$
|
|
4
|
-
import _sfc_main$
|
|
5
|
-
import _sfc_main$
|
|
6
|
-
import _sfc_main$
|
|
7
|
-
import _sfc_main$
|
|
8
|
-
import _sfc_main$
|
|
9
|
-
import _sfc_main$
|
|
10
|
-
import _sfc_main$
|
|
11
|
-
import _sfc_main$
|
|
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$
|
|
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("
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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 === "
|
|
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$
|
|
169
|
-
key:
|
|
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$
|
|
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$
|
|
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);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed, ref, shallowRef, watch } from "vue";
|
|
2
2
|
import { debounce } from "@cat-kit/core";
|
|
3
|
-
import { inferCellType, normalizeInputValue } from "@veltra/sheet-core/core/cell-store";
|
|
4
|
-
import { findAll, findNextFrom, findPrevFrom } from "@veltra/sheet-core/core/find";
|
|
3
|
+
import { inferCellType, normalizeInputValue } from "@veltra/sheet-core/core/cell-store.js";
|
|
4
|
+
import { findAll, findNextFrom, findPrevFrom } from "@veltra/sheet-core/core/find.js";
|
|
5
5
|
//#region src/components/sheet/use-find-replace.ts
|
|
6
6
|
/**
|
|
7
7
|
* 查找条逻辑(关键词 / 上一个 / 下一个 / 命中计数 / 替换):
|