diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 2f78314f66..95d41c5f8c 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -13,16 +13,19 @@ import { ChevronUp, CircleHelp, Cloud, + Code, Copy, Download, ExternalLink, Eye, Filter, + Globe, GripVertical, Loader2, Moon, PackageSearch, Palette, + PanelLeft, Pencil, Plus, RefreshCw, @@ -32,6 +35,7 @@ import { Sun, Star, SunMoon, + Table, Terminal, Trash2, Upload, @@ -167,7 +171,8 @@ import { type WebDavConfig, } from "@/lib/backend/api"; import { eventToModifierOnlyShortcut, eventToShortcut } from "@/lib/editor/keyboardShortcuts"; -import { SHORTCUT_DEFINITIONS, findShortcutConflict, isReservedShortcut, normalizeShortcutSettings, type ShortcutActionId } from "@/lib/editor/shortcutRegistry"; +import { SHORTCUT_DEFINITIONS, countShortcutConflictPairs, findCrossScopeShortcutConflicts, findShortcutConflict, isReservedShortcut, normalizeShortcutSettings, type ShortcutActionId, type ShortcutDefinition, type ShortcutScope } from "@/lib/editor/shortcutRegistry"; +import LightTooltip from "@/components/ui/LightTooltip.vue"; import { formatShortcutDisplay } from "@/lib/editor/shortcutDisplay"; import { COLUMN_NAME_COPY_SEPARATOR_LABELS, COLUMN_NAME_COPY_SEPARATOR_OPTIONS, isColumnNameCopySeparator, type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy"; import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay"; @@ -1544,12 +1549,24 @@ watch( { deep: true }, ); -const shortcutConflicts = computed(() => - SHORTCUT_DEFINITIONS.flatMap((definition) => { +// 行 → 同作用域冲突对象(必顶阻断)。保留 map 形态是为了能算“重复对数”: +// 行级列表会把同一对重复计两次(双向各一次),而摘要文案说的是“几组重复”。 +const shortcutConflictMap = computed(() => { + const conflicts: Partial> = {}; + for (const definition of SHORTCUT_DEFINITIONS) { const conflict = findShortcutConflict(definition.id, editShortcuts.value[definition.id], editShortcuts.value); - return conflict ? [definition.id] : []; - }), -); + if (conflict) conflicts[definition.id] = conflict; + } + return conflicts; +}); +const shortcutConflicts = computed(() => Object.keys(shortcutConflictMap.value) as ShortcutActionId[]); +// 行 → 跨作用域同键对象(**仅提示**,不进应用门禁)。不同作用域共用组合是 +// 有意的设计(find / focusSearch 默认都是 Mod+F,运行时按焦点路由),所以这里 +// 只用于展示;normalizeShortcutSettings 的占用判定依旧只看同作用域。 +const crossScopeShortcutConflicts = computed(() => findCrossScopeShortcutConflicts(editShortcuts.value)); +const crossScopeShortcutConflictIds = computed(() => Object.keys(crossScopeShortcutConflicts.value) as ShortcutActionId[]); +const shortcutConflictPairCount = computed(() => countShortcutConflictPairs(shortcutConflictMap.value)); +const crossScopeShortcutPairCount = computed(() => countShortcutConflictPairs(crossScopeShortcutConflicts.value)); const sqlShortcutConflicts = computed(() => findSqlShortcutConflicts(editSqlShortcuts.value, editShortcuts.value)); const hasSqlShortcutConflicts = computed(() => sqlShortcutConflicts.value.length > 0); const shortcutSearchQuery = ref(""); @@ -1582,11 +1599,103 @@ const filteredShortcutDefinitions = computed(() => { const query = shortcutSearchQuery.value.trim().toLowerCase(); if (!query) return SHORTCUT_DEFINITIONS; return SHORTCUT_DEFINITIONS.filter((definition) => { - const scope = t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`); + const scope = t(shortcutScopeLabelKey(definition.scope)); const shortcut = formatShortcutPill(editShortcuts.value[definition.id]); return [definition.id, t(definition.labelKey), scope, shortcut].some((value) => value.toLowerCase().includes(query)); }); }); + +// 二级归类:快捷键页签按作用域分组展示。顺序即运行时优先级——越外层先响应, +// 用户读到的顺序与事件实际分发顺序一致。 +const SHORTCUT_SCOPE_ORDER: readonly ShortcutScope[] = ["global", "editor", "grid", "search", "sidebar"]; + +function shortcutScopeLabelKey(scope: ShortcutScope): string { + return `settings.shortcutScope${scope[0].toUpperCase()}${scope.slice(1)}`; +} + +function shortcutScopeHintKey(scope: ShortcutScope): string { + return `settings.shortcutScopeHint${scope[0].toUpperCase()}${scope.slice(1)}`; +} + +function shortcutScopeIcon(scope: ShortcutScope) { + switch (scope) { + case "global": + return Globe; + case "editor": + return Code; + case "grid": + return Table; + case "search": + return Search; + default: + return PanelLeft; + } +} + +interface ShortcutScopeGroup { + scope: ShortcutScope; + label: string; + hint: string; + definitions: ShortcutDefinition[]; + unboundCount: number; + conflictCount: number; + crossScopeCount: number; +} + +// 计数一律基于“当前可见行”(受搜索过滤影响):组头是它下方那份列表的摘要, +// 拿全局总数会导致搜索时组头数目与实际行数对不上。 +const shortcutScopeGroups = computed(() => + SHORTCUT_SCOPE_ORDER.map((scope) => { + const definitions = filteredShortcutDefinitions.value.filter((definition) => definition.scope === scope); + return { + scope, + label: t(shortcutScopeLabelKey(scope)), + hint: t(shortcutScopeHintKey(scope)), + definitions, + unboundCount: definitions.filter((definition) => !editShortcuts.value[definition.id]).length, + conflictCount: definitions.filter((definition) => shortcutConflictMap.value[definition.id]).length, + crossScopeCount: definitions.filter((definition) => (crossScopeShortcutConflicts.value[definition.id] ?? []).length > 0).length, + }; + }).filter((group) => group.definitions.length > 0), +); + +function isShortcutModified(definition: ShortcutDefinition): boolean { + return editShortcuts.value[definition.id] !== definition.defaultShortcut; +} + +function shortcutDefinitionById(actionId: ShortcutActionId): ShortcutDefinition | undefined { + return SHORTCUT_DEFINITIONS.find((definition) => definition.id === actionId); +} + +// 同作用域冲突的浮层文案:说清“与谁重复”与“为什么必须改”。 +function shortcutConflictTooltipText(definition: ShortcutDefinition): string { + const partnerId = shortcutConflictMap.value[definition.id]; + const partner = partnerId ? shortcutDefinitionById(partnerId) : undefined; + if (!partner) return ""; + return t("settings.shortcutConflictTooltip", { label: t(partner.labelKey), scope: t(shortcutScopeLabelKey(partner.scope)) }); +} + +// 跨作用域同键的浮层文案:列全对方动作及其作用域,并说明“无需处理”。 +function shortcutCrossScopeTooltipText(definition: ShortcutDefinition): string { + const others = crossScopeShortcutConflicts.value[definition.id] ?? []; + const targets = others + .map((actionId) => shortcutDefinitionById(actionId)) + .filter((partner): partner is ShortcutDefinition => !!partner) + .map((partner) => `${t(shortcutScopeLabelKey(partner.scope))} · ${t(partner.labelKey)}`) + .join(t("settings.shortcutCrossScopeTooltipSeparator")); + if (!targets) return ""; + return t("settings.shortcutCrossScopeTooltip", { targets }); +} + +function shortcutHasCrossScopeConflict(definition: ShortcutDefinition): boolean { + return (crossScopeShortcutConflicts.value[definition.id] ?? []).length > 0; +} + +// 行级浮层的唯一入口:优先讲阻断性冲突(同作用域),其次才是跨作用域提示。 +// 返回空串时 LightTooltip 不展示(hasContent 为假),无需额外开关。 +function shortcutConflictHintText(definition: ShortcutDefinition): string { + return shortcutConflictTooltipText(definition) || shortcutCrossScopeTooltipText(definition); +} const hasShortcutConflicts = computed(() => shortcutConflicts.value.length > 0); const shortcutsChanged = computed(() => JSON.stringify(editShortcuts.value) !== JSON.stringify(editEditorSettingsBase.value.shortcuts)); const sqlShortcutsChanged = computed(() => JSON.stringify(editSqlShortcuts.value) !== JSON.stringify(editEditorSettingsBase.value.sqlShortcuts)); @@ -7554,81 +7663,131 @@ onUnmounted(() => {
-
- - +
+
+ + +
+ + + + + + + + +
-
-
- {{ t("settings.shortcutSearchNoResults") }} -
-
-
-
- - - {{ t(`settings.shortcutScope${definition.scope[0].toUpperCase()}${definition.scope.slice(1)}`) }} - + +
+ {{ t("settings.shortcutSearchNoResults") }} +
+ + +
+
+
+
+
+
+
+ + + + + {{ t("settings.shortcutModifiedTag") }} + + +
+
+
+
+ + + + + + + + +
-
-
- - - - - -
-

- {{ t("settings.shortcutConflict") }} -

-
-
+
@@ -9569,6 +9728,11 @@ LIMIT 100; {{ t("settings.resetDefaults") }} + + +