初始化模版工程

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,436 @@
import { useCallback } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { toast } from 'sonner'
import {
imageMatting,
imageOCR,
recognizeImage,
updateImageOCRText,
segmentLayer,
getSegmentLayerResult,
} from '../service/api'
import type {
ImageOCRResponse,
ImageOCRTextUpdateItem,
RecognizedImageElement,
} from '../service/type'
import {
type ImageElement,
type PlaceholderElement,
type SceneElement,
type ArtboardElement,
useDominoStoreInstance,
} from '../components/canvas'
import {
MAX_IMAGE_SIZE_BYTES,
MATTING_MIN_DIM,
MATTING_MAX_DIM,
NEED_COMPRESS_MAX_SIZE,
NEED_COMPRESS_MIN_SIZE,
} from '../consts'
import {
getImageFileSize,
getImageDimension,
getImageUrl,
isValidImageFormat,
isValidImageSize,
manualPersistence,
} from '../utils/helper'
export function useImageEditActions(
taskId: string,
addElementToFlow: (elementTemplate: SceneElement) => SceneElement,
onPartialRedraw?: (
element: ImageElement,
recognizedElements: readonly RecognizedImageElement[],
) => void,
) {
const store = useDominoStoreInstance()
const handleMatting = useCallback(
async (element: ImageElement) => {
const { originalWidth: w, originalHeight: h } = element
// 1. Dimension check
if (w < MATTING_MIN_DIM || h < MATTING_MIN_DIM) {
toast.error('图片尺寸不能小于 32x32px')
return
}
if (w > MATTING_MAX_DIM || h > MATTING_MAX_DIM) {
toast.error('图片尺寸不能超过 4000x4000px')
return
}
// 2. Size check
const { updateElement } = store.getState()
const fileSize = await getImageFileSize(element)
if (!element.data?.size && fileSize > 0) {
updateElement(element.id, { data: { ...element.data, size: fileSize } })
}
if (fileSize > MAX_IMAGE_SIZE_BYTES) {
toast.error('图片大小不能超过 20MB')
return
}
const placeholderId = uuidv4()
const { removePlaceholder, updateElementUIState, addElement } =
store.getState()
// Immediately insert placeholder element for instant feedback
const placeholder: PlaceholderElement = {
id: placeholderId,
type: 'placeholder',
x: 0,
y: 0,
width: element.width,
height: element.height,
rotation: element.rotation || 0,
originalWidth: element.width,
originalHeight: element.height,
label: '生成中',
}
const addedPlaceholder = addElementToFlow(
placeholder,
) as PlaceholderElement
updateElementUIState(placeholderId, { status: 'pending' })
try {
const res = await imageMatting({
image_url: element.data?.path || element.id,
task_id: taskId,
compress: fileSize > NEED_COMPRESS_MIN_SIZE,
})
try {
// Preload image
const url = await getImageUrl(taskId, res.path)
const { width, height } = await getImageDimension(url)
// Replace placeholder with matting result
removePlaceholder(placeholderId)
const newImage: ImageElement = {
...addedPlaceholder,
id: res.path,
type: 'image',
src: url,
width,
height,
fileName: res.path.split('/').pop() || 'image.jpg',
originalWidth: width,
originalHeight: height,
data: {
path: res.path,
},
}
addElement(newImage)
updateElementUIState(res.path, { status: 'idle' })
// Support background task persistence
manualPersistence(taskId, store)
} catch (e) {
console.warn('Preload failed', e)
}
} catch (error) {
console.error(error)
removePlaceholder(placeholderId)
}
},
[taskId, addElementToFlow, store],
)
const handlePartialRedraw = useCallback(
async (element: ImageElement) => {
if (!isValidImageFormat(element.fileName)) {
toast.error('不合法的图片格式')
return
}
if (!isValidImageSize(element.originalWidth, element.originalHeight)) {
toast.error('图片尺寸不能超过 2160x3840px 或 3840x2160px')
return
}
const { updateElement, updateElementUIState } = store.getState()
const fileSize = await getImageFileSize(element)
if (!element.data?.size && fileSize > 0) {
updateElement(element.id, { data: { ...element.data, size: fileSize } })
}
if (fileSize >= MAX_IMAGE_SIZE_BYTES) {
toast.error('图片大小不能超过 20MB')
return
}
updateElementUIState(element.id, {
status: 'pending',
statusText: '识图中...',
})
try {
const res = await recognizeImage({
image_url: element.data?.path || element.id,
task_id: taskId,
width: element.originalWidth,
height: element.originalHeight,
})
onPartialRedraw?.(element, res || [])
} catch (err) {
console.error('Failed to recognize image:', err)
} finally {
updateElementUIState(element.id, {
status: 'idle',
})
}
},
[taskId, onPartialRedraw, store],
)
const handleEditText = useCallback(
async (
element: ImageElement,
onOCRSuccess: (data: ImageOCRResponse) => void,
) => {
if (!isValidImageFormat(element.fileName)) {
toast.error('不合法的图片格式')
return
}
if (!isValidImageSize(element.originalWidth, element.originalHeight)) {
toast.error('图片尺寸不能超过 2160x3840px 或 3840x2160px')
return
}
const { updateElement, updateElementUIState } = store.getState()
const fileSize = await getImageFileSize(element)
if (!element.data?.size && fileSize > 0) {
updateElement(element.id, { data: { ...element.data, size: fileSize } })
}
if (fileSize >= MAX_IMAGE_SIZE_BYTES) {
toast.error('图片大小不能超过 20MB')
return
}
updateElementUIState(element.id, {
status: 'pending',
statusText: '正在提取文字',
})
const errorText = '没有提取到文字'
try {
const data = await imageOCR({
image_url: element.data?.path || element.id,
task_id: taskId,
})
if (data.length > 0) {
onOCRSuccess(data)
} else {
toast.error(errorText)
}
} catch (error) {
console.error(error)
toast.error(errorText)
} finally {
updateElementUIState(element.id, { status: 'idle' })
}
},
[taskId, store],
)
const handleConfirmEditText = useCallback(
async (element: ImageElement, updatedList: ImageOCRTextUpdateItem[]) => {
const placeholderId = uuidv4()
const { removePlaceholder, updateElementUIState, addElement } =
store.getState()
// Insert new placeholder element into the flow
const placeholder: PlaceholderElement = {
id: placeholderId,
type: 'placeholder',
x: 0,
y: 0,
width: element.width,
height: element.height,
rotation: element.rotation || 0,
originalWidth: element.width,
originalHeight: element.height,
label: '处理中...',
}
const addedPlaceholder = addElementToFlow(
placeholder,
) as PlaceholderElement
updateElementUIState(placeholderId, { status: 'pending' })
try {
const res = await updateImageOCRText({
image_url: element.data?.path || element.id,
texts: updatedList,
task_id: taskId,
compress: (element.data?.size ?? 0) > NEED_COMPRESS_MAX_SIZE,
})
// Preload next image
try {
const url = await getImageUrl(taskId, res.path)
const { width, height } = await getImageDimension(url)
// Replace placeholder with result
removePlaceholder(placeholderId)
const newImage: ImageElement = {
...addedPlaceholder,
id: res.path,
type: 'image',
src: url,
width,
height,
fileName: res.path.split('/').pop() || 'image.jpg',
originalWidth: width,
originalHeight: height,
data: {
path: res.path,
},
}
addElement(newImage)
updateElementUIState(res.path, { status: 'idle' })
// Support background task persistence
manualPersistence(taskId, store)
} catch (e) {
console.warn('Preload failed', e)
}
} catch (error) {
console.error(error)
removePlaceholder(placeholderId)
toast.error('修改失败')
}
},
[taskId, addElementToFlow, store],
)
const handleEditElements = useCallback(
async (element: ImageElement) => {
const { updateElementUIState, addElement, setSelectedIds } =
store.getState()
updateElementUIState(element.id, {
status: 'pending',
statusText: '正在分层...',
})
try {
const taskRecordId = await segmentLayer({
image_url: element.data?.path || element.id,
task_id: taskId,
width: element.originalWidth,
height: element.originalHeight,
})
// Polling loop
let task = await getSegmentLayerResult(taskRecordId)
while (task.status === 'PROCESSING') {
await new Promise(resolve => setTimeout(resolve, 2000))
task = await getSegmentLayerResult(taskRecordId)
}
if (
task.status === 'TIMEOUT' ||
task.status === 'FAILED' ||
!task.layers
) {
throw new Error('分层任务执行失败')
}
const layersData = task.layers
// 1. Prepare all layers (Sorted by layer_order from server)
const allLayersData = layersData.map(layer => ({
image: layer.image,
line_rect: {
x: layer.x,
y: layer.y,
width: layer.width,
height: layer.height,
},
line_text: layer.desc,
hideMetadata: layer.desc === 'background',
}))
// 2. Preload all layers and prepare IDs
const layers = await Promise.all(
allLayersData.map(async item => {
const itemUrl = await getImageUrl(taskId, item.image.path)
const childId = uuidv4()
return { item, itemUrl, childId }
}),
)
// 3. Create Artboard Template
const artboardId = uuidv4()
const artboardTemplate: ArtboardElement = {
id: artboardId,
type: 'artboard',
name: `画板 - ${element.fileName}`,
x: 0,
y: 0,
width: element.width,
height: element.height,
rotation: element.rotation || 0,
originalWidth: element.originalWidth,
originalHeight: element.originalHeight,
background: '#FFFFFF',
childrenIds: layers.map(l => l.childId),
}
// Add artboard to flow
const addedArtboard = addElementToFlow(
artboardTemplate,
) as ArtboardElement
// 4. Add all child elements to store
for (const layer of layers) {
const { item, itemUrl, childId } = layer
const childImage: ImageElement = {
id: childId,
type: 'image',
x: item.line_rect.x,
y: item.line_rect.y,
width: item.line_rect.width,
height: item.line_rect.height,
rotation: 0,
originalWidth: item.line_rect.width,
originalHeight: item.line_rect.height,
src: itemUrl,
fileName: item.image.path.split('/').pop() || 'layer.png',
parentId: artboardId,
selectable: true,
hideMetadata: item.hideMetadata,
data: {
path: item.image.path,
text: item.line_text,
},
}
addElement(childImage)
}
setSelectedIds([addedArtboard.id])
// Support background task persistence
manualPersistence(taskId, store)
} catch (err) {
console.error('Failed to segment text layer:', err)
toast.error('修改失败')
} finally {
updateElementUIState(element.id, {
status: 'idle',
statusText: undefined,
})
}
},
[taskId, store, addElementToFlow],
)
return {
handleMatting,
handlePartialRedraw,
handleEditText,
handleConfirmEditText,
handleEditElements,
}
}