diff --git a/src/pages/CharactersCreate/index.tsx b/src/pages/CharactersCreate/index.tsx index 6955c43..3e75826 100644 --- a/src/pages/CharactersCreate/index.tsx +++ b/src/pages/CharactersCreate/index.tsx @@ -162,7 +162,6 @@ export default function CharactersCreate() { // 左侧角色风格库数据与选中项。 const [characterList, setCharacterList] = useState([]); const [diyCategoryList, setDiyCategoryList] = useState[]>([]); - const [selectedCharacterList, setSelectedCharacterList] = useState([]); // 生成能力相关:分类 -> 模型 -> 参数。 const [aiCategories, setAiCategories] = useState([]); @@ -211,11 +210,21 @@ export default function CharactersCreate() { const taskPageRef = useRef(1); const loadMoreRequestIdRef = useRef(0); + const selectedCharacterIdSet = useMemo(() => { + const idSet = new Set(); + images.forEach((imageItem) => { + if (typeof imageItem === "object" && imageItem.type === "character" && imageItem.id != null) { + idSet.add(String(imageItem.id)); + } + }); + return idSet; + }, [images]); + const handleSelectCharacter = useCallback( (item: any) => { const id = item?.id; if (id === undefined || id === null) return; - const inList = selectedCharacterList.some((c) => String(c?.id) === String(id)); + const normalizedId = String(id); const nextCharacterItem: CharacterImageItem = { type: "character", id: item?.id, @@ -223,36 +232,23 @@ export default function CharactersCreate() { avatar: item?.avatar, }; setImages((prev) => { - const inImages = prev.some( + const existingIndex = prev.findIndex( (imageItem) => typeof imageItem === "object" && imageItem.type === "character" && - String(imageItem.id) === String(id), + String(imageItem.id) === normalizedId, ); - if (inImages) { - setSelectedCharacterList((p) => p.filter((c) => String(c?.id) !== String(id))); - return prev.filter( - (imageItem) => - !( - typeof imageItem === "object" && - imageItem.type === "character" && - String(imageItem.id) === String(id) - ), - ); - } - if (!inImages && inList) { - setSelectedCharacterList((p) => p.filter((c) => String(c?.id) !== String(id))); - return prev; + if (existingIndex >= 0) { + return prev.filter((_, idx) => idx !== existingIndex); } if (prev.length >= MAX_UPLOAD_IMAGES) { Message.warning(t("composer.maxUploadReached", { max: MAX_UPLOAD_IMAGES })); return prev; } - setSelectedCharacterList((p) => (p.some((c) => String(c?.id) === String(id)) ? p : [...p, item])); return [...prev, nextCharacterItem]; }); }, - [selectedCharacterList, t], + [t], ); const handleSelectMaterial = useCallback((item: any) => { @@ -626,14 +622,8 @@ export default function CharactersCreate() { // 删除某一张已上传图片。 const handleRemoveImage = useCallback((targetIdx: number) => { - setImages((prev) => { - const removedItem = prev[targetIdx]; - if (typeof removedItem === "object" && removedItem.type === "character") { - setSelectedCharacterList((list) => list.filter((c) => String(c?.id) !== String(removedItem.id))); - } - return prev.filter((_, idx) => idx !== targetIdx); - }); - }, [t]); + setImages((prev) => prev.filter((_, idx) => idx !== targetIdx)); + }, []); const handleDownloadAsset = useCallback((asset: any, task: any, assetIdx: number) => { const imageUrl = asset?.images?.[0]; @@ -888,7 +878,7 @@ export default function CharactersCreate() { type="button" className={classNames( "charactersCreate__styleCell", - selectedCharacterList.some((c) => String(c?.id) === String(item.id)) && "charactersCreate__styleCell_selected", + selectedCharacterIdSet.has(String(item.id)) && "charactersCreate__styleCell_selected", )} draggable onMouseEnter={() => { diff --git a/src/pages/Explore/index.tsx b/src/pages/Explore/index.tsx index 51e8ac9..5ce8162 100644 --- a/src/pages/Explore/index.tsx +++ b/src/pages/Explore/index.tsx @@ -59,7 +59,7 @@ export default function Explore() { useEffect(() => { dragTransferManager.warmupPreviewImages( - officialRoles.map((item) => item?.threeViewImage ?? item?.threeView), + officialRoles.map((item) => item?.threeViewImage), ); }, [dragTransferManager, officialRoles]); diff --git a/src/utils/imageDragTransfer.ts b/src/utils/imageDragTransfer.ts index 80d59b0..5b2ec17 100644 --- a/src/utils/imageDragTransfer.ts +++ b/src/utils/imageDragTransfer.ts @@ -58,6 +58,15 @@ function normalizeUrl(rawUrl?: unknown): string { */ export function createImageDragTransferManager(): ImageDragTransferManager { const cache = new Map(); + const preloadTasks = new Map>(); + const fallbackDragImage = document.createElement("div"); + fallbackDragImage.style.width = "1px"; + fallbackDragImage.style.height = "1px"; + fallbackDragImage.style.position = "fixed"; + fallbackDragImage.style.top = "-9999px"; + fallbackDragImage.style.left = "-9999px"; + fallbackDragImage.style.opacity = "0"; + document.body.appendChild(fallbackDragImage); const ensurePreviewImage = (rawUrl?: unknown): HTMLImageElement | null => { const url = normalizeUrl(rawUrl); @@ -79,9 +88,46 @@ export function createImageDragTransferManager(): ImageDragTransferManager { return img; }; + const ensurePreviewReady = (rawUrl?: unknown): Promise => { + const url = normalizeUrl(rawUrl); + if (!url) return Promise.resolve(); + + const existedTask = preloadTasks.get(url); + if (existedTask) return existedTask; + + const img = ensurePreviewImage(url); + if (!img) return Promise.resolve(); + + const task = new Promise((resolve) => { + if (img.complete && img.naturalWidth > 0) { + resolve(); + return; + } + + const done = () => { + img.removeEventListener("load", done); + img.removeEventListener("error", done); + resolve(); + }; + img.addEventListener("load", done, { once: true }); + img.addEventListener("error", done, { once: true }); + }).then(async () => { + try { + if (typeof img.decode === "function") { + await img.decode(); + } + } catch { + // decode 失败不影响拖拽流程,继续使用已有资源状态 + } + }); + + preloadTasks.set(url, task); + return task; + }; + const warmupPreviewImages = (urls: unknown[]) => { urls.forEach((url) => { - ensurePreviewImage(url); + void ensurePreviewReady(url); }); }; @@ -97,6 +143,9 @@ export function createImageDragTransferManager(): ImageDragTransferManager { const preview = ensurePreviewImage(previewUrl ?? dataUrl); if (preview && preview.complete && preview.naturalWidth > 0) { evt.dataTransfer.setDragImage(preview, 60, 60); + } else { + // 图片未就绪时使用极小占位图,避免浏览器回退截图拖拽源导致卡顿。 + evt.dataTransfer.setDragImage(fallbackDragImage, 0, 0); } return true; }; @@ -108,6 +157,10 @@ export function createImageDragTransferManager(): ImageDragTransferManager { } }); cache.clear(); + preloadTasks.clear(); + if (fallbackDragImage.parentNode) { + fallbackDragImage.parentNode.removeChild(fallbackDragImage); + } }; return {