feat(M9): FR-K1/K3/K4/K6/K7 快捷键体系——多作用域(global/app/editor)+Scope.resolveActive 裁决(K1); 编辑器类绑定 find/bold/italic/strike/inlineCode/clearFormat(K3); 冲突数量汇总 conflictCount+SettingsPanel 顶部警示(K4); CheatSheet 从 store 动态渲染+formatCombo Mac 适配(K6); Ctrl+F 捕获阶段 keydown 拦截宿主查找+FindReplace 切换(K7); keymap-enhanced E2E 2 项

This commit is contained in:
hunter-dev
2026-08-26 14:43:57 +08:00
parent fcd822b9f4
commit bfb1f4b014
8 changed files with 217 additions and 29 deletions
@@ -1,16 +1,26 @@
import { create } from 'zustand';
import { IPC } from '@hunter/shared';
// KeymapService — 快捷键绑定管理 + 冲突检测(FR-K3/K4/K5)
// 注:? 帮助键保留硬编码(非 ctrl 系,不便录入);此处管理 4 个 ctrl 系全局绑定
// KeymapService — 快捷键绑定管理 + 多作用域 + 冲突检测(FR-K1/K3/K4/K5)
export type Scope = 'global' | 'app' | 'editor';
export interface Binding { id: string; scope: Scope; label: string; combo: string; defaultCombo: string; }
const DEFAULTS: Binding[] = [
// FR-K1 全局作用域——任何位置生效
{ id: 'newDoc', scope: 'global', label: '新建文档', combo: 'ctrl+n', defaultCombo: 'ctrl+n' },
{ id: 'save', scope: 'global', label: '保存', combo: 'ctrl+s', defaultCombo: 'ctrl+s' },
{ id: 'focusSearch', scope: 'global', label: '全库搜索', combo: 'ctrl+shift+f', defaultCombo: 'ctrl+shift+f' },
{ id: 'toggleOutline', scope: 'global', label: '大纲面板', combo: 'ctrl+shift+l', defaultCombo: 'ctrl+shift+l' },
// FR-K3 应用级——编辑器外也生效,但编辑器聚焦时优先编辑器
{ id: 'toggleCheat', scope: 'app', label: '快捷键速查', combo: 'ctrl+/', defaultCombo: 'ctrl+/' },
{ id: 'toggleSettings', scope: 'app', label: '设置', combo: 'ctrl+shift+,', defaultCombo: 'ctrl+shift+,' },
// FR-K3 编辑器作用域——仅编辑器聚焦时生效
{ id: 'find', scope: 'editor', label: '查找', combo: 'ctrl+f', defaultCombo: 'ctrl+f' },
{ id: 'bold', scope: 'editor', label: '加粗', combo: 'ctrl+b', defaultCombo: 'ctrl+b' },
{ id: 'italic', scope: 'editor', label: '斜体', combo: 'ctrl+i', defaultCombo: 'ctrl+i' },
{ id: 'strike', scope: 'editor', label: '删除线', combo: 'alt+shift+5', defaultCombo: 'alt+shift+5' },
{ id: 'inlineCode', scope: 'editor', label: '行内代码', combo: 'ctrl+e', defaultCombo: 'ctrl+e' },
{ id: 'clearFormat', scope: 'editor', label: '清除格式', combo: 'ctrl+\\', defaultCombo: 'ctrl+\\' },
];
interface KeymapState {
@@ -20,6 +30,9 @@ interface KeymapState {
load: () => Promise<void>;
save: () => Promise<void>;
conflicts: () => Record<string, string[]>;
conflictCount: () => number; // FR-K4:冲突条目总数
// FR-K1:作用域裁决——返回当前焦点应生效的绑定
resolveActive: (editorFocused: boolean) => Binding[];
}
export const useKeymap = create<KeymapState>((set, get) => ({
@@ -45,4 +58,25 @@ export const useKeymap = create<KeymapState>((set, get) => ({
for (const [combo, ids] of byCombo) if (ids.length > 1) out[combo] = ids;
return out;
},
conflictCount: () => {
const c = get().conflicts();
return Object.values(c).reduce((s, ids) => s + ids.length, 0);
},
resolveActive: (editorFocused: boolean) => {
const all = get().bindings;
// 编辑器聚焦时:editor + global 生效,app 作用域的非编辑器专属键也生效
// 非编辑器聚焦时:global + app 生效,editor 作用域不生效
if (editorFocused) return all.filter(b => b.scope === 'global' || b.scope === 'editor' || b.scope === 'app');
return all.filter(b => b.scope === 'global' || b.scope === 'app');
},
}));
// FR-K6:Mac 适配——将 ctrl 显示为 ⌘
export function formatCombo(combo: string): string {
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
if (!isMac) return combo.replace(/ctrl/g, 'Ctrl').replace(/shift/g, 'Shift').replace(/alt/g, 'Alt').replace(/meta/g, 'Win');
return combo.replace(/ctrl/g, '⌘').replace(/shift/g, '⇧').replace(/alt/g, '⌥').replace(/meta/g, '⌘');
}
// FR-K6:暴露 store 供 E2E 验证快捷键绑定
if (typeof window !== 'undefined') (window as any).__KEYMAP_STORE__ = useKeymap;