|
|
@@ -116,9 +116,11 @@ export default function AssetUploader({ creativeId, assets, assetDefs, onUpdated
|
|
|
|
|
|
// 哪些 key 是可手动替换的(optional + 图片文件,排除 special)
|
|
|
const EXCLUDE_MANUAL = new Set(["special"]);
|
|
|
- const replaceableKeys = new Set(
|
|
|
- assetDefs.optional.filter((d) => isImageFile(d.file) && !EXCLUDE_MANUAL.has(d.key)).map((d) => d.key)
|
|
|
- );
|
|
|
+ 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 (
|
|
|
<div className={styles.wrapper}>
|
|
|
@@ -191,32 +193,65 @@ export default function AssetUploader({ creativeId, assets, assetDefs, onUpdated
|
|
|
{error && <p className={styles.error}>{error}</p>}
|
|
|
|
|
|
{/* 文件列表 */}
|
|
|
- {assets.length > 0 && (
|
|
|
- <div className={styles.fileList}>
|
|
|
- <div className={styles.fileListHeader}>
|
|
|
- <span>
|
|
|
- 已上传文件 ({assets.filter((a) => a.isRequired).length}/{assetDefs.required.length} 必填)
|
|
|
- </span>
|
|
|
+ <div className={styles.fileList}>
|
|
|
+ <div className={styles.fileListHeader}>
|
|
|
+ <span>素材文件</span>
|
|
|
+ {assets.length > 0 && (
|
|
|
<button onClick={handleClear} className={styles.clearBtn}>
|
|
|
清除素材
|
|
|
</button>
|
|
|
- </div>
|
|
|
- {allDefs.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);
|
|
|
- const canReplace = replaceableKeys.has(def.key);
|
|
|
- const isUploading = singleUploading === def.key;
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 填色素材 (ZIP 导入) */}
|
|
|
+ <div className={styles.sectionLabel}>填色素材</div>
|
|
|
+ {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 (
|
|
|
+ <div key={def.key} className={styles.fileItem}>
|
|
|
+ {showThumb ? (
|
|
|
+ <img
|
|
|
+ className={styles.thumb}
|
|
|
+ src={`${BASE}/creatives/${creativeId}/assets/${def.key}`}
|
|
|
+ alt={def.label}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <span className={asset ? styles.fileOk : styles.fileMissing}>
|
|
|
+ {asset ? "✅" : isRequired ? "❌" : "⬜"}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ <span className={styles.fileName}>{def.file}</span>
|
|
|
+ <span className={styles.fileLabel}>
|
|
|
+ {def.label} {!isRequired && "(选填)"}
|
|
|
+ </span>
|
|
|
+ {asset && (
|
|
|
+ <span className={styles.fileSize}>
|
|
|
+ {(asset.fileSize / 1024).toFixed(0)} KB
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+
|
|
|
+ {/* 品牌素材 (可手动替换) */}
|
|
|
+ {brandAssetDefs.length > 0 && (
|
|
|
+ <>
|
|
|
+ <div className={styles.divider} />
|
|
|
+ <div className={styles.sectionLabel}>品牌素材 · 点击可替换</div>
|
|
|
+ {brandAssetDefs.map((def) => {
|
|
|
+ const asset = assets.find((a) => a.key === def.key);
|
|
|
+ const isUploading = singleUploading === def.key;
|
|
|
|
|
|
- return (
|
|
|
- <div
|
|
|
- key={def.key}
|
|
|
- className={`${styles.fileItem} ${canReplace ? styles.fileItemClickable : ""}`}
|
|
|
- onClick={canReplace ? () => handleSingleUploadClick(def.key) : undefined}
|
|
|
- title={canReplace ? "点击上传替换" : undefined}
|
|
|
- >
|
|
|
- {/* 隐藏的文件选择器 */}
|
|
|
- {canReplace && (
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ key={def.key}
|
|
|
+ className={`${styles.fileItem} ${styles.fileItemClickable}`}
|
|
|
+ onClick={() => handleSingleUploadClick(def.key)}
|
|
|
+ title="点击上传替换"
|
|
|
+ >
|
|
|
<input
|
|
|
type="file"
|
|
|
accept={def.accept || ".png,.jpg,.jpeg"}
|
|
|
@@ -224,35 +259,35 @@ export default function AssetUploader({ creativeId, assets, assetDefs, onUpdated
|
|
|
onChange={(e) => handleSingleFileChange(def.key, e)}
|
|
|
className={styles.fileInput}
|
|
|
/>
|
|
|
- )}
|
|
|
|
|
|
- {isUploading ? (
|
|
|
- <span className={styles.thumbLoading}>⏳</span>
|
|
|
- ) : showThumb ? (
|
|
|
- <img
|
|
|
- className={styles.thumb}
|
|
|
- src={`${BASE}/creatives/${creativeId}/assets/${def.key}`}
|
|
|
- alt={def.label}
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <span className={asset ? styles.fileOk : styles.fileMissing}>
|
|
|
- {asset ? "✅" : isRequired ? "❌" : "⬜"}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- <span className={styles.fileName}>{def.file}</span>
|
|
|
- <span className={styles.fileLabel}>
|
|
|
- {def.label} {!isRequired && "(选填)"}
|
|
|
- {canReplace && asset && <span className={styles.replaceHint}> 🔄</span>}
|
|
|
- </span>
|
|
|
- {asset && (
|
|
|
- <span className={styles.fileSize}>
|
|
|
- {(asset.fileSize / 1024).toFixed(0)} KB
|
|
|
+ {isUploading ? (
|
|
|
+ <span className={styles.thumbLoading}>⏳</span>
|
|
|
+ ) : (
|
|
|
+ <img
|
|
|
+ className={styles.thumb}
|
|
|
+ src={`${BASE}/creatives/${creativeId}/assets/${def.key}`}
|
|
|
+ alt={def.label}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <span className={styles.fileName}>{def.file}</span>
|
|
|
+ <span className={styles.fileLabel}>
|
|
|
+ {def.label}
|
|
|
+ {asset && <span className={styles.replaceHint}> 🔄</span>}
|
|
|
</span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- );
|
|
|
- })}
|
|
|
- </div>
|
|
|
+ {asset && (
|
|
|
+ <span className={styles.fileSize}>
|
|
|
+ {(asset.fileSize / 1024).toFixed(0)} KB
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {!hasAllRequired && (
|
|
|
+ <p className={styles.hint}>请上传包含所有必填文件的 zip 包</p>
|
|
|
)}
|
|
|
|
|
|
{!hasAllRequired && assets.length > 0 && (
|