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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user