import { useRef, useState } from "react"; import { api } from "../api/client"; import type { CreativeAsset, AssetDef } from "../types"; import styles from "./AssetUploader.module.css"; const BASE = `${import.meta.env.BASE_URL}api/v1`.replace(/\/+$/, ""); // 可展示缩略图的图片后缀 const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]); function isImageFile(fileName: string): boolean { const ext = fileName.substring(fileName.lastIndexOf(".")).toLowerCase(); return IMAGE_EXTENSIONS.has(ext); } interface Props { creativeId: string; assets: CreativeAsset[]; assetDefs: { required: AssetDef[]; optional: AssetDef[] }; onUpdated: () => void; } type ImportMode = "file" | "url"; export default function AssetUploader({ creativeId, assets, assetDefs, onUpdated }: Props) { const [mode, setMode] = useState("file"); const [uploading, setUploading] = useState(false); const [singleUploading, setSingleUploading] = useState(null); const [url, setUrl] = useState(""); const [uploadResult, setUploadResult] = useState<{ files: Array<{ key: string; fileName: string; fileSize: number; valid: boolean }>; warnings: string[]; } | null>(null); const [error, setError] = useState(""); const fileInputRefs = useRef>({}); // === ZIP 上传 === async function handleFileUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setUploading(true); setError(""); setUploadResult(null); try { const res = await api.uploadAssets(creativeId, file); setUploadResult(res.data); onUpdated(); } catch (err: any) { setError(err.message); } finally { setUploading(false); e.target.value = ""; } } // === URL 导入 === async function handleUrlImport() { if (!url.trim()) return; setUploading(true); setError(""); setUploadResult(null); try { const res = await api.importFromUrl(creativeId, url.trim()); setUploadResult(res.data); onUpdated(); } catch (err: any) { setError(err.message); } finally { setUploading(false); } } // === 单个素材文件上传(点击替换) === function handleSingleUploadClick(key: string) { fileInputRefs.current[key]?.click(); } async function handleSingleFileChange(key: string, e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setSingleUploading(key); setError(""); try { await api.uploadAssetFile(creativeId, key, file); onUpdated(); } catch (err: any) { setError(err.message); } finally { setSingleUploading(null); e.target.value = ""; } } // === 清除 === async function handleClear() { if (!confirm("确认清除所有素材?")) return; try { await api.clearAssets(creativeId); setUploadResult(null); setUrl(""); onUpdated(); } catch (err: any) { setError(err.message); } } const allDefs = [...assetDefs.required, ...assetDefs.optional]; const hasAllRequired = assetDefs.required.every((def) => assets.some((a) => a.key === def.key) ); // 哪些 key 是可手动替换的(optional + 图片文件,排除 special) const EXCLUDE_MANUAL = new Set(["special"]); const replaceableDefs = assetDefs.optional.filter((d) => isImageFile(d.file) && !EXCLUDE_MANUAL.has(d.key)); const replaceableKeys = new Set(replaceableDefs.map((d) => d.key)); // 填色素材(ZIP 导入的)与可替换素材分开 const zipAssetDefs = allDefs.filter((d) => !replaceableKeys.has(d.key)); const brandAssetDefs = allDefs.filter((d) => replaceableKeys.has(d.key)); return (
{/* 模式切换 */}
{/* 文件上传 */} {mode === "file" && ( )} {/* URL 导入 */} {mode === "url" && (
setUrl(e.target.value)} disabled={uploading} />
)} {/* 上传结果 */} {uploadResult && uploadResult.warnings.length > 0 && (
{uploadResult.warnings.map((w, i) => (

⚠️ {w}

))}
)} {error &&

{error}

} {/* 文件列表 */}
素材文件 {assets.length > 0 && ( )}
{/* 填色素材 (ZIP 导入) */}
填色素材
{zipAssetDefs.map((def) => { const asset = assets.find((a) => a.key === def.key); const isRequired = assetDefs.required.some((r) => r.key === def.key); const showThumb = asset && isImageFile(def.file); return (
{showThumb ? ( {def.label} ) : ( {asset ? "✅" : isRequired ? "❌" : "⬜"} )} {def.file} {def.label} {!isRequired && !asset ? "(无)" : !isRequired ? "(选填)" : ""} {asset && ( {(asset.fileSize / 1024).toFixed(0)} KB )}
); })} {/* 品牌素材 (可手动替换) */} {brandAssetDefs.length > 0 && ( <>
品牌素材 · 点击可替换
{brandAssetDefs.map((def) => { const asset = assets.find((a) => a.key === def.key); const isUploading = singleUploading === def.key; return (
handleSingleUploadClick(def.key)} title="点击上传替换" > { fileInputRefs.current[def.key] = el; }} onChange={(e) => handleSingleFileChange(def.key, e)} className={styles.fileInput} /> {isUploading ? ( ) : ( {def.label} )} {def.file} {def.label} {def.dimensions} {asset && 🔄} {asset && ( {(asset.fileSize / 1024).toFixed(0)} KB )}
); })} )}
{!hasAllRequired && (

请上传包含所有必填文件的 zip 包

)} {!hasAllRequired && assets.length > 0 && (

请上传包含所有必填文件的 zip 包

)}
); }