Files
test1/components/image-editor/hooks/use-element-actions.ts
2026-03-20 07:33:46 +00:00

228 lines
5.9 KiB
TypeScript

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<TextStyle>({
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<TextStyle> }) => {
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,
}
}