diff --git a/src/App.tsx b/src/App.tsx index 592f2de..5c53b5c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import { toast } from "./components/toast/toastStore"; import { flushPendingGenerationRecords } from "./api/generationRecordClient"; import { keyServerClient } from "./api/keyServerClient"; import { preloadPublicConfig, getLogoUrl } from "./api/publicConfigClient"; +import { preloadPlatformRules } from "./api/platformRulesClient"; import { setUserMaxConcurrency } from "./api/generationConcurrency"; import { SERVER_SESSION_EXPIRED_EVENT, @@ -156,6 +157,9 @@ function App() { const [sessionNotice, setSessionNotice] = useState(null); const [profileMenuOpen, setProfileMenuOpen] = useState(false); const [currentPage, setCurrentPage] = useState<"workspace" | "profile">("workspace"); + // 平台规则 gating:数据就绪(或兜底超时)后才渲染 EcommercePage, + // 保证其模块求值时 platformRulesClient 缓存已填充,拿到 API 数据。 + const [platformRulesReady, setPlatformRulesReady] = useState(false); const [workspaceChrome, setWorkspaceChrome] = useState({ isToolPage: false, }); @@ -184,6 +188,20 @@ function App() { initNotificationPermission(); }, []); + // 启动 gating:预加载平台规则。preload 自带超时+fallback 一定会 resolve; + // 另加 3s 兜底,避免极端情况下首屏久等(兜底放行后用 fallback,数据=正确值)。 + useEffect(() => { + let settled = false; + const markReady = () => { + if (settled) return; + settled = true; + setPlatformRulesReady(true); + }; + void preloadPlatformRules().then(markReady, markReady); + const fallbackTimer = window.setTimeout(markReady, 3_000); + return () => window.clearTimeout(fallbackTimer); + }, []); + useEffect(() => { if (!session) return; void flushPendingGenerationRecords(); @@ -381,19 +399,26 @@ function App() { } > - undefined} - onOpenProject={() => undefined} - onDeleteProject={() => undefined} - onImportWorkflow={() => undefined} - onCreateTask={() => undefined} - onRequireLogin={() => openAuth("login")} - initialTemplate={null} - onInitialTemplateConsumed={() => undefined} - /> + {platformRulesReady ? ( + undefined} + onOpenProject={() => undefined} + onDeleteProject={() => undefined} + onImportWorkflow={() => undefined} + onCreateTask={() => undefined} + onRequireLogin={() => openAuth("login")} + initialTemplate={null} + onInitialTemplateConsumed={() => undefined} + /> + ) : ( +
+
+ 加载中... +
+ )}
diff --git a/src/api/platformRulesClient.ts b/src/api/platformRulesClient.ts new file mode 100644 index 0000000..2270bc9 --- /dev/null +++ b/src/api/platformRulesClient.ts @@ -0,0 +1,470 @@ +// 电商平台规格 + 市场语言业务数据客户端(AGENTS.md 规则4合规)。 +// 数据由后端 GET /api/public/config/profile?name=web-ecommerce-platform-rules 下发, +// 不硬编码在前端业务逻辑里。 +// +// 时序设计(启动 gating):App 启动 boot splash 期间 await preloadPlatformRules(), +// 数据就绪后才渲染 EcommercePage(React.lazy)。因此 platformRules.ts 模块求值时 +// (随 EcommercePage chunk 加载)缓存已填充,其顶层派生常量拿到的是 API 数据。 +// +// FALLBACK = 完整当前生产数据:API 超时/失败时仍能正常工作(fallback 即正确值)。 +import type { EcommercePlatformSpec } from "../features/ecommerce/utils/platformRules"; +import { serverRequest } from "./serverConnection"; + +export interface MarketLanguageOption { + country: string; + languages: string[]; +} + +export interface PlatformRulesData { + platformSpecOptions: EcommercePlatformSpec[]; + marketLanguageOptions: MarketLanguageOption[]; + languageAliases: Record; + legacyPlatformAliases: Record; + domesticPlatformLabels: string[]; + domesticPlatformLanguages: string[]; + defaultEcommercePlatform: string; +} + +// ── FALLBACK:完整当前数据,逐字迁移自原 platformRules.ts ────────────── +const FALLBACK_PLATFORM_RULES: PlatformRulesData = { + platformSpecOptions: [ + { + 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×1920px,9: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×800px,1: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"], + }, + ], + marketLanguageOptions: [ + { 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: ["波兰语"] }, + ], + languageAliases: { + "英文": "英文", + "中文": "中文", + "英语": "英文", + "日语": "日文", + "日文": "日文", + "德语": "德文", + "德文": "德文", + "法语": "法文", + "法文": "法文", + "韩语": "韩文", + "韩文": "韩文", + "西文": "西班牙语", + "西班牙语": "西班牙语", + "葡文": "葡萄牙语", + "葡萄牙语": "葡萄牙语", + "印尼语": "印度尼西亚语", + "印度尼西亚语": "印度尼西亚语", + "菲律宾语": "菲律宾语(他加禄语)", + "菲律宾语(他加禄语)": "菲律宾语(他加禄语)", + }, + legacyPlatformAliases: { + "淘宝/天猫": "淘宝/天猫", + "京东": "京东", + "拼多多": "拼多多", + "抖音电商": "抖音电商", + "亚马逊Amazon": "亚马逊 Amazon", + "速卖通": "速卖通", + }, + domesticPlatformLabels: ["淘宝/天猫", "京东", "拼多多", "抖音电商"], + domesticPlatformLanguages: ["中文"], + defaultEcommercePlatform: "淘宝/天猫", +}; + +interface PlatformRulesPayload { + name: string; + config: Partial; +} + +let cached: PlatformRulesData | null = null; +let loadPromise: Promise | null = null; + +function isNonEmptyArray(value: unknown): boolean { + return Array.isArray(value) && value.length > 0; +} + +// 合并 API 返回与 fallback:仅当 API 字段有效(非空)时覆盖,避免后端漏配某字段导致 UI 空白。 +function mergeWithFallback(config: Partial): PlatformRulesData { + return { + platformSpecOptions: isNonEmptyArray(config.platformSpecOptions) + ? (config.platformSpecOptions as EcommercePlatformSpec[]) + : FALLBACK_PLATFORM_RULES.platformSpecOptions, + marketLanguageOptions: isNonEmptyArray(config.marketLanguageOptions) + ? (config.marketLanguageOptions as MarketLanguageOption[]) + : FALLBACK_PLATFORM_RULES.marketLanguageOptions, + languageAliases: + config.languageAliases && typeof config.languageAliases === "object" + ? config.languageAliases + : FALLBACK_PLATFORM_RULES.languageAliases, + legacyPlatformAliases: + config.legacyPlatformAliases && typeof config.legacyPlatformAliases === "object" + ? config.legacyPlatformAliases + : FALLBACK_PLATFORM_RULES.legacyPlatformAliases, + domesticPlatformLabels: isNonEmptyArray(config.domesticPlatformLabels) + ? (config.domesticPlatformLabels as string[]) + : FALLBACK_PLATFORM_RULES.domesticPlatformLabels, + domesticPlatformLanguages: isNonEmptyArray(config.domesticPlatformLanguages) + ? (config.domesticPlatformLanguages as string[]) + : FALLBACK_PLATFORM_RULES.domesticPlatformLanguages, + defaultEcommercePlatform: + typeof config.defaultEcommercePlatform === "string" && config.defaultEcommercePlatform.trim() + ? config.defaultEcommercePlatform + : FALLBACK_PLATFORM_RULES.defaultEcommercePlatform, + }; +} + +async function fetchPlatformRules(): Promise { + const payload = await serverRequest( + "public/config/profile?name=web-ecommerce-platform-rules", + { maxRetries: 2, timeoutMs: 8_000, fallbackMessage: "加载电商平台规则失败" }, + ); + return mergeWithFallback(payload?.config ?? {}); +} + +/** 预加载平台规则。App 启动 gating 调用,await 其完成(带超时,失败用 fallback)。可安全重复调用。 */ +export async function preloadPlatformRules(): Promise { + if (loadPromise) return loadPromise.then(() => undefined); + loadPromise = fetchPlatformRules() + .then((data) => { + cached = data; + return data; + }) + .catch((error) => { + console.warn("[platformRules] 加载失败,使用 fallback 数据", error); + cached = null; + loadPromise = null; + return FALLBACK_PLATFORM_RULES; + }); + return loadPromise.then(() => undefined); +} + +/** 同步获取平台规则。未加载时返回 fallback(=当前生产值,永远可用)。 */ +export function getPlatformRules(): PlatformRulesData { + return cached ?? FALLBACK_PLATFORM_RULES; +} diff --git a/src/features/ecommerce/utils/platformRules.ts b/src/features/ecommerce/utils/platformRules.ts index 4e48447..b4a142e 100644 --- a/src/features/ecommerce/utils/platformRules.ts +++ b/src/features/ecommerce/utils/platformRules.ts @@ -1,4 +1,5 @@ import { formatAspectRatio, normalizeRatioToken } from "./ratioUtils"; +import { getPlatformRules } from "../../../api/platformRulesClient"; export type ProductSetOutputKey = "set" | "detail" | "model" | "video"; export type CloneOutputKey = ProductSetOutputKey | "hot"; @@ -18,414 +19,26 @@ export interface EcommercePlatformSpec { 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×1920px,9: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×800px,1: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"], - }, -]; + +// 业务数据由后端 API 下发(AGENTS.md 规则4),见 src/api/platformRulesClient.ts。 +// 启动 gating 保证本模块求值时(随 EcommercePage chunk 加载)缓存已填充。 +// 顶层读取一次:gating 后 getPlatformRules() 返回 API 数据;未就绪则返回 fallback。 +const rules = getPlatformRules(); + +export const platformSpecOptions: EcommercePlatformSpec[] = rules.platformSpecOptions; 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 marketLanguageOptions: Array<{ country: string; languages: string[] }> = + rules.marketLanguageOptions; export const marketOptions = marketLanguageOptions.map((option) => option.country); export const languageOptions = Array.from(new Set(marketLanguageOptions.flatMap((option) => option.languages))); -export const languageAliases: Record = { - "英文": "英文", - "中文": "中文", - "英语": "英文", - "日语": "日文", - "日文": "日文", - "德语": "德文", - "德文": "德文", - "法语": "法文", - "法文": "法文", - "韩语": "韩文", - "韩文": "韩文", - "西文": "西班牙语", - "西班牙语": "西班牙语", - "葡文": "葡萄牙语", - "葡萄牙语": "葡萄牙语", - "印尼语": "印度尼西亚语", - "印度尼西亚语": "印度尼西亚语", - "菲律宾语": "菲律宾语(他加禄语)", - "菲律宾语(他加禄语)": "菲律宾语(他加禄语)", -}; +export const languageAliases: Record = rules.languageAliases; 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 = { - "淘宝/天猫": "淘宝/天猫", - "京东": "京东", - "拼多多": "拼多多", - "抖音电商": "抖音电商", - "亚马逊Amazon": "亚马逊 Amazon", - "速卖通": "速卖通", -}; +export const legacyPlatformAliases: Record = rules.legacyPlatformAliases; export const normalizePlatform = (value: string) => getPlatformSpec(legacyPlatformAliases[value] ?? value).label; -export const domesticPlatformLabels = new Set(["淘宝/天猫", "京东", "拼多多", "抖音电商"]); -export const domesticPlatformLanguages = ["中文"]; +export const domesticPlatformLabels = new Set(rules.domesticPlatformLabels); +export const domesticPlatformLanguages = rules.domesticPlatformLanguages; export const isDomesticPlatform = (platformValue: string) => domesticPlatformLabels.has(normalizePlatform(platformValue)); export const getPlatformRatioGroup = (value: string, mode?: PlatformRatioModeKey): PlatformRatioGroup => { const platformSpec = getPlatformSpec(value); @@ -467,7 +80,7 @@ export const normalizeLanguageForPlatform = (platformValue: string, marketValue: return platformLanguages.includes(normalizedLanguage) ? normalizedLanguage : getPlatformDefaultLanguage(platformValue, marketValue); }; -export const defaultEcommercePlatform = "淘宝/天猫"; +export const defaultEcommercePlatform = rules.defaultEcommercePlatform; export const defaultProductSetOutput: ProductSetOutputKey = "set"; export const defaultCloneOutput: CloneOutputKey = "set"; @@ -477,3 +90,4 @@ export const formatUploadedImageRatio = (image?: { width?: number; height?: numb 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}`; }; +