feat(desktop): 完成编辑快捷键与警告框菜单

This commit is contained in:
2026-09-07 22:33:10 +08:00
parent c3061fdf12
commit 51105f6e53
8 changed files with 151 additions and 17 deletions
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import { resolveEditorShortcut } from './lifecycle'
const key = (code: string, options: Partial<Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>> = {}) =>
resolveEditorShortcut({ code, ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, ...options })
describe('桌面编辑快捷键', () => {
it('映射正文与六级标题', () => {
expect(key('Digit0', { ctrlKey: true })).toEqual({ id: 'editor.paragraph' })
expect(key('Digit6', { ctrlKey: true })).toEqual({ id: 'editor.heading', params: 6 })
})
it('映射格式、列表与警告框', () => {
expect(key('Digit5', { altKey: true, shiftKey: true })?.id).toBe('editor.strikethrough')
expect(key('BracketRight', { ctrlKey: true, shiftKey: true })?.id).toBe('editor.bullet-list')
expect(key('KeyC', { ctrlKey: true, altKey: true })).toMatchObject({ id: 'editor.callout', params: { type: 'note' } })
expect(key('KeyK', { ctrlKey: true })?.id).toBe('editor.link')
expect(key('KeyT', { ctrlKey: true, altKey: true })?.id).toBe('editor.table')
})
})
+50 -4
View File
@@ -6,24 +6,70 @@ import { executeEditorCommand, updateNativeEditorMenu } from '@/services/editorC
import { watch } from 'vue'
import { isDesktop } from './desktop'
type ShortcutCommand = { id: Parameters<typeof executeEditorCommand>[0]; params?: unknown }
/** 桌面菜单快捷键表;使用 code 避免数字键受键盘布局影响。 */
export function resolveEditorShortcut(event: Pick<KeyboardEvent, 'code' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>): ShortcutCommand | undefined {
const mod = event.ctrlKey || event.metaKey
if (mod && !event.altKey && !event.shiftKey && /^Digit[0-6]$/.test(event.code)) {
const level = Number(event.code.at(-1))
return level === 0 ? { id: 'editor.paragraph' } : { id: 'editor.heading', params: level }
}
const key = `${mod ? 'M' : ''}${event.altKey ? 'A' : ''}${event.shiftKey ? 'S' : ''}:${event.code}`
const commands: Record<string, ShortcutCommand> = {
'AS:Digit5': { id: 'editor.strikethrough' },
'M:Backquote': { id: 'editor.inline-code' },
'M:KeyK': { id: 'editor.link' },
'MS:KeyK': { id: 'editor.code-block' },
'MS:KeyM': { id: 'editor.math-block' },
'MA:KeyC': { id: 'editor.callout', params: { type: 'note', body: '提示内容' } },
'MA:KeyT': { id: 'editor.table' },
'MS:KeyL': { id: 'editor.inline-math' },
'MS:KeyH': { id: 'editor.horizontal-rule' },
'MS:BracketLeft': { id: 'editor.ordered-list' },
'MS:BracketRight': { id: 'editor.bullet-list' },
'MS:KeyX': { id: 'editor.task-list' },
'MS:KeyQ': { id: 'editor.blockquote' },
}
return commands[key]
}
async function executeMetadataCommand() {
const result = await executeEditorCommand('editor.import-note-properties')
if (!result.ok) await executeEditorCommand('editor.metadata.edit')
}
export async function installDesktopLifecycle() {
if (!isDesktop()) return
const activeEditor = useEditorStore()
// 保存属于窗口级命令;编辑区原生处理撤销/重做,避免重复派发。
window.addEventListener('keydown', event => {
if (event.defaultPrevented || event.isComposing || event.repeat || event.altKey || event.shiftKey) return
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return
if (event.defaultPrevented || event.isComposing || event.repeat) return
const target = event.target as HTMLElement | null
if (target?.closest('input, textarea, select, dialog[open], [role="dialog"]')) return
if ((event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 's') {
if (!activeEditor.currentFilePath || ['saving', 'conflict', 'external_changed'].includes(activeEditor.saveStatus)) return
event.preventDefault()
void activeEditor.save()
return
}
if ((event.ctrlKey || event.metaKey) && event.altKey && !event.shiftKey && event.code === 'KeyP') {
event.preventDefault()
void executeMetadataCommand()
return
}
const shortcut = resolveEditorShortcut(event)
if (!shortcut) return
if (!activeEditor.currentFilePath || ['saving', 'conflict', 'external_changed'].includes(activeEditor.saveStatus)) return
event.preventDefault()
void activeEditor.save()
void executeEditorCommand(shortcut.id, shortcut.params)
})
watch(() => [activeEditor.currentFilePath, activeEditor.saveStatus, activeEditor.mode], updateNativeEditorMenu, { flush: 'post' })
await listen<string>('editor-command', event => {
const focused = document.activeElement as HTMLElement | null
if (focused?.closest('input, textarea, select, dialog[open], [role="dialog"]')) return
void executeEditorCommand(event.payload)
if (event.payload === 'editor.import-note-properties') void executeMetadataCommand()
else void executeEditorCommand(event.payload)
})
let closing = false
await listen('host-close-requested', async () => {