Merge origin/main into main-merge-work
This commit is contained in:
@@ -752,6 +752,7 @@ const maxCloneProductImages = 20;
|
||||
const maxCloneReferenceImages = 20;
|
||||
const cloneVideoDurationMin = 5;
|
||||
const cloneVideoDurationMax = 45;
|
||||
const composerDurationOptions = [5, 10, 15];
|
||||
const cloneVideoQualityOptions: Array<{ key: CloneVideoQualityKey; label: string; desc: string }> = [
|
||||
{ key: "standard", label: "标准", desc: "快速出片" },
|
||||
{ key: "high", label: "高清", desc: "推荐" },
|
||||
@@ -875,6 +876,42 @@ const blobToDataUrl = (blob: Blob): Promise<string> =>
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
function createLocalImageItems(files: File[], limit: number, prefix: string): CloneImageItem[] {
|
||||
const selectedFiles = Array.from(files).slice(0, limit);
|
||||
const stamp = Date.now();
|
||||
return selectedFiles.map((file, index) => {
|
||||
const localPreviewUrl = URL.createObjectURL(file);
|
||||
const mimeType = normalizeEcommerceImageMime(file.type);
|
||||
return {
|
||||
id: `${prefix}-${stamp}-${index}`,
|
||||
src: localPreviewUrl,
|
||||
name: file.name,
|
||||
file,
|
||||
format: getImageFileFormat(file),
|
||||
mimeType,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadImageItem(item: CloneImageItem): Promise<{ src?: string; ossKey?: string; width?: number; height?: number }> {
|
||||
if (!item.file) return {};
|
||||
const mimeType = normalizeEcommerceImageMime(item.file.type);
|
||||
try {
|
||||
const uploadBlob = item.file.type === mimeType ? item.file : new Blob([item.file], { type: mimeType });
|
||||
const [uploaded, dimensions] = await Promise.all([
|
||||
aiGenerationClient.uploadAssetBinary(uploadBlob, {
|
||||
name: item.file.name,
|
||||
mimeType,
|
||||
scope: ecommerceOssScopes.productSource,
|
||||
}),
|
||||
readImageDimensions(item.src).catch(() => ({})),
|
||||
]);
|
||||
return { src: uploaded.url, ossKey: uploaded.ossKey, ...dimensions };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function createUploadedImageItems(files: File[], limit: number, prefix: string): Promise<CloneImageItem[]> {
|
||||
const selectedFiles = Array.from(files).slice(0, limit);
|
||||
const stamp = Date.now();
|
||||
@@ -1550,13 +1587,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const cloneRatioOptions = baseCloneRatioOptions;
|
||||
const composerRatioOptions = useMemo(
|
||||
() => [
|
||||
"1000×1000px 1:1",
|
||||
"800×1200px 2:3",
|
||||
"1200×800px 3:2",
|
||||
"1200×900px 4:3",
|
||||
"900×1200px 3:4",
|
||||
"1080×1920px 9:16",
|
||||
"1920×1080px 16:9",
|
||||
"1000×1000px\u00a0\u00a0\u00a01:1",
|
||||
"800×1200px\u00a0\u00a0\u00a02:3",
|
||||
"1200×800px\u00a0\u00a0\u00a03:2",
|
||||
"1200×900px\u00a0\u00a0\u00a04:3",
|
||||
"900×1200px\u00a0\u00a0\u00a03:4",
|
||||
"1080×1920px\u00a0\u00a0\u00a09:16",
|
||||
"1920×1080px\u00a0\u00a0\u00a016:9",
|
||||
],
|
||||
[],
|
||||
);
|
||||
@@ -1691,20 +1728,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
const addSetImages = async (files: File[]) => {
|
||||
const addSetImages = (files: File[]) => {
|
||||
if (setImages.length >= 3) return;
|
||||
const imageFiles = notifyRejectedImages(files);
|
||||
if (!imageFiles.length) return;
|
||||
try {
|
||||
const nextImages = await createUploadedImageItems(imageFiles, 3 - setImages.length, "set");
|
||||
setSetImages((current) => {
|
||||
if (current.length >= 3) return current;
|
||||
return nextImages.length ? [...current, ...nextImages].slice(0, 3) : current;
|
||||
});
|
||||
setProductSetStatus("ready");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "商品图上传失败");
|
||||
}
|
||||
const remainingSlots = 3 - setImages.length;
|
||||
if (remainingSlots <= 0) return;
|
||||
|
||||
const localItems = createLocalImageItems(imageFiles, remainingSlots, "set");
|
||||
setSetImages((current) => [...current, ...localItems].slice(0, 3));
|
||||
setProductSetStatus("ready");
|
||||
|
||||
Promise.all(localItems.map(async (item) => {
|
||||
const uploaded = await uploadImageItem(item);
|
||||
if (uploaded.src) URL.revokeObjectURL(item.src);
|
||||
return { id: item.id, uploaded };
|
||||
})).then((results) => {
|
||||
const updateMap = new Map(results.map((result) => [result.id, result.uploaded]));
|
||||
setSetImages((current) => current.map((item) => {
|
||||
const update = updateMap.get(item.id);
|
||||
return update ? { ...item, ...update } : item;
|
||||
}));
|
||||
}).catch(() => {
|
||||
toast.error("套图后台上传失败,请检查网络后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -2789,20 +2836,33 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
const addProductImages = async (files: File[]) => {
|
||||
const addProductImages = (files: File[]) => {
|
||||
const imageFiles = notifyRejectedImages(files);
|
||||
if (!imageFiles.length) return;
|
||||
try {
|
||||
const nextImages = await createUploadedImageItems(imageFiles, maxCloneProductImages - productImages.length, "product");
|
||||
setProductImages((current) => {
|
||||
if (current.length >= maxCloneProductImages) return current;
|
||||
return nextImages.length ? [...current, ...nextImages].slice(0, maxCloneProductImages) : current;
|
||||
});
|
||||
setStatus("ready");
|
||||
setResults([]);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "商品图上传失败");
|
||||
}
|
||||
const remainingSlots = maxCloneProductImages - productImages.length;
|
||||
if (remainingSlots <= 0) return;
|
||||
|
||||
const localItems = createLocalImageItems(imageFiles, remainingSlots, "product");
|
||||
setProductImages((current) => [...current, ...localItems].slice(0, maxCloneProductImages));
|
||||
setStatus("ready");
|
||||
setResults([]);
|
||||
|
||||
Promise.all(localItems.map(async (item) => {
|
||||
const uploaded = await uploadImageItem(item);
|
||||
if (uploaded.src) {
|
||||
URL.revokeObjectURL(item.src);
|
||||
}
|
||||
return { id: item.id, uploaded };
|
||||
})).then((results) => {
|
||||
const updateMap = new Map(results.map((result) => [result.id, result.uploaded]));
|
||||
setProductImages((current) => current.map((item) => {
|
||||
const update = updateMap.get(item.id);
|
||||
if (!update) return item;
|
||||
return { ...item, ...update };
|
||||
}));
|
||||
}).catch(() => {
|
||||
toast.error("商品图后台上传失败,请检查网络后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const handleProductUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -2856,22 +2916,29 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
const addCloneReferenceImages = async (files: File[]) => {
|
||||
const addCloneReferenceImages = (files: File[]) => {
|
||||
const imageFiles = notifyRejectedImages(files);
|
||||
if (!imageFiles.length) return;
|
||||
const remainingSlots = maxCloneReferenceImages - cloneReferenceImages.length;
|
||||
if (remainingSlots <= 0) return;
|
||||
try {
|
||||
const nextImages = await createUploadedImageItems(imageFiles, remainingSlots, "reference");
|
||||
if (!nextImages.length) return;
|
||||
setCloneReferenceImages((current) => {
|
||||
if (current.length >= maxCloneReferenceImages) return current;
|
||||
return nextImages.length ? [...current, ...nextImages].slice(0, maxCloneReferenceImages) : current;
|
||||
});
|
||||
hydrateCloneReferenceImageMeta(nextImages);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "参考图上传失败");
|
||||
}
|
||||
|
||||
const localItems = createLocalImageItems(imageFiles, remainingSlots, "reference");
|
||||
setCloneReferenceImages((current) => [...current, ...localItems].slice(0, maxCloneReferenceImages));
|
||||
hydrateCloneReferenceImageMeta(localItems);
|
||||
|
||||
Promise.all(localItems.map(async (item) => {
|
||||
const uploaded = await uploadImageItem(item);
|
||||
if (uploaded.src) URL.revokeObjectURL(item.src);
|
||||
return { id: item.id, uploaded };
|
||||
})).then((results) => {
|
||||
const updateMap = new Map(results.map((result) => [result.id, result.uploaded]));
|
||||
setCloneReferenceImages((current) => current.map((item) => {
|
||||
const update = updateMap.get(item.id);
|
||||
return update ? { ...item, ...update } : item;
|
||||
}));
|
||||
}).catch(() => {
|
||||
toast.error("参考图后台上传失败,请检查网络后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const removeCloneReferenceImage = (imageId: string) => {
|
||||
@@ -3327,23 +3394,38 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
};
|
||||
}, [openCloneModelSelect]);
|
||||
|
||||
const addGarmentImages = (files: File[]) => {
|
||||
const uploadedFiles = notifyRejectedImages(files);
|
||||
if (!uploadedFiles.length) return;
|
||||
const remainingSlots = 5 - garmentImages.length;
|
||||
if (remainingSlots <= 0) return;
|
||||
|
||||
const localItems = createLocalImageItems(uploadedFiles, remainingSlots, "garment");
|
||||
setGarmentImages((current) => [...current, ...localItems].slice(0, 5));
|
||||
setTryOnStatus("ready");
|
||||
|
||||
Promise.all(localItems.map(async (item) => {
|
||||
const uploaded = await uploadImageItem(item);
|
||||
if (uploaded.src) URL.revokeObjectURL(item.src);
|
||||
return { id: item.id, uploaded };
|
||||
})).then((results) => {
|
||||
const updateMap = new Map(results.map((result) => [result.id, result.uploaded]));
|
||||
setGarmentImages((current) => current.map((item) => {
|
||||
const update = updateMap.get(item.id);
|
||||
return update ? { ...item, ...update } : item;
|
||||
}));
|
||||
}).catch(() => {
|
||||
toast.error("服饰图后台上传失败,请检查网络后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const handleGarmentUpload = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files?.length) return;
|
||||
const uploadedFiles = notifyRejectedImages(Array.from(files));
|
||||
if (!uploadedFiles.length) {
|
||||
if (!files?.length) {
|
||||
event.target.value = "";
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const nextImages = await createUploadedImageItems(uploadedFiles, 5 - garmentImages.length, "garment");
|
||||
setGarmentImages((current) => [...current, ...nextImages].slice(0, 5));
|
||||
setTryOnStatus("ready");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "服饰图上传失败");
|
||||
}
|
||||
})();
|
||||
addGarmentImages(Array.from(files));
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
@@ -3379,20 +3461,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
const addDetailImages = async (files: File[]) => {
|
||||
const addDetailImages = (files: File[]) => {
|
||||
const uploadedFiles = notifyRejectedImages(files);
|
||||
if (!uploadedFiles.length) return;
|
||||
try {
|
||||
const nextImages = await createUploadedImageItems(uploadedFiles, 3 - detailProductImages.length, "detail");
|
||||
setDetailProductImages((current) => {
|
||||
if (current.length >= 3) return current;
|
||||
return nextImages.length ? [...current, ...nextImages].slice(0, 3) : current;
|
||||
});
|
||||
setDetailStatus("ready");
|
||||
setDetailResultUrl(null);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "详情图上传失败");
|
||||
}
|
||||
const remainingSlots = 3 - detailProductImages.length;
|
||||
if (remainingSlots <= 0) return;
|
||||
|
||||
const localItems = createLocalImageItems(uploadedFiles, remainingSlots, "detail");
|
||||
setDetailProductImages((current) => [...current, ...localItems].slice(0, 3));
|
||||
setDetailStatus("ready");
|
||||
setDetailResultUrl(null);
|
||||
|
||||
Promise.all(localItems.map(async (item) => {
|
||||
const uploaded = await uploadImageItem(item);
|
||||
if (uploaded.src) URL.revokeObjectURL(item.src);
|
||||
return { id: item.id, uploaded };
|
||||
})).then((results) => {
|
||||
const updateMap = new Map(results.map((result) => [result.id, result.uploaded]));
|
||||
setDetailProductImages((current) => current.map((item) => {
|
||||
const update = updateMap.get(item.id);
|
||||
return update ? { ...item, ...update } : item;
|
||||
}));
|
||||
}).catch(() => {
|
||||
toast.error("详情图后台上传失败,请检查网络后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const uploadCloneImages = async (images: CloneImageItem[]): Promise<string[]> => {
|
||||
@@ -5114,11 +5206,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
}
|
||||
if (menuToRender === "platform") {
|
||||
return (
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--list ecom-command-popover--ratio ecom-command-popover--platform${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--list ecom-command-popover--platforms${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{platformOptions.map((option) => (
|
||||
<button key={option} type="button" className={platform === option ? "is-active" : ""} onClick={() => { handleClonePlatformChange(option); setComposerMenu(null); }}>
|
||||
{renderPlatformLogo(option)}
|
||||
<span className="ecom-platform-name">{option}</span>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -5149,6 +5240,17 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (menuToRender === "settings" && activeCommerceScenario === "salesVideo") {
|
||||
return (
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--list ecom-command-popover--duration${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{composerDurationOptions.map((option) => (
|
||||
<button key={option} type="button" className={cloneVideoDuration === option ? "is-active" : ""} onClick={() => { setCloneVideoDuration(option); setComposerMenu(null); }}>
|
||||
{option}秒
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={composerPopoverKey} className={`ecom-command-popover ecom-command-popover--settings ecom-command-popover--settings-${cloneOutput}${popoverClosingClass}`} style={composerPopoverStyle}>
|
||||
{cloneOutput === "set" ? (
|
||||
@@ -5738,14 +5840,18 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
) : null}
|
||||
{shouldShowScenarioSettings ? (
|
||||
<div className="ecom-command-option-row ecom-command-option-row--settings">
|
||||
<button type="button" className={composerMenu === "platform" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("platform", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><GlobalOutlined /></span>
|
||||
<span>平台</span>{platform}
|
||||
</button>
|
||||
<button type="button" className={composerMenu === "language" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("language", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><FileImageOutlined /></span>
|
||||
<span>语种</span>{language}
|
||||
</button>
|
||||
{activeCommerceScenario !== "salesVideo" && activeCommerceScenario !== "poster" ? (
|
||||
<button type="button" className={composerMenu === "platform" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("platform", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><GlobalOutlined /></span>
|
||||
<span>平台</span>{platform}
|
||||
</button>
|
||||
) : null}
|
||||
{activeCommerceScenario !== "salesVideo" ? (
|
||||
<button type="button" className={composerMenu === "language" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("language", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><FileImageOutlined /></span>
|
||||
<span>语种</span>{language}
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" className={composerMenu === "ratio" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("ratio", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><TableOutlined /></span>
|
||||
<span>比例</span>{getRatioDisplayParts(ratio).aspect}
|
||||
@@ -5753,7 +5859,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{scenarioAdvancedSettingsKeys.includes(activeCommerceScenario) ? (
|
||||
<button type="button" className={composerMenu === "settings" ? "is-active" : ""} onClick={(event) => toggleComposerMenu("settings", event)}>
|
||||
<span className="ecom-command-option-icon" aria-hidden="true"><SettingOutlined /></span>
|
||||
<span>设置</span>{composerSettingLabel}
|
||||
<span>{activeCommerceScenario === "salesVideo" ? "时长" : "设置"}</span>
|
||||
{activeCommerceScenario === "salesVideo" ? `${cloneVideoDuration}秒` : composerSettingLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user