feat: 快捷键录入重绑(FR-K3,keymap:load/save IPC+config.json,SettingsPanel 录入 UI+冲突检测,use-hotkeys 读 store 实时生效,重开恢复)

This commit is contained in:
hunter-dev
2026-08-24 23:48:17 +08:00
parent cf2600901b
commit 49c174851e
9 changed files with 166 additions and 36 deletions
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useSettings } from './settings-store';
import { useKeymap } from '../keymap/keymap-service';
const pageBtn: React.CSSProperties = { padding: '8px 14px', border: 'none', background: 'transparent', color: '#888', cursor: 'pointer', borderBottom: '2px solid transparent', fontSize: 13 };
const row: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: '1px solid #2a2a3a' };
@@ -9,6 +10,30 @@ const toggle = (on: boolean): React.CSSProperties => ({ width: 36, height: 20, b
export function SettingsPanel({ onClose }: { onClose: () => void }) {
const s = useSettings();
const [page, setPage] = useState<'keymap' | 'editor' | 'path' | 'other'>('editor');
const { bindings, setCombo, resetAll, save: saveKeymap, conflicts } = useKeymap();
const [recId, setRecId] = useState<string | null>(null);
useEffect(() => {
if (!recId) return;
const onKey = (e: KeyboardEvent) => {
e.preventDefault();
if (e.key === 'Escape') { setRecId(null); return; }
if (!e.ctrlKey && !e.metaKey && !e.altKey) return;
const parts: string[] = [];
if (e.ctrlKey) parts.push('ctrl');
if (e.shiftKey) parts.push('shift');
if (e.altKey) parts.push('alt');
if (e.metaKey) parts.push('meta');
const k = e.key.toLowerCase();
if (k.length > 1) return;
parts.push(k);
const combo = parts.join('+');
setCombo(recId, combo);
void saveKeymap();
setRecId(null);
};
window.addEventListener('keydown', onKey, true);
return () => window.removeEventListener('keydown', onKey, true);
}, [recId, setCombo, saveKeymap]);
return (
<div onClick={e => { if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}>
<div style={{ width: 520, maxHeight: '80vh', overflow: 'auto', background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
@@ -30,7 +55,24 @@ export function SettingsPanel({ onClose }: { onClose: () => void }) {
</>}
{page === 'other' && <div style={row}><span style={lbl}>删除前确认</span><button onClick={() => s.set('confirmDelete', !s.confirmDelete)} style={toggle(s.confirmDelete)}><span style={dot(s.confirmDelete)} /></button></div>}
{page === 'path' && <div style={{ ...lbl, opacity: 0.6, padding: '12px 0' }}>文件路径设置将在 M9 完善(默认库路径配置)</div>}
{page === 'keymap' && <div style={{ ...lbl, opacity: 0.6, padding: '12px 0' }}>快捷键自定义与冲突检测已就绪(见速查面板 ?)。键位录入 UI 在此页完善。</div>}
{page === 'keymap' && <>
<div style={{ ...lbl, opacity: 0.6, marginBottom: 8 }}>点击「录入」后按下新组合键(需含 Ctrl/Meta/Alt);Esc 取消</div>
{bindings.map(b => {
const cf = conflicts();
const inConflict = Object.values(cf).some(ids => ids.includes(b.id));
return (
<div key={b.id} style={row}>
<span style={lbl}>{b.label}</span>
<span style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{inConflict && <span style={{ color: '#e88', fontSize: 11 }}>冲突</span>}
<code style={{ background: '#15151f', padding: '1px 6px', borderRadius: 3, color: recId === b.id ? '#fc4' : '#9cf', minWidth: 90, textAlign: 'center' }}>{recId === b.id ? '按下组合键…' : b.combo}</code>
<button onClick={() => setRecId(b.id)} style={{ ...sel, cursor: 'pointer' }}>录入</button>
</span>
</div>
);
})}
<button onClick={async () => { resetAll(); await saveKeymap(); }} style={{ marginTop: 12, ...sel, cursor: 'pointer' }}>重置默认</button>
</>}
<div style={{ marginTop: 16, fontSize: 12, opacity: 0.5 }}>设置已自动保存</div>
</div>
</div>