import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined"; import FileUploadOutlined from "@mui/icons-material/FileUploadOutlined"; import ImageOutlined from "@mui/icons-material/ImageOutlined"; import InsertDriveFileOutlined from "@mui/icons-material/InsertDriveFileOutlined"; import CircularProgress from "@mui/material/CircularProgress"; import Tooltip from "@mui/material/Tooltip"; import { useEffect, useId, useRef, useState } from "react"; import { uploadFile, uploadImage } from "@/shared/api/upload"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import styles from "./UploadField.module.css"; const imageAccept = "image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp,.svga,.pag"; const pagWasmURL = "/vendor/libpag.wasm"; let pagModulePromise; export function UploadField({ accept, disabled = false, kind = "image", label, onChange, value = "" }) { const inputId = useId(); const inputRef = useRef(null); const { showToast } = useToast(); const [uploading, setUploading] = useState(false); const [localPreview, setLocalPreview] = useState(null); const isImage = kind === "image"; const source = localPreview?.url || value; const assetKind = localPreview?.assetKind || getAssetKind(value, kind); const displayName = localPreview?.name || getDisplayValue(value); const inputAccept = accept || (isImage ? imageAccept : undefined); useEffect(() => { return () => { if (localPreview?.url) { URL.revokeObjectURL(localPreview.url); } }; }, [localPreview]); const openPicker = () => { if (!disabled && !uploading) { inputRef.current?.click(); } }; const handleFileChange = async (event) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) { return; } setLocalPreview((previous) => { if (previous?.url) { URL.revokeObjectURL(previous.url); } return { assetKind: getAssetKind(file.name, kind, file.type), name: file.name, url: URL.createObjectURL(file) }; }); setUploading(true); try { const result = isImage ? await uploadImage(file) : await uploadFile(file); onChange(result.url); setLocalPreview(null); showToast("上传成功", "success"); } catch (err) { setLocalPreview(null); showToast(err.message || "上传失败", "error"); } finally { setUploading(false); } }; const clearValue = () => { setLocalPreview(null); onChange(""); }; return (
{label} {assetKindLabel(assetKind, isImage)}
{displayName || "未上传"} {source ? ( ) : null}
); } function AssetPreview({ assetKind, isImage, src }) { if (!src) { return ( {isImage ? : } ); } if (assetKind === "svga") { return ; } if (assetKind === "pag") { return ; } if (isImage) { return ; } return ( ); } function RasterAssetPreview({ src }) { const [failed, setFailed] = useState(false); useEffect(() => { setFailed(false); }, [src]); if (failed) { return ( ); } return setFailed(true)} />; } function SVGAAssetPreview({ src }) { const containerRef = useRef(null); const [failed, setFailed] = useState(false); useEffect(() => { let cancelled = false; const container = containerRef.current; let player; setFailed(false); async function load() { try { const module = await import("svgaplayerweb"); const SVGA = module.default || module; if (cancelled || !container) { return; } container.innerHTML = ""; player = new SVGA.Player(container); player.loops = 0; player.clearsAfterStop = false; player.setContentMode("AspectFit"); const parser = new SVGA.Parser(container); parser.load( src, (videoItem) => { if (cancelled) { return; } player.setVideoItem(videoItem); player.startAnimation(); }, () => { if (!cancelled) { setFailed(true); } } ); } catch { if (!cancelled) { setFailed(true); } } } load(); return () => { cancelled = true; if (player) { try { player.stopAnimation(true); player.clear(); } catch { // Ignore player cleanup errors from partially loaded assets. } } if (container) { container.innerHTML = ""; } }; }, [src]); return ( {failed ? SVGA 预览失败 : null} ); } function PAGAssetPreview({ src }) { const canvasRef = useRef(null); const [failed, setFailed] = useState(false); useEffect(() => { let cancelled = false; let pagFile; let pagView; setFailed(false); async function load() { try { const PAG = await getPAGModule(); const buffer = await fetch(src).then((response) => response.arrayBuffer()); pagFile = await PAG.PAGFile.load(buffer); if (cancelled || !canvasRef.current) { return; } canvasRef.current.width = pagFile.width(); canvasRef.current.height = pagFile.height(); pagView = await PAG.PAGView.init(pagFile, canvasRef.current, { useScale: true }); if (!pagView) { throw new Error("PAGView init failed"); } pagView.setRepeatCount(0); await pagView.play(); } catch { if (!cancelled) { setFailed(true); } } } load(); return () => { cancelled = true; if (pagView) { pagView.destroy(); } if (pagFile?.destroy) { pagFile.destroy(); } }; }, [src]); return ( {failed ? PAG 预览失败 : null} ); } async function getPAGModule() { if (!pagModulePromise) { pagModulePromise = import("libpag").then(({ PAGInit }) => PAGInit({ locateFile: () => pagWasmURL })); } return pagModulePromise; } function getDisplayValue(value) { if (!value) { return ""; } const raw = String(value); try { const url = new URL(raw); const name = url.pathname.split("/").filter(Boolean).pop(); return name ? decodeURIComponent(name) : raw; } catch { return raw; } } function getAssetKind(value, kind, contentType = "") { const extension = getExtension(value); if (extension === "svga") { return "svga"; } if (extension === "pag") { return "pag"; } const normalizedType = String(contentType || "").toLowerCase(); if (normalizedType.includes("svga")) { return "svga"; } if (normalizedType.includes("pag")) { return "pag"; } return kind === "image" ? "image" : "file"; } function getExtension(value) { if (!value) { return ""; } const raw = String(value); const pathname = safePathname(raw); const name = pathname.split("/").filter(Boolean).pop() || raw; const cleanName = name.split("?")[0].split("#")[0]; const dotIndex = cleanName.lastIndexOf("."); return dotIndex >= 0 ? cleanName.slice(dotIndex + 1).toLowerCase() : ""; } function safePathname(value) { try { return new URL(value).pathname; } catch { return value; } } function assetKindLabel(assetKind, isImage) { if (assetKind === "svga") { return "SVGA"; } if (assetKind === "pag") { return "PAG"; } return isImage ? "图片" : "文件"; }