import { useCallback, useEffect, useRef } from 'react' import { v4 as uuidv4 } from 'uuid' import { toast } from 'sonner' import { type ImageElement, type SceneElement, type TextStyle, useDominoStoreInstance, } from '../components/canvas' import { MAX_IMAGE_SIZE_BYTES } from '../consts' import { getImageDimension, isValidImageFormat, uploadImage, waitForElement, } from '../utils/helper' export function useElementActions( taskId: string, addElementToFlow: (elementTemplate: SceneElement) => SceneElement, scrollIntoView?: (elementId: string) => void, ) { const store = useDominoStoreInstance() const lastTextStyleRef = useRef({ fontSize: 48, color: '#000000', fontFamily: undefined, fontWeight: 'normal', fontStyle: 'normal', textAlign: 'left', }) useEffect(() => { let lastSelection = store.getState().selectedIds return store.subscribe(state => { const selectedIds = state.selectedIds if (selectedIds === lastSelection) return lastSelection = selectedIds const lastId = selectedIds[selectedIds.length - 1] const el = state.elements[lastId] if (el?.type === 'text') { const textEl = el lastTextStyleRef.current = { fontSize: textEl.fontSize, fontWeight: textEl.fontWeight, color: textEl.color, fontFamily: textEl.fontFamily, lineHeight: textEl.lineHeight, textAlign: textEl.textAlign, fontStyle: textEl.fontStyle, } } }) }, [store]) const handleAddArtboard = useCallback(() => { const id = uuidv4() addElementToFlow({ id, type: 'artboard', x: 0, y: 0, width: 750, height: 1750, rotation: 0, originalWidth: 750, originalHeight: 1750, childrenIds: [], background: '#FFFFFF', lockAspectRatio: false, }) scrollIntoView?.(id) }, [addElementToFlow, scrollIntoView]) const handleAddText = useCallback( (options?: { content?: string; style?: Partial }) => { const id = uuidv4() const lastTextStyle = lastTextStyleRef.current const addedElement = addElementToFlow({ id, type: 'text', fontSize: 48, x: 0, y: 0, width: 0, height: 100, rotation: 0, originalWidth: 200, originalHeight: 100, content: options?.content || 'New Text', resize: 'horizontal', ...lastTextStyle, ...options?.style, }) scrollIntoView?.(id) // 等待 DOM 渲染后再选中,确保能获取到正确的元素尺寸 waitForElement(id).then(() => { store.getState().setSelectedIds([id]) }) return addedElement }, [addElementToFlow, scrollIntoView, store], ) const handleAddImageFromFile = useCallback( async (file: File) => { const { name, type } = file const isImage = type.startsWith('image/') if (!isImage && !isValidImageFormat(name)) { toast.error('不合法的图片格式') return } if (file.size >= MAX_IMAGE_SIZE_BYTES) { toast.error('图片大小不能超过 20MB') return } const objectUrl = URL.createObjectURL(file) const { removeElement, addElement, updateElementUIState } = store.getState() const tempId = uuidv4() try { const { width, height } = await getImageDimension(objectUrl) // Optimistic update const optimisticImage: ImageElement = { id: tempId, type: 'image', x: 0, y: 0, width, height, rotation: 0, src: objectUrl, fileName: name, originalWidth: width, originalHeight: height, data: { size: file.size, }, } const addedElement = addElementToFlow(optimisticImage) as ImageElement updateElementUIState(tempId, { status: 'pending', statusText: '处理中...', }) scrollIntoView?.(tempId) let url = objectUrl let file_path = '' let size = file.size try { // Upload const res = await uploadImage(taskId, file) url = res.url file_path = res.file_path size = res.size } catch (e) { console.warn('Preload failed', e) } // Replace optimistic image with permanent one removeElement(tempId) const newImage: ImageElement = { ...addedElement, id: file_path, src: url, data: { path: file_path, size, }, } addElement(newImage) updateElementUIState(file_path, { status: 'idle', }) store.getState().setFocusedElementId(file_path) scrollIntoView?.(file_path) if (url !== objectUrl) { URL.revokeObjectURL(objectUrl) } } catch (error) { console.error('Failed to add image:', error) removeElement(tempId) toast.error('添加失败') URL.revokeObjectURL(objectUrl) } }, [taskId, addElementToFlow, scrollIntoView, store], ) const handleAddImage = useCallback(() => { const input = document.createElement('input') input.type = 'file' input.accept = 'image/*' input.onchange = async e => { const file = (e.target as HTMLInputElement).files?.[0] if (!file) return handleAddImageFromFile(file) } input.click() }, [handleAddImageFromFile]) const handleDeleteElements = useCallback( async (ids: string[]) => { if (ids.length === 0) return const { removeElement, takeSnapshot } = store.getState() takeSnapshot() ids.forEach(id => removeElement(id)) }, [store], ) return { handleAddArtboard, handleAddImage, handleAddImageFromFile, handleAddText, handleDeleteElements, } }