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 (
);
}
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 ? "图片" : "文件";
}