初始化模版工程
This commit is contained in:
227
components/image-editor/hooks/use-element-actions.ts
Normal file
227
components/image-editor/hooks/use-element-actions.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user