初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
import { useEffect } from 'react'
import type React from 'react'
import { toast } from 'sonner'
import {
useDominoStore,
useDominoStoreInstance,
type ImageElement,
type TextElement,
} from '../components/canvas'
import {
copyImageToClipboard,
copyTextElementToClipboard,
} from '../utils/helper'
import { useZoomActions } from './use-zoom-actions'
export interface UseShortcutsProps {
rootRef: React.RefObject<HTMLDivElement | null>
onDelete?: (ids: string[]) => void
onPaste?: (e: ClipboardEvent) => void
}
export function useShortcuts({ rootRef, onDelete, onPaste }: UseShortcutsProps) {
const store = useDominoStoreInstance()
const { stepZoom } = useZoomActions(rootRef)
const mode = useDominoStore(s => s.mode)
const setMode = useDominoStore(s => s.setMode)
const setViewport = useDominoStore(s => s.setViewport)
const undo = useDominoStore(s => s.undo)
const redo = useDominoStore(s => s.redo)
const selectedIds = useDominoStore(s => s.selectedIds)
const focusedElementId = useDominoStore(s => s.focusedElementId)
const readOnly = useDominoStore(s => s.readOnly)
useEffect(() => {
const isInputActive = () => {
// 1. 如果有元素正在被聚焦(比如正在编辑文字),禁用快捷键
if (focusedElementId) {
const { elements } = store.getState()
const focusedEl = elements[focusedElementId]
if (focusedEl?.type === 'text') return true
}
// 2. 如果原生 DOM 的输入框处于激活状态,禁用快捷键
const activeElement = document.activeElement
if (!activeElement) return false
return (
activeElement.tagName.toLowerCase() === 'input' ||
activeElement.tagName.toLowerCase() === 'textarea' ||
(activeElement as HTMLElement).isContentEditable
)
}
const handleKeyDown = (e: KeyboardEvent) => {
if (isInputActive()) return
const isMod = e.metaKey || e.ctrlKey
const isShift = e.shiftKey
// 1. Space -> Pan Mode
if (e.code === 'Space' && mode !== 'pan') {
e.preventDefault()
setMode('pan')
}
// 2. Undo/Redo
if (isMod && e.key.toLowerCase() === 'z') {
e.preventDefault()
if (isShift) redo()
else undo()
} else if (isMod && e.key.toLowerCase() === 'y') {
e.preventDefault()
redo()
}
// 3. Zoom Shortcuts
if (isMod && (e.key === '=' || e.key === '+')) {
e.preventDefault()
stepZoom(1)
}
if (isMod && (e.key === '-' || e.key === '_')) {
e.preventDefault()
stepZoom(-1)
}
if (e.key === '0') {
e.preventDefault()
setViewport({ scale: 1 })
}
// 4. Delete
if (e.key === 'Delete' || e.key === 'Backspace') {
if (selectedIds.length > 0 && !readOnly) {
e.preventDefault()
onDelete?.(selectedIds)
}
}
// 5. Select All
if (isMod && e.key.toLowerCase() === 'a') {
e.preventDefault()
const { elements, setSelectedIds } = store.getState()
setSelectedIds(Object.keys(elements))
}
// 6. Escape
if (e.key === 'Escape') {
const { setSelectedIds, setFocusedElementId } = store.getState()
setSelectedIds([])
setFocusedElementId(null)
if (mode !== 'select') setMode('select')
}
// 7. Arrow Keys
const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
if (ARROW_KEYS.includes(e.key) && selectedIds.length > 0 && !readOnly) {
e.preventDefault()
const step = isShift ? 10 : 1
const dx =
e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0
const dy =
e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0
const { moveElements } = store.getState()
moveElements(selectedIds, dx, dy)
}
// 8. Copy
if (isMod && e.key.toLowerCase() === 'c') {
const { elements } = store.getState()
const selectedElements = selectedIds
.map(id => elements[id])
.filter(Boolean)
if (selectedElements.length === 0) return
e.preventDefault()
if (selectedElements.length === 1) {
const el = selectedElements[0]
if (el.type === 'text') {
const textEl = el as TextElement
copyTextElementToClipboard(textEl).then(success => {
if (success) toast.success('已复制文本')
})
} else if (el.type === 'image') {
const imageEl = el as ImageElement
copyImageToClipboard(imageEl.id).then(success => {
if (success) toast.success('已复制图片')
else toast.error('复制失败')
})
}
return
}
const texts = selectedElements
.filter((el): el is TextElement => el.type === 'text')
.map(el => el.content)
if (texts.length > 0) {
navigator.clipboard
.writeText(texts.join('\n'))
.then(() => toast.success(`已复制 ${texts.length} 条内容`))
}
}
}
const handleKeyUp = (e: KeyboardEvent) => {
if (e.code === 'Space' && !isInputActive()) {
setMode('select')
}
}
const handlePaste = (e: ClipboardEvent) => {
if (isInputActive()) return
if (!onPaste) return
onPaste(e)
}
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
window.addEventListener('paste', handlePaste)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
window.removeEventListener('paste', handlePaste)
}
}, [
stepZoom,
mode,
setMode,
setViewport,
undo,
redo,
selectedIds,
focusedElementId,
readOnly,
store,
rootRef,
onDelete,
onPaste,
])
}