Merge f30e585: extract platform rules and prompt builders

This commit is contained in:
2026-06-16 13:55:02 +08:00
7 changed files with 848 additions and 547 deletions
+35 -535
View File
@@ -42,6 +42,32 @@ import { ecommerceOssScopes, saveUnifiedEcommerceGenerationRecord, deleteEcommer
import { downloadResultAsset } from "../workbench/workbenchDownload";
import { clampNumber, normalizeHexColor, hexToRgb, rgbToHex, parseSmartCutoutAspect, parseSmartCutoutPercent, hsvToRgb, hexToHsv } from "./utils/colorUtils";
import { normalizeRatioToken, quickSetRatioOptions, getQuickSetRatioValue, formatRatioDisplayValue, getRatioDisplayParts, parseRatioToAspectCss, supportedImageApiRatios, toSupportedImageApiRatio, normalizeRatioForApi, greatestCommonDivisor, formatAspectRatio } from "./utils/ratioUtils";
import {
defaultCloneOutput,
defaultEcommercePlatform,
defaultProductSetOutput,
formatUploadedImageRatio,
getPlatformDefaultLanguage,
getPlatformDefaultRatio,
getPlatformLanguageOptions,
getPlatformRatioOptions,
getUniqueRatioOptions,
marketLanguageOptions,
marketOptions,
normalizeLanguageForPlatform,
normalizeMarket,
normalizePlatform,
normalizeRatioForPlatform,
platformOptions,
type CloneOutputKey,
type ProductSetOutputKey,
} from "./utils/platformRules";
import {
buildEcommerceImagePrompt,
buildSetSubPrompt,
setCountLabels,
type EcommerceImagePromptOptions,
} from "./utils/promptBuilder";
import { aiGenerationClient } from "../../api/aiGenerationClient";
const smartCutoutColorPresets = [
@@ -186,8 +212,6 @@ interface ProductClonePageProps {
}
type ProductCloneStatus = "idle" | "ready" | "generating" | "done" | "failed";
type ProductSetOutputKey = "set" | "detail" | "model" | "video";
type CloneOutputKey = ProductSetOutputKey | "hot";
type CloneSetCountKey = "selling" | "white" | "scene";
type CloneModelPanelTab = "scene" | "model";
type CloneVideoQualityKey = "standard" | "high" | "ultra";
@@ -286,25 +310,6 @@ interface ProductSetPreviewSelection {
removable?: boolean;
}
interface EcommerceImagePromptOptions {
gender?: string;
age?: string;
ethnicity?: string;
body?: string;
appearance?: string;
scenes?: string[];
customScene?: string;
smartScene?: boolean;
detailModules?: string[];
}
type PlatformRatioModeKey = ProductSetOutputKey | "hot";
interface PlatformRatioGroup {
ratios: string[];
defaultRatio: string;
}
const sideTools: Array<{ key: ProductKitToolKey; label: string; icon: ReactNode }> = [
{ key: "set", label: "商品套图", icon: <AppstoreOutlined /> },
{ key: "detail", label: "A+详情", icon: <FileImageOutlined /> },
@@ -312,358 +317,43 @@ const sideTools: Array<{ key: ProductKitToolKey; label: string; icon: ReactNode
{ key: "clone", label: "电商AI作图", icon: <AppstoreOutlined /> },
];
const platformSpecOptions: Array<{
label: string;
ratios: string[];
defaultRatio: string;
ratioGroups?: Partial<Record<PlatformRatioModeKey, PlatformRatioGroup>>;
specs: string[];
tip?: string;
aliases?: string[];
}> = [
{
label: "淘宝/天猫",
ratios: ["淘宝主图 / SKU 图 800×800px", "详情页宽 750px", "详情页宽 790px"],
defaultRatio: "淘宝主图 / SKU 图 800×800px",
ratioGroups: {
set: {
ratios: ["1000×1000px\u00a0\u00a0\u00a01:1", "800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: [
"750×1000px\u00a0\u00a0\u00a03:4",
"790×1053px\u00a0\u00a0\u00a03:4",
"750×1125px\u00a0\u00a0\u00a02:3",
"790×1185px\u00a0\u00a0\u00a02:3",
],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1080×1440px\u00a0\u00a0\u00a03:4", "1080×1080px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 / SKU 图 800×800px,≤3MB", "详情页宽 750px 或 790px,单张高度≤1546px"],
tip: "建议主图 200-400KB JPG,超过 500KB 会影响加载速度。",
},
{
label: "京东",
ratios: ["京东主图 / SKU 图 800×800px", "详情页宽 750px", "首图主体占比 ≥80%"],
defaultRatio: "京东主图 / SKU 图 800×800px",
ratioGroups: {
set: {
ratios: ["1000×1000px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: [
"750×1000px\u00a0\u00a0\u00a03:4",
"990×1320px\u00a0\u00a0\u00a03:4",
"750×1125px\u00a0\u00a0\u00a02:3",
"990×1485px\u00a0\u00a0\u00a02:3",
],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1125px\u00a0\u00a0\u00a02:3", "990×1485px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1920×1080px\u00a0\u00a0\u00a016:9"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 / SKU 图 800×800px,白底,≤3MB", "详情页宽 750px,首图主体占比 ≥80%"],
},
{
label: "拼多多",
ratios: ["主图 750×352px", "主图 800×800px", "详情页宽 750px"],
defaultRatio: "主图 750×352px",
ratioGroups: {
set: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1", "750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 750×352px 或 800×800px,≤1MB", "详情页宽 750px,要求纯白底、无水印、无拼接"],
},
{
label: "抖音电商",
ratios: ["短视频1080×1920px"],
defaultRatio: "短视频1080×1920px",
ratioGroups: {
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["短视频 1080×1920px9:16", "30s 内最佳"],
},
{
label: "亚马逊 Amazon",
ratios: ["主图 ≥1600×1600px", "建议 2000×2000px+", "最小 500×500px"],
defaultRatio: "主图 ≥1600×1600px",
ratioGroups: {
set: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1", "1200×1800px\u00a0\u00a0\u00a02:3", "1200×1600px\u00a0\u00a0\u00a03:4"],
defaultRatio: "1200×1800px\u00a0\u00a0\u00a02:3",
},
model: {
ratios: ["1200×1800px\u00a0\u00a0\u00a02:3"],
defaultRatio: "1200×1800px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1920×1080px\u00a0\u00a0\u00a016:9"],
defaultRatio: "1920×1080px\u00a0\u00a0\u00a016:9",
},
hot: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 1600×1600px+,纯白底,≤10MB", "最小 500×500px,建议 2000px+ 以支持缩放"],
aliases: ["亚马逊"],
},
{
label: "Shopee",
ratios: ["商品主图 1024×1024px", "基础主图 800×800px"],
defaultRatio: "商品主图 1024×1024px",
ratioGroups: {
set: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品主图推荐 1024×1024px,基础 800×800px", "≤2MB,白底或浅色底"],
aliases: ["虾皮 Shopee/Lazada", "虾皮"],
},
{
label: "Lazada",
ratios: ["商品主图 800×800px"],
defaultRatio: "商品主图 800×800px",
ratioGroups: {
set: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品主图 800×800px1:1"],
},
{
label: "Instagram",
ratios: ["帖子 1080×1350px", "帖子 1080×1080px", "Stories / Reels 1080×1920px", "头像 320×320px"],
defaultRatio: "帖子 1080×1350px",
ratioGroups: {
set: {
ratios: ["1080×1080px\u00a0\u00a0\u00a01:1", "1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1080px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
model: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
},
specs: ["帖子 1080×1350px 或 1080×1080px", "Stories / Reels 封面 1080×1920px,头像 320×320px"],
tip: "建议 ≤8MB JPG。",
aliases: ["Instagram Reels"],
},
{
label: "速卖通",
ratios: ["主图 800×800px", "主图 1000×1000px+"],
defaultRatio: "主图 800×800px",
ratioGroups: {
set: {
ratios: ["1000×1000px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1125px\u00a0\u00a0\u00a02:3", "750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3",
},
model: {
ratios: ["750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1920×1080px\u00a0\u00a0\u00a016:9"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图建议 800×800px 或更高,1:1", "适合跨境电商主图、SKU 图和场景图"],
},
{
label: "eBay",
ratios: ["商品图1:1", "白底多角度展示图 1:1"],
defaultRatio: "商品图1:1",
ratioGroups: {
set: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1000×1500px\u00a0\u00a0\u00a02:3", "1200×1600px\u00a0\u00a0\u00a03:4"],
defaultRatio: "1000×1500px\u00a0\u00a0\u00a02:3",
},
model: {
ratios: ["1000×1500px\u00a0\u00a0\u00a02:3"],
defaultRatio: "1000×1500px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1920×1080px\u00a0\u00a0\u00a016:9", "1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1920×1080px\u00a0\u00a0\u00a016:9",
},
hot: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品图建议 1:1,主体清晰居中", "适合白底主图和多角度展示图"],
},
{
label: "TikTok Shop",
ratios: ["商品主图 1:1", "短视频/ 竖版封面 9:16"],
defaultRatio: "商品主图 1:1",
ratioGroups: {
set: {
ratios: ["1280×1280px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1280×1280px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
model: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品主图建议 1:1", "短视频竖版封面建议 9:16"],
},
];
const platformOptions = platformSpecOptions.map((option) => option.label);
const getPlatformLogoText = (value: string) => {
const normalized = value.toLowerCase();
if (value.includes("淘宝") || value.includes("天猫")) return "淘";
if (value.includes("京东")) return "京";
if (value.includes("拼多多") || value.includes("鎷煎澶")) return "拼";
if (value.includes("拼多多")) return "拼";
if (value.includes("抖音")) return "抖";
if (normalized.includes("amazon")) return "a";
if (normalized.includes("shopee")) return "S";
if (normalized.includes("lazada")) return "L";
if (normalized.includes("instagram")) return "IG";
if (value.includes("速卖通") || value.includes("閫熷崠閫")) return "AE";
if (value.includes("速卖通")) return "AE";
if (normalized.includes("ebay")) return "eB";
if (normalized.includes("tiktok")) return "♪";
return value.trim().slice(0, 1).toUpperCase() || "商";
};
const getPlatformLogoVariant = (value: string) => {
const normalized = value.toLowerCase();
if (value.includes("淘宝") || value.includes("天猫")) return "taobao";
if (value.includes("京东")) return "jd";
if (value.includes("拼多多") || value.includes("鎷煎澶")) return "pdd";
if (value.includes("拼多多")) return "pdd";
if (value.includes("抖音")) return "douyin";
if (normalized.includes("amazon")) return "amazon";
if (normalized.includes("shopee")) return "shopee";
if (normalized.includes("lazada")) return "lazada";
if (normalized.includes("instagram")) return "instagram";
if (value.includes("速卖通") || value.includes("閫熷崠閫")) return "aliexpress";
if (value.includes("速卖通")) return "aliexpress";
if (normalized.includes("ebay")) return "ebay";
if (normalized.includes("tiktok")) return "tiktok";
return "default";
};
const getPlatformLogoMarks = (value: string) => {
if (value.includes("淘宝") || value.includes("天猫")) return ["淘", "猫"];
return [getPlatformLogoText(value)];
};
const renderPlatformLogo = (value: string) => {
const marks = getPlatformLogoMarks(value);
const variant = getPlatformLogoVariant(value);
@@ -680,115 +370,7 @@ const renderPlatformLogo = (value: string) => {
</span>
);
};
const marketLanguageOptions: Array<{ country: string; languages: string[] }> = [
{ country: "中国", languages: ["中文"] },
{ country: "美国", languages: ["英文"] },
{ country: "加拿大", languages: ["英文", "法文"] },
{ country: "英国", languages: ["英文"] },
{ country: "德国", languages: ["德文"] },
{ country: "法国", languages: ["法文"] },
{ country: "意大利", languages: ["意大利语"] },
{ country: "西班牙", languages: ["西班牙语"] },
{ country: "日本", languages: ["日文"] },
{ country: "韩国", languages: ["韩文"] },
{ country: "澳大利亚", languages: ["英文"] },
{ country: "新加坡", languages: ["英文", "中文"] },
{ country: "马来西亚", languages: ["马来语", "英文", "中文"] },
{ country: "印尼", languages: ["印度尼西亚语", "英文"] },
{ country: "越南", languages: ["越南语", "英文"] },
{ country: "泰国", languages: ["泰语", "英文"] },
{ country: "菲律宾", languages: ["菲律宾语(他加禄语)", "英文"] },
{ country: "巴西", languages: ["葡萄牙语"] },
{ country: "墨西哥", languages: ["西班牙语"] },
{ country: "智利", languages: ["西班牙语"] },
{ country: "哥伦比亚", languages: ["西班牙语"] },
{ country: "阿联酋", languages: ["阿拉伯语", "英文"] },
{ country: "沙特阿拉伯", languages: ["阿拉伯语", "英文"] },
{ country: "俄罗斯", languages: ["俄语"] },
{ country: "波兰", languages: ["波兰语"] },
];
const marketOptions = marketLanguageOptions.map((option) => option.country);
const languageOptions = Array.from(new Set(marketLanguageOptions.flatMap((option) => option.languages)));
const languageAliases: Record<string, string> = {
"英文": "英文",
"中文": "中文",
"英语": "英文",
"日语": "日文",
"日文": "日文",
"德语": "德文",
"德文": "德文",
"法语": "法文",
"法文": "法文",
"韩语": "韩文",
"韩文": "韩文",
"西文": "西班牙语",
"西班牙语": "西班牙语",
"葡文": "葡萄牙语",
"葡萄牙语": "葡萄牙语",
"印尼语": "印度尼西亚语",
"印度尼西亚语": "印度尼西亚语",
"菲律宾语": "菲律宾语(他加禄语)",
"菲律宾语(他加禄语)": "菲律宾语(他加禄语)",
};
const defaultPlatformSpec = platformSpecOptions[0]!;
const getPlatformSpec = (value: string) =>
platformSpecOptions.find((option) => option.label === value || option.aliases?.includes(value)) ?? defaultPlatformSpec;
const legacyPlatformAliases: Record<string, string> = {
"淘宝/天猫": "淘宝/天猫",
"京东": "京东",
"拼多多": "拼多多",
"抖音电商": "抖音电商",
"亚马逊Amazon": "亚马逊 Amazon",
"速卖通": "速卖通",
};
const normalizePlatform = (value: string) => getPlatformSpec(legacyPlatformAliases[value] ?? value).label;
const domesticPlatformLabels = new Set(["淘宝/天猫", "京东", "拼多多", "抖音电商"]);
const domesticPlatformLanguages = ["中文"];
const isDomesticPlatform = (platformValue: string) => domesticPlatformLabels.has(normalizePlatform(platformValue));
const getPlatformRatioGroup = (value: string, mode?: PlatformRatioModeKey): PlatformRatioGroup => {
const platformSpec = getPlatformSpec(value);
return (mode ? platformSpec.ratioGroups?.[mode] : null) ?? {
ratios: platformSpec.ratios,
defaultRatio: platformSpec.defaultRatio,
};
};
const getPlatformRatioOptions = (value: string, mode?: PlatformRatioModeKey) => getPlatformRatioGroup(value, mode).ratios;
const getPlatformDefaultRatio = (value: string, mode?: PlatformRatioModeKey) => getPlatformRatioGroup(value, mode).defaultRatio;
const getUniqueRatioOptions = (ratios: string[]) => Array.from(new Set(ratios));
const normalizeRatioForPlatform = (platformValue: string, ratioValue: string, mode?: PlatformRatioModeKey) => {
const platformRatios = getPlatformRatioOptions(platformValue, mode);
if (platformRatios.includes(ratioValue)) return ratioValue;
const normalizedRatio = normalizeRatioToken(ratioValue);
const matchedRatio = platformRatios.find((option) => normalizeRatioToken(option).includes(normalizedRatio));
return matchedRatio ?? getPlatformDefaultRatio(platformValue, mode);
};
const formatUploadedImageRatio = (image?: CloneImageItem) => {
if (!image) return null;
const format = image.format ? `\u00a0\u00a0\u00a0${image.format}` : "";
if (!image.width || !image.height) return `上传图片\u00a0\u00a0\u00a0原图比例${format}`;
return `上传图片 ${image.width}×${image.height}px\u00a0\u00a0\u00a0${formatAspectRatio(image.width, image.height)}${format}`;
};
const defaultMarketLanguageOption = marketLanguageOptions[0]!;
const normalizeMarket = (value: string) =>
marketLanguageOptions.some((option) => option.country === value) ? value : defaultMarketLanguageOption.country;
const normalizeLanguage = (value: string) => languageAliases[value] ?? value;
const uniqueLanguages = (languages: string[]) => Array.from(new Set(languages));
const appendEnglish = (languages: string[]) => Array.from(new Set([...languages, "英文"]));
const getMarketLanguageOptions = (marketValue: string) =>
appendEnglish((marketLanguageOptions.find((option) => option.country === marketValue) ?? defaultMarketLanguageOption).languages);
const getPlatformLanguageOptions = (platformValue: string, marketValue: string) => {
const marketLanguages = getMarketLanguageOptions(marketValue);
if (!isDomesticPlatform(platformValue)) return marketLanguages;
const localLanguages = marketLanguages.filter((item) => item !== "英文");
return uniqueLanguages([...localLanguages, ...domesticPlatformLanguages, "英文"]);
};
const getPlatformDefaultLanguage = (platformValue: string, marketValue: string) =>
isDomesticPlatform(platformValue) ? "中文" : (getPlatformLanguageOptions(platformValue, marketValue)[0] ?? languageOptions[0] ?? "英文");
const normalizeLanguageForPlatform = (platformValue: string, marketValue: string, languageValue: string) => {
const normalizedLanguage = normalizeLanguage(languageValue);
const platformLanguages = getPlatformLanguageOptions(platformValue, marketValue);
return platformLanguages.includes(normalizedLanguage) ? normalizedLanguage : getPlatformDefaultLanguage(platformValue, marketValue);
};
const productSetOutputOptions: Array<{ key: ProductSetOutputKey; label: string; desc: string; icon: ReactNode }> = [
{ key: "set", label: "套图", desc: "主图/卖点/场景", icon: <AppstoreOutlined /> },
{ key: "detail", label: "详情图", desc: "长图模块化生成", icon: <LayoutOutlined /> },
@@ -820,9 +402,6 @@ const maxCloneProductImages = 7;
const maxCloneReferenceImages = 20;
const cloneVideoDurationMin = 5;
const cloneVideoDurationMax = 45;
const defaultEcommercePlatform = "淘宝/天猫";
const defaultProductSetOutput: ProductSetOutputKey = "set";
const defaultCloneOutput: CloneOutputKey = "set";
const cloneLatestSettingStorageKey = "omniai.clone-ai.latest-setting";
const ecommerceHistoryStorageKey = "omniai.ecommerce.history.records";
const cloneVideoQualityOptions: Array<{ key: CloneVideoQualityKey; label: string; desc: string }> = [
@@ -3335,85 +2914,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
return urls;
};
const setCountLabels: Record<CloneSetCountKey, { label: string; promptDesc: string }> = {
selling: { label: "卖点图", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" },
white: { label: "白底图", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" },
scene: { label: "场景图", promptDesc: "lifestyle scene image showing the product in a realistic usage environment with natural surroundings" },
};
const buildDetailModulePrompt = (moduleIds: string[]): string => {
if (!moduleIds.length) {
return "Generate a complete A+ detail layout with hero, selling points, usage scene, product detail, and specification modules.";
}
const selectedModules = cloneDetailModules.filter((module) => moduleIds.includes(module.id));
if (!selectedModules.length) return "";
const moduleList = selectedModules.map((module) => `${module.title}: ${module.desc}`).join("; ");
return `Only generate these selected A+ detail modules, no extra modules: ${moduleList}. Keep the output focused even if only one or two modules are selected.`;
};
const buildSetSubPrompt = (countKey: CloneSetCountKey, index: number, totalCount: number, pPlatform: string, pRatio: string, pLanguage: string, pMarket: string): string => {
const info = setCountLabels[countKey];
const parts: string[] = [];
parts.push(`Generate an e-commerce ${info.label.toLowerCase()} for a product listing.`);
parts.push(info.promptDesc);
if (countKey === "white") {
parts.push("The output must be a clean white-background product image. Do not use lifestyle backgrounds, props, text overlays, or people.");
}
if (countKey === "scene") {
parts.push("The output must be a realistic usage scene image. Keep the product clearly visible and preserve its shape, color, and key details.");
}
if (countKey === "selling") {
parts.push("The output must be a selling-point graphic with clear hierarchy, concise copy, and product detail callouts.");
}
if (totalCount > 1) {
parts.push(`This is variant ${index + 1} of ${totalCount} —vary the angle, composition, or emphasis to make each distinct.`);
}
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
parts.push("Must comply with platform image guidelines —proper margins, no watermark, professional quality.");
return parts.join(" ");
};
const buildEcommerceImagePrompt = (
outputKey: CloneOutputKey, userText: string,
pPlatform: string, pRatio: string, pLanguage: string, pMarket: string,
tryOnOptions?: EcommerceImagePromptOptions,
): string => {
const parts: string[] = [];
if (outputKey === "detail") {
parts.push("Generate a professional A+ detail page hero image for an e-commerce product listing.");
parts.push("Create a high-impact first-screen visual that combines the product photo with key selling points, usage scenes, and detailed specifications in a cohesive layout.");
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
if (outputKey === "detail" && tryOnOptions?.detailModules) parts.push(buildDetailModulePrompt(tryOnOptions.detailModules));
parts.push("Follow platform A+ page best practices —clear hierarchy, professional typography, high visual impact.");
} else if (outputKey === "model") {
parts.push("Generate model/try-on lifestyle images for an e-commerce product listing.");
parts.push("Show the product being used or worn by a model in attractive lifestyle settings.");
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
if (tryOnOptions) {
if (tryOnOptions.gender) parts.push(`Model gender: ${tryOnOptions.gender}.`);
if (tryOnOptions.age) parts.push(`Model age: ${tryOnOptions.age}.`);
if (tryOnOptions.ethnicity) parts.push(`Model ethnicity: ${tryOnOptions.ethnicity}.`);
if (tryOnOptions.body) parts.push(`Model body type: ${tryOnOptions.body}.`);
if (tryOnOptions.appearance) parts.push(`Model appearance details: ${tryOnOptions.appearance}.`);
if (tryOnOptions.scenes?.length) parts.push(`Background scenes: ${tryOnOptions.scenes.join(", ")}.`);
if (tryOnOptions.customScene) parts.push(`Custom background scene: ${tryOnOptions.customScene}.`);
if (tryOnOptions.smartScene) parts.push("Use smart scene matching to select the best background context.");
}
parts.push("Model should appear natural and appealing. Background should complement the product. Image must meet platform standards.");
} else if (outputKey === "hot") {
parts.push("Generate a high-conversion e-commerce product image that closely replicates the style and composition of the reference image while adapting it to the target platform.");
parts.push(`Replicate the visual style, color palette, and layout feel of the source product image, then adapt it for ${pPlatform} marketplace standards.`);
parts.push(`Platform: ${pPlatform}. Aspect ratio: ${pRatio}. Language/copy: ${pLanguage}. Market: ${pMarket}.`);
parts.push("The result must look professional and optimized for high click-through rate and conversion on the specified platform.");
}
if (userText.trim()) {
parts.push(`Additional user requirements: ${userText.trim()}`);
}
return parts.join(" ");
};
const generateSetImages = async (
images: CloneImageItem[],
counts: Record<CloneSetCountKey, number>,
@@ -3539,7 +3039,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
return;
}
const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket, tryOnOptions);
const prompt = buildEcommerceImagePrompt(outputKey, userText, pPlatform, pRatio, pLanguage, pMarket, tryOnOptions, cloneDetailModules);
const stamp = Date.now();
setGenerationProgress(0);
@@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import {
defaultCloneOutput,
defaultEcommercePlatform,
defaultProductSetOutput,
formatUploadedImageRatio,
getPlatformDefaultLanguage,
getPlatformDefaultRatio,
getPlatformLanguageOptions,
getPlatformRatioOptions,
getUniqueRatioOptions,
normalizeLanguageForPlatform,
normalizeMarket,
normalizePlatform,
normalizeRatioForPlatform,
platformOptions,
} from "./platformRules";
describe("platform defaults", () => {
it("exposes the default ecommerce platform and outputs", () => {
expect(defaultEcommercePlatform).toBe("淘宝/天猫");
expect(defaultProductSetOutput).toBe("set");
expect(defaultCloneOutput).toBe("set");
});
it("lists platform labels for UI selectors", () => {
expect(platformOptions).toContain("淘宝/天猫");
expect(platformOptions).toContain("亚马逊 Amazon");
expect(platformOptions).toContain("TikTok Shop");
});
});
describe("normalizePlatform", () => {
it("normalizes legacy labels", () => {
expect(normalizePlatform("亚马逊Amazon")).toBe("亚马逊 Amazon");
expect(normalizePlatform("亚马逊")).toBe("亚马逊 Amazon");
});
it("falls back to the default platform for unknown labels", () => {
expect(normalizePlatform("unknown")).toBe("淘宝/天猫");
});
});
describe("platform ratios", () => {
it("returns mode-specific ratios", () => {
expect(getPlatformRatioOptions("淘宝/天猫", "set")).toContain("1000×1000px\u00a0\u00a0\u00a01:1");
expect(getPlatformDefaultRatio("淘宝/天猫", "video")).toBe("1080×1920px\u00a0\u00a0\u00a09:16");
});
it("normalizes an existing or partially matching ratio for a platform", () => {
expect(normalizeRatioForPlatform("淘宝/天猫", "1000×1000px\u00a0\u00a0\u00a01:1", "set")).toBe("1000×1000px\u00a0\u00a0\u00a01:1");
expect(normalizeRatioForPlatform("淘宝/天猫", "1000×1000px", "set")).toBe("1000×1000px\u00a0\u00a0\u00a01:1");
});
it("falls back to the mode default when no ratio matches", () => {
expect(normalizeRatioForPlatform("淘宝/天猫", "nope", "set")).toBe("1000×1000px\u00a0\u00a0\u00a01:1");
});
it("deduplicates ratio lists without changing order", () => {
expect(getUniqueRatioOptions(["1:1", "3:4", "1:1"])).toEqual(["1:1", "3:4"]);
});
});
describe("market and language rules", () => {
it("normalizes unknown markets to the default country", () => {
expect(normalizeMarket("火星")).toBe("中国");
});
it("uses Chinese by default for domestic platforms", () => {
expect(getPlatformDefaultLanguage("淘宝/天猫", "美国")).toBe("中文");
});
it("includes English for domestic platforms while preserving local languages", () => {
expect(getPlatformLanguageOptions("淘宝/天猫", "美国")).toEqual(["中文", "英文"]);
});
it("uses market languages for cross-border platforms", () => {
expect(getPlatformDefaultLanguage("亚马逊 Amazon", "日本")).toBe("日文");
});
it("normalizes language aliases and falls back when not available", () => {
expect(normalizeLanguageForPlatform("亚马逊 Amazon", "日本", "日语")).toBe("日文");
expect(normalizeLanguageForPlatform("亚马逊 Amazon", "日本", "德语")).toBe("日文");
});
});
describe("formatUploadedImageRatio", () => {
it("formats dimensions and aspect ratio", () => {
expect(formatUploadedImageRatio({ width: 750, height: 1000, format: "PNG" })).toBe("上传图片 750×1000px\u00a0\u00a0\u00a03:4\u00a0\u00a0\u00a0PNG");
});
it("falls back to original ratio when dimensions are missing", () => {
expect(formatUploadedImageRatio({ format: "JPG" })).toBe("上传图片\u00a0\u00a0\u00a0原图比例\u00a0\u00a0\u00a0JPG");
});
});
@@ -0,0 +1,479 @@
import { formatAspectRatio, normalizeRatioToken } from "./ratioUtils";
export type ProductSetOutputKey = "set" | "detail" | "model" | "video";
export type CloneOutputKey = ProductSetOutputKey | "hot";
export type PlatformRatioModeKey = ProductSetOutputKey | "hot";
export interface PlatformRatioGroup {
ratios: string[];
defaultRatio: string;
}
export interface EcommercePlatformSpec {
label: string;
ratios: string[];
defaultRatio: string;
ratioGroups?: Partial<Record<PlatformRatioModeKey, PlatformRatioGroup>>;
specs: string[];
tip?: string;
aliases?: string[];
}
export const platformSpecOptions: EcommercePlatformSpec[] = [
{
label: "淘宝/天猫",
ratios: ["淘宝主图 / SKU 图 800×800px", "详情页宽 750px", "详情页宽 790px"],
defaultRatio: "淘宝主图 / SKU 图 800×800px",
ratioGroups: {
set: {
ratios: ["1000×1000px\u00a0\u00a0\u00a01:1", "800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: [
"750×1000px\u00a0\u00a0\u00a03:4",
"790×1053px\u00a0\u00a0\u00a03:4",
"750×1125px\u00a0\u00a0\u00a02:3",
"790×1185px\u00a0\u00a0\u00a02:3",
],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1080×1440px\u00a0\u00a0\u00a03:4", "1080×1080px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 / SKU 图 800×800px,≤3MB", "详情页宽 750px 或 790px,单张高度≤1546px"],
tip: "建议主图 200-400KB JPG,超过 500KB 会影响加载速度。",
},
{
label: "京东",
ratios: ["京东主图 / SKU 图 800×800px", "详情页宽 750px", "首图主体占比 ≥80%"],
defaultRatio: "京东主图 / SKU 图 800×800px",
ratioGroups: {
set: {
ratios: ["1000×1000px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: [
"750×1000px\u00a0\u00a0\u00a03:4",
"990×1320px\u00a0\u00a0\u00a03:4",
"750×1125px\u00a0\u00a0\u00a02:3",
"990×1485px\u00a0\u00a0\u00a02:3",
],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1125px\u00a0\u00a0\u00a02:3", "990×1485px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1920×1080px\u00a0\u00a0\u00a016:9"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 / SKU 图 800×800px,白底,≤3MB", "详情页宽 750px,首图主体占比 ≥80%"],
},
{
label: "拼多多",
ratios: ["主图 750×352px", "主图 800×800px", "详情页宽 750px"],
defaultRatio: "主图 750×352px",
ratioGroups: {
set: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1", "750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 750×352px 或 800×800px,≤1MB", "详情页宽 750px,要求纯白底、无水印、无拼接"],
},
{
label: "抖音电商",
ratios: ["短视频1080×1920px"],
defaultRatio: "短视频1080×1920px",
ratioGroups: {
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["短视频 1080×1920px9:16", "30s 内最佳"],
},
{
label: "亚马逊 Amazon",
ratios: ["主图 ≥1600×1600px", "建议 2000×2000px+", "最小 500×500px"],
defaultRatio: "主图 ≥1600×1600px",
ratioGroups: {
set: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1", "1200×1800px\u00a0\u00a0\u00a02:3", "1200×1600px\u00a0\u00a0\u00a03:4"],
defaultRatio: "1200×1800px\u00a0\u00a0\u00a02:3",
},
model: {
ratios: ["1200×1800px\u00a0\u00a0\u00a02:3"],
defaultRatio: "1200×1800px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1920×1080px\u00a0\u00a0\u00a016:9"],
defaultRatio: "1920×1080px\u00a0\u00a0\u00a016:9",
},
hot: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图 1600×1600px+,纯白底,≤10MB", "最小 500×500px,建议 2000px+ 以支持缩放"],
aliases: ["亚马逊"],
},
{
label: "Shopee",
ratios: ["商品主图 1024×1024px", "基础主图 800×800px"],
defaultRatio: "商品主图 1024×1024px",
ratioGroups: {
set: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品主图推荐 1024×1024px,基础 800×800px", "≤2MB,白底或浅色底"],
aliases: ["虾皮 Shopee/Lazada", "虾皮"],
},
{
label: "Lazada",
ratios: ["商品主图 800×800px"],
defaultRatio: "商品主图 800×800px",
ratioGroups: {
set: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
model: {
ratios: ["750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品主图 800×800px1:1"],
},
{
label: "Instagram",
ratios: ["帖子 1080×1350px", "帖子 1080×1080px", "Stories / Reels 1080×1920px", "头像 320×320px"],
defaultRatio: "帖子 1080×1350px",
ratioGroups: {
set: {
ratios: ["1080×1080px\u00a0\u00a0\u00a01:1", "1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1080px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
model: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
},
specs: ["帖子 1080×1350px 或 1080×1080px", "Stories / Reels 封面 1080×1920px,头像 320×320px"],
tip: "建议 ≤8MB JPG。",
aliases: ["Instagram Reels"],
},
{
label: "速卖通",
ratios: ["主图 800×800px", "主图 1000×1000px+"],
defaultRatio: "主图 800×800px",
ratioGroups: {
set: {
ratios: ["1000×1000px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["750×1125px\u00a0\u00a0\u00a02:3", "750×1000px\u00a0\u00a0\u00a03:4"],
defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3",
},
model: {
ratios: ["750×1125px\u00a0\u00a0\u00a02:3"],
defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1920×1080px\u00a0\u00a0\u00a016:9"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["主图建议 800×800px 或更高,1:1", "适合跨境电商主图、SKU 图和场景图"],
},
{
label: "eBay",
ratios: ["商品图1:1", "白底多角度展示图 1:1"],
defaultRatio: "商品图1:1",
ratioGroups: {
set: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1000×1500px\u00a0\u00a0\u00a02:3", "1200×1600px\u00a0\u00a0\u00a03:4"],
defaultRatio: "1000×1500px\u00a0\u00a0\u00a02:3",
},
model: {
ratios: ["1000×1500px\u00a0\u00a0\u00a02:3"],
defaultRatio: "1000×1500px\u00a0\u00a0\u00a02:3",
},
video: {
ratios: ["1920×1080px\u00a0\u00a0\u00a016:9", "1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1920×1080px\u00a0\u00a0\u00a016:9",
},
hot: {
ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品图建议 1:1,主体清晰居中", "适合白底主图和多角度展示图"],
},
{
label: "TikTok Shop",
ratios: ["商品主图 1:1", "短视频/ 竖版封面 9:16"],
defaultRatio: "商品主图 1:1",
ratioGroups: {
set: {
ratios: ["1280×1280px\u00a0\u00a0\u00a01:1"],
defaultRatio: "1280×1280px\u00a0\u00a0\u00a01:1",
},
detail: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
model: {
ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"],
defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5",
},
video: {
ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"],
defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16",
},
hot: {
ratios: ["800×800px\u00a0\u00a0\u00a01:1"],
defaultRatio: "800×800px\u00a0\u00a0\u00a01:1",
},
},
specs: ["商品主图建议 1:1", "短视频竖版封面建议 9:16"],
},
];
export const platformOptions = platformSpecOptions.map((option) => option.label);
const getPlatformLogoText = (value: string) => {
const normalized = value.toLowerCase();
if (value.includes("淘宝") || value.includes("天猫")) return "淘";
if (value.includes("京东")) return "京";
if (value.includes("拼多多") || value.includes("鎷煎澶")) return "拼";
if (value.includes("抖音")) return "抖";
if (normalized.includes("amazon")) return "a";
if (normalized.includes("shopee")) return "S";
if (normalized.includes("lazada")) return "L";
if (normalized.includes("instagram")) return "IG";
if (value.includes("速卖通") || value.includes("閫熷崠閫")) return "AE";
if (normalized.includes("ebay")) return "eB";
if (normalized.includes("tiktok")) return "♪";
return value.trim().slice(0, 1).toUpperCase() || "商";
};
const getPlatformLogoVariant = (value: string) => {
const normalized = value.toLowerCase();
if (value.includes("淘宝") || value.includes("天猫")) return "taobao";
if (value.includes("京东")) return "jd";
if (value.includes("拼多多") || value.includes("鎷煎澶")) return "pdd";
if (value.includes("抖音")) return "douyin";
if (normalized.includes("amazon")) return "amazon";
if (normalized.includes("shopee")) return "shopee";
if (normalized.includes("lazada")) return "lazada";
if (normalized.includes("instagram")) return "instagram";
if (value.includes("速卖通") || value.includes("閫熷崠閫")) return "aliexpress";
if (normalized.includes("ebay")) return "ebay";
if (normalized.includes("tiktok")) return "tiktok";
return "default";
};
const getPlatformLogoMarks = (value: string) => {
if (value.includes("淘宝") || value.includes("天猫")) return ["淘", "猫"];
return [getPlatformLogoText(value)];
};
export const marketLanguageOptions: Array<{ country: string; languages: string[] }> = [
{ country: "中国", languages: ["中文"] },
{ country: "美国", languages: ["英文"] },
{ country: "加拿大", languages: ["英文", "法文"] },
{ country: "英国", languages: ["英文"] },
{ country: "德国", languages: ["德文"] },
{ country: "法国", languages: ["法文"] },
{ country: "意大利", languages: ["意大利语"] },
{ country: "西班牙", languages: ["西班牙语"] },
{ country: "日本", languages: ["日文"] },
{ country: "韩国", languages: ["韩文"] },
{ country: "澳大利亚", languages: ["英文"] },
{ country: "新加坡", languages: ["英文", "中文"] },
{ country: "马来西亚", languages: ["马来语", "英文", "中文"] },
{ country: "印尼", languages: ["印度尼西亚语", "英文"] },
{ country: "越南", languages: ["越南语", "英文"] },
{ country: "泰国", languages: ["泰语", "英文"] },
{ country: "菲律宾", languages: ["菲律宾语(他加禄语)", "英文"] },
{ country: "巴西", languages: ["葡萄牙语"] },
{ country: "墨西哥", languages: ["西班牙语"] },
{ country: "智利", languages: ["西班牙语"] },
{ country: "哥伦比亚", languages: ["西班牙语"] },
{ country: "阿联酋", languages: ["阿拉伯语", "英文"] },
{ country: "沙特阿拉伯", languages: ["阿拉伯语", "英文"] },
{ country: "俄罗斯", languages: ["俄语"] },
{ country: "波兰", languages: ["波兰语"] },
];
export const marketOptions = marketLanguageOptions.map((option) => option.country);
export const languageOptions = Array.from(new Set(marketLanguageOptions.flatMap((option) => option.languages)));
export const languageAliases: Record<string, string> = {
"英文": "英文",
"中文": "中文",
"英语": "英文",
"日语": "日文",
"日文": "日文",
"德语": "德文",
"德文": "德文",
"法语": "法文",
"法文": "法文",
"韩语": "韩文",
"韩文": "韩文",
"西文": "西班牙语",
"西班牙语": "西班牙语",
"葡文": "葡萄牙语",
"葡萄牙语": "葡萄牙语",
"印尼语": "印度尼西亚语",
"印度尼西亚语": "印度尼西亚语",
"菲律宾语": "菲律宾语(他加禄语)",
"菲律宾语(他加禄语)": "菲律宾语(他加禄语)",
};
export const defaultPlatformSpec = platformSpecOptions[0]!;
export const getPlatformSpec = (value: string) =>
platformSpecOptions.find((option) => option.label === value || option.aliases?.includes(value)) ?? defaultPlatformSpec;
export const legacyPlatformAliases: Record<string, string> = {
"淘宝/天猫": "淘宝/天猫",
"京东": "京东",
"拼多多": "拼多多",
"抖音电商": "抖音电商",
"亚马逊Amazon": "亚马逊 Amazon",
"速卖通": "速卖通",
};
export const normalizePlatform = (value: string) => getPlatformSpec(legacyPlatformAliases[value] ?? value).label;
export const domesticPlatformLabels = new Set(["淘宝/天猫", "京东", "拼多多", "抖音电商"]);
export const domesticPlatformLanguages = ["中文"];
export const isDomesticPlatform = (platformValue: string) => domesticPlatformLabels.has(normalizePlatform(platformValue));
export const getPlatformRatioGroup = (value: string, mode?: PlatformRatioModeKey): PlatformRatioGroup => {
const platformSpec = getPlatformSpec(value);
return (mode ? platformSpec.ratioGroups?.[mode] : null) ?? {
ratios: platformSpec.ratios,
defaultRatio: platformSpec.defaultRatio,
};
};
export const getPlatformRatioOptions = (value: string, mode?: PlatformRatioModeKey) => getPlatformRatioGroup(value, mode).ratios;
export const getPlatformDefaultRatio = (value: string, mode?: PlatformRatioModeKey) => getPlatformRatioGroup(value, mode).defaultRatio;
export const getUniqueRatioOptions = (ratios: string[]) => Array.from(new Set(ratios));
export const normalizeRatioForPlatform = (platformValue: string, ratioValue: string, mode?: PlatformRatioModeKey) => {
const platformRatios = getPlatformRatioOptions(platformValue, mode);
if (platformRatios.includes(ratioValue)) return ratioValue;
const normalizedRatio = normalizeRatioToken(ratioValue);
const matchedRatio = platformRatios.find((option) => normalizeRatioToken(option).includes(normalizedRatio));
return matchedRatio ?? getPlatformDefaultRatio(platformValue, mode);
};
export const defaultMarketLanguageOption = marketLanguageOptions[0]!;
export const normalizeMarket = (value: string) =>
marketLanguageOptions.some((option) => option.country === value) ? value : defaultMarketLanguageOption.country;
export const normalizeLanguage = (value: string) => languageAliases[value] ?? value;
export const uniqueLanguages = (languages: string[]) => Array.from(new Set(languages));
export const appendEnglish = (languages: string[]) => Array.from(new Set([...languages, "英文"]));
export const getMarketLanguageOptions = (marketValue: string) =>
appendEnglish((marketLanguageOptions.find((option) => option.country === marketValue) ?? defaultMarketLanguageOption).languages);
export const getPlatformLanguageOptions = (platformValue: string, marketValue: string) => {
const marketLanguages = getMarketLanguageOptions(marketValue);
if (!isDomesticPlatform(platformValue)) return marketLanguages;
const localLanguages = marketLanguages.filter((item) => item !== "英文");
return uniqueLanguages([...localLanguages, ...domesticPlatformLanguages, "英文"]);
};
export const getPlatformDefaultLanguage = (platformValue: string, marketValue: string) =>
isDomesticPlatform(platformValue) ? "中文" : (getPlatformLanguageOptions(platformValue, marketValue)[0] ?? languageOptions[0] ?? "英文");
export const normalizeLanguageForPlatform = (platformValue: string, marketValue: string, languageValue: string) => {
const normalizedLanguage = normalizeLanguage(languageValue);
const platformLanguages = getPlatformLanguageOptions(platformValue, marketValue);
return platformLanguages.includes(normalizedLanguage) ? normalizedLanguage : getPlatformDefaultLanguage(platformValue, marketValue);
};
export const defaultEcommercePlatform = "淘宝/天猫";
export const defaultProductSetOutput: ProductSetOutputKey = "set";
export const defaultCloneOutput: CloneOutputKey = "set";
export const formatUploadedImageRatio = (image?: { width?: number; height?: number; format?: string }) => {
if (!image) return null;
const format = image.format ? `\u00a0\u00a0\u00a0${image.format}` : "";
if (!image.width || !image.height) return `上传图片\u00a0\u00a0\u00a0原图比例${format}`;
return `上传图片 ${image.width}×${image.height}px\u00a0\u00a0\u00a0${formatAspectRatio(image.width, image.height)}${format}`;
};
@@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest";
import {
buildDetailModulePrompt,
buildEcommerceImagePrompt,
buildSetSubPrompt,
setCountLabels,
type EcommercePromptDetailModule,
} from "./promptBuilder";
const detailModules: EcommercePromptDetailModule[] = [
{ id: "hero", title: "首页焦点图", desc: "集中呈现核心利益点" },
{ id: "usage", title: "使用情境图", desc: "还原实际使用画面" },
{ id: "spec", title: "参数信息表", desc: "整理商品关键数据" },
];
describe("buildDetailModulePrompt", () => {
it("uses the complete-detail prompt when no modules are selected", () => {
expect(buildDetailModulePrompt([], detailModules)).toContain("complete A+ detail layout");
});
it("includes only selected modules", () => {
const prompt = buildDetailModulePrompt(["hero", "spec"], detailModules);
expect(prompt).toContain("首页焦点图: 集中呈现核心利益点");
expect(prompt).toContain("参数信息表: 整理商品关键数据");
expect(prompt).not.toContain("使用情境图");
});
it("returns an empty prompt for unknown selected ids", () => {
expect(buildDetailModulePrompt(["missing"], detailModules)).toBe("");
});
});
describe("buildSetSubPrompt", () => {
it("builds white-background prompts with strict background guidance", () => {
const prompt = buildSetSubPrompt("white", 0, 1, "淘宝/天猫", "1:1", "中文", "中国");
expect(prompt).toContain(setCountLabels.white.label);
expect(prompt).toContain("clean white-background product image");
expect(prompt).toContain("Platform: 淘宝/天猫. Aspect ratio: 1:1. Language/copy: 中文. Market: 中国.");
});
it("adds variant guidance when generating multiple images", () => {
expect(buildSetSubPrompt("scene", 1, 3, "Amazon", "3:4", "英文", "美国")).toContain("variant 2 of 3");
});
});
describe("buildEcommerceImagePrompt", () => {
it("builds detail prompts with selected A+ modules", () => {
const prompt = buildEcommerceImagePrompt(
"detail",
"突出轻量化",
"京东",
"3:4",
"中文",
"中国",
{ detailModules: ["usage"] },
detailModules,
);
expect(prompt).toContain("professional A+ detail page");
expect(prompt).toContain("使用情境图: 还原实际使用画面");
expect(prompt).toContain("Additional user requirements: 突出轻量化");
});
it("builds model prompts with model attributes and scenes", () => {
const prompt = buildEcommerceImagePrompt(
"model",
"",
"Shopee",
"3:4",
"英文",
"美国",
{ gender: "女", age: "青年", ethnicity: "亚洲人", body: "标准", appearance: "短发", scenes: ["都市街头"], smartScene: true },
);
expect(prompt).toContain("Model gender: 女.");
expect(prompt).toContain("Background scenes: 都市街头.");
expect(prompt).toContain("Use smart scene matching");
});
it("builds hot-replication prompts", () => {
const prompt = buildEcommerceImagePrompt("hot", "", "TikTok Shop", "9:16", "英文", "美国");
expect(prompt).toContain("closely replicates the style");
expect(prompt).toContain("TikTok Shop marketplace standards");
});
});
@@ -0,0 +1,113 @@
import type { CloneOutputKey } from "./platformRules";
export type EcommerceSetCountKey = "selling" | "white" | "scene";
export interface EcommercePromptDetailModule {
id: string;
title: string;
desc: string;
}
export interface EcommerceImagePromptOptions {
gender?: string;
age?: string;
ethnicity?: string;
body?: string;
appearance?: string;
scenes?: string[];
customScene?: string;
smartScene?: boolean;
detailModules?: string[];
}
export const setCountLabels: Record<EcommerceSetCountKey, { label: string; promptDesc: string }> = {
selling: { label: "卖点图", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" },
white: { label: "白底图", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" },
scene: { label: "场景图", promptDesc: "lifestyle scene image showing the product in a realistic usage environment with natural surroundings" },
};
export const buildDetailModulePrompt = (moduleIds: string[], modules: EcommercePromptDetailModule[]): string => {
if (!moduleIds.length) {
return "Generate a complete A+ detail layout with hero, selling points, usage scene, product detail, and specification modules.";
}
const selectedModules = modules.filter((module) => moduleIds.includes(module.id));
if (!selectedModules.length) return "";
const moduleList = selectedModules.map((module) => `${module.title}: ${module.desc}`).join("; ");
return `Only generate these selected A+ detail modules, no extra modules: ${moduleList}. Keep the output focused even if only one or two modules are selected.`;
};
export const buildSetSubPrompt = (
countKey: EcommerceSetCountKey,
index: number,
totalCount: number,
platform: string,
ratio: string,
language: string,
market: string,
): string => {
const info = setCountLabels[countKey];
const parts: string[] = [];
parts.push(`Generate an e-commerce ${info.label.toLowerCase()} for a product listing.`);
parts.push(info.promptDesc);
if (countKey === "white") {
parts.push("The output must be a clean white-background product image. Do not use lifestyle backgrounds, props, text overlays, or people.");
}
if (countKey === "scene") {
parts.push("The output must be a realistic usage scene image. Keep the product clearly visible and preserve its shape, color, and key details.");
}
if (countKey === "selling") {
parts.push("The output must be a selling-point graphic with clear hierarchy, concise copy, and product detail callouts.");
}
if (totalCount > 1) {
parts.push(`This is variant ${index + 1} of ${totalCount} —vary the angle, composition, or emphasis to make each distinct.`);
}
parts.push(`Platform: ${platform}. Aspect ratio: ${ratio}. Language/copy: ${language}. Market: ${market}.`);
parts.push("Must comply with platform image guidelines —proper margins, no watermark, professional quality.");
return parts.join(" ");
};
export const buildEcommerceImagePrompt = (
outputKey: CloneOutputKey,
userText: string,
platform: string,
ratio: string,
language: string,
market: string,
options?: EcommerceImagePromptOptions,
detailModules: EcommercePromptDetailModule[] = [],
): string => {
const parts: string[] = [];
if (outputKey === "detail") {
parts.push("Generate a professional A+ detail page hero image for an e-commerce product listing.");
parts.push("Create a high-impact first-screen visual that combines the product photo with key selling points, usage scenes, and detailed specifications in a cohesive layout.");
parts.push(`Platform: ${platform}. Aspect ratio: ${ratio}. Language/copy: ${language}. Market: ${market}.`);
if (options?.detailModules) parts.push(buildDetailModulePrompt(options.detailModules, detailModules));
parts.push("Follow platform A+ page best practices —clear hierarchy, professional typography, high visual impact.");
} else if (outputKey === "model") {
parts.push("Generate model/try-on lifestyle images for an e-commerce product listing.");
parts.push("Show the product being used or worn by a model in attractive lifestyle settings.");
parts.push(`Platform: ${platform}. Aspect ratio: ${ratio}. Language/copy: ${language}. Market: ${market}.`);
if (options) {
if (options.gender) parts.push(`Model gender: ${options.gender}.`);
if (options.age) parts.push(`Model age: ${options.age}.`);
if (options.ethnicity) parts.push(`Model ethnicity: ${options.ethnicity}.`);
if (options.body) parts.push(`Model body type: ${options.body}.`);
if (options.appearance) parts.push(`Model appearance details: ${options.appearance}.`);
if (options.scenes?.length) parts.push(`Background scenes: ${options.scenes.join(", ")}.`);
if (options.customScene) parts.push(`Custom background scene: ${options.customScene}.`);
if (options.smartScene) parts.push("Use smart scene matching to select the best background context.");
}
parts.push("Model should appear natural and appealing. Background should complement the product. Image must meet platform standards.");
} else if (outputKey === "hot") {
parts.push("Generate a high-conversion e-commerce product image that closely replicates the style and composition of the reference image while adapting it to the target platform.");
parts.push(`Replicate the visual style, color palette, and layout feel of the source product image, then adapt it for ${platform} marketplace standards.`);
parts.push(`Platform: ${platform}. Aspect ratio: ${ratio}. Language/copy: ${language}. Market: ${market}.`);
parts.push("The result must look professional and optimized for high click-through rate and conversion on the specified platform.");
}
if (userText.trim()) {
parts.push(`Additional user requirements: ${userText.trim()}`);
}
return parts.join(" ");
};
@@ -12,18 +12,23 @@ import {
} from "./ratioUtils";
describe("normalizeRatioToken", () => {
it("converts fullwidth and mojibake characters", () => {
it("normalizes non-breaking spaces", () => {
expect(normalizeRatioToken("800\u00a0\u00a0px")).toBe("800 px");
});
it("replaces * with ×", () => {
it("replaces plain separators with the normal multiplication sign", () => {
expect(normalizeRatioToken("800*800")).toBe("800×800");
});
it("replaces mojibake 脳 with ×", () => {
expect(normalizeRatioToken("800脳800")).toBe("800×800");
it("replaces legacy mojibake multiply signs", () => {
expect(normalizeRatioToken("800\u8133800")).toBe("800×800");
});
it("replaces fullwidth colon", () => {
it("replaces fullwidth and legacy mojibake colons", () => {
expect(normalizeRatioToken("11")).toBe("1:1");
expect(normalizeRatioToken("1\u951b?1")).toBe("1:1");
});
it("collapses whitespace and trims", () => {
expect(normalizeRatioToken(" 1 : 1 ")).toBe("1 : 1");
});
@@ -34,10 +39,12 @@ describe("greatestCommonDivisor", () => {
expect(greatestCommonDivisor(12, 8)).toBe(4);
expect(greatestCommonDivisor(1920, 1080)).toBe(120);
});
it("handles zero with fallback to 1", () => {
expect(greatestCommonDivisor(0, 5)).toBe(5);
expect(greatestCommonDivisor(0, 0)).toBe(1);
});
it("handles negatives via abs", () => {
expect(greatestCommonDivisor(-12, 8)).toBe(4);
});
@@ -47,9 +54,11 @@ describe("formatAspectRatio", () => {
it("reduces 1920x1080 to 16:9", () => {
expect(formatAspectRatio(1920, 1080)).toBe("16:9");
});
it("reduces 750x1000 to 3:4", () => {
expect(formatAspectRatio(750, 1000)).toBe("3:4");
});
it("reduces 800x800 to 1:1", () => {
expect(formatAspectRatio(800, 800)).toBe("1:1");
});
@@ -59,13 +68,16 @@ describe("getQuickSetRatioValue", () => {
it("passes through a canonical quick-set value", () => {
expect(getQuickSetRatioValue("1:1")).toBe("1:1");
});
it("derives from a WxH size string", () => {
expect(getQuickSetRatioValue("1920×1080px")).toBe("16:9");
expect(getQuickSetRatioValue("750×1000px")).toBe("3:4");
});
it("derives from a raw ratio string", () => {
expect(getQuickSetRatioValue("9:16")).toBe("9:16");
});
it("falls back to 1:1 for unparseable input", () => {
expect(getQuickSetRatioValue("unknown")).toBe("1:1");
});
@@ -75,8 +87,13 @@ describe("formatRatioDisplayValue", () => {
it("formats a WxHpx string with aspect suffix", () => {
expect(formatRatioDisplayValue("1000×1000px 1:1")).toBe("1000×1000px\u00a0\u00a0\u00a01:1");
});
it("reformats 800×800px without explicit aspect", () => {
expect(formatRatioDisplayValue("800×800px")).toBe("800×800px\u00a0\u00a0\u00a01:1");
it("reformats 800x800px without explicit aspect", () => {
expect(formatRatioDisplayValue("800x800px")).toBe("800×800px\u00a0\u00a0\u00a01:1");
});
it("replaces legacy mojibake product image labels", () => {
expect(formatRatioDisplayValue("\u935f\u55d7\u6427\u9365?")).toBe("商品图");
});
});
@@ -87,6 +104,7 @@ describe("getRatioDisplayParts", () => {
aspect: "1:1",
});
});
it("uses 自适应 when no aspect present", () => {
const parts = getRatioDisplayParts("原图");
expect(parts.aspect).toBe("自适应");
@@ -97,6 +115,7 @@ describe("parseRatioToAspectCss", () => {
it("extracts CSS aspect-ratio", () => {
expect(parseRatioToAspectCss("1000×1000px 1:1")).toBe("1000 / 1000");
});
it("falls back to 1 / 1", () => {
expect(parseRatioToAspectCss("no numbers")).toBe("1 / 1");
});
@@ -106,18 +125,23 @@ describe("toSupportedImageApiRatio", () => {
it("snaps square to 1:1", () => {
expect(toSupportedImageApiRatio(800, 800)).toBe("1:1");
});
it("snaps 750x1000 to 3:4", () => {
expect(toSupportedImageApiRatio(750, 1000)).toBe("3:4");
});
it("snaps 1920x1080 to 16:9", () => {
expect(toSupportedImageApiRatio(1920, 1080)).toBe("16:9");
});
it("snaps 1080x1920 to 9:16", () => {
expect(toSupportedImageApiRatio(1080, 1920)).toBe("9:16");
});
it("snaps 800x600 to 4:3", () => {
expect(toSupportedImageApiRatio(800, 600)).toBe("4:3");
});
it("returns 1:1 for non-finite or non-positive", () => {
expect(toSupportedImageApiRatio(NaN, 100)).toBe("1:1");
expect(toSupportedImageApiRatio(0, 100)).toBe("1:1");
@@ -130,13 +154,16 @@ describe("normalizeRatioForApi", () => {
expect(normalizeRatioForApi("1000×1000px 1:1")).toBe("1:1");
expect(normalizeRatioForApi("750×1000px 3:4")).toBe("3:4");
});
it("derives ratio from a bare size string", () => {
expect(normalizeRatioForApi("1920×1080px")).toBe("16:9");
});
it("returns 1:1 for unparseable input", () => {
expect(normalizeRatioForApi("")).toBe("1:1");
expect(normalizeRatioForApi("无尺寸信息")).toBe("1:1");
});
it("uses the last explicit ratio when multiple present", () => {
expect(normalizeRatioForApi("4:3 16:9")).toBe("16:9");
});
+9 -5
View File
@@ -1,15 +1,19 @@
// 比例 / 尺寸相关的纯计算工具。
// 从 EcommercePage.tsx 抽出,逻辑零改动,仅加 export 以便单测。
// Ratio and dimension formatting helpers.
// Keep compatibility with a few legacy mojibake tokens, but never emit them.
// normalizeRatioForPlatform / formatUploadedImageRatio 因依赖平台规格表与 CloneImageItem
// 暂留在 EcommercePage.tsx,后续随 platformSpec 一起整理。
const LEGACY_MULTIPLY_SIGN = "\u8133";
const LEGACY_FULLWIDTH_COLON = "\u951b?";
const LEGACY_PRODUCT_IMAGE_LABEL = "\u935f\u55d7\u6427\u9365?";
export const normalizeRatioToken = (value: string) =>
value
.replaceAll("\u00a0", " ")
.replaceAll("脳", "×")
.replaceAll(LEGACY_MULTIPLY_SIGN, "×")
.replaceAll("*", "×")
.replaceAll("", ":")
.replace(/锛\?/g, ":")
.replaceAll(LEGACY_FULLWIDTH_COLON, ":")
.replace(/\s+/g, " ")
.trim();
@@ -64,7 +68,7 @@ export const formatRatioDisplayValue = (value: string) => {
.replace("短视频", "短视频")
.replace("主图", "主图")
.replace("商品主图", "商品主图")
.replace("鍟嗗搧鍥?", "商品图")
.replace(LEGACY_PRODUCT_IMAGE_LABEL, "商品图")
.replace(/\s+:/g, ":")
.replace(/:\s+/g, ":");
};