Codex/ecommerce history sync #29

Merged
stringadmin merged 14 commits from codex/ecommerce-history-sync into main 2026-06-18 02:20:06 +00:00
3 changed files with 524 additions and 415 deletions
Showing only changes of commit a0018353ec - Show all commits
+38 -13
View File
@@ -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<string | null>(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<WorkspaceChromeState>({
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() {
</div>
}
>
<EcommercePage
projects={[]}
isAuthenticated={Boolean(session)}
onWorkspaceChromeChange={setWorkspaceChrome}
onStartCreate={() => undefined}
onOpenProject={() => undefined}
onDeleteProject={() => undefined}
onImportWorkflow={() => undefined}
onCreateTask={() => undefined}
onRequireLogin={() => openAuth("login")}
initialTemplate={null}
onInitialTemplateConsumed={() => undefined}
/>
{platformRulesReady ? (
<EcommercePage
projects={[]}
isAuthenticated={Boolean(session)}
onWorkspaceChromeChange={setWorkspaceChrome}
onStartCreate={() => undefined}
onOpenProject={() => undefined}
onDeleteProject={() => undefined}
onImportWorkflow={() => undefined}
onCreateTask={() => undefined}
onRequireLogin={() => openAuth("login")}
initialTemplate={null}
onInitialTemplateConsumed={() => undefined}
/>
) : (
<div className="page-loading-center">
<div className="page-loading-spinner" />
<span className="page-loading-center__text">...</span>
</div>
)}
</Suspense>
</ErrorBoundary>
</div>
+470
View File
@@ -0,0 +1,470 @@
// 电商平台规格 + 市场语言业务数据客户端(AGENTS.md 规则4合规)。
// 数据由后端 GET /api/public/config/profile?name=web-ecommerce-platform-rules 下发,
// 不硬编码在前端业务逻辑里。
//
// 时序设计(启动 gating):App 启动 boot splash 期间 await preloadPlatformRules()
// 数据就绪后才渲染 EcommercePageReact.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<string, string>;
legacyPlatformAliases: Record<string, string>;
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×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"],
},
],
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<PlatformRulesData>;
}
let cached: PlatformRulesData | null = null;
let loadPromise: Promise<PlatformRulesData> | null = null;
function isNonEmptyArray(value: unknown): boolean {
return Array.isArray(value) && value.length > 0;
}
// 合并 API 返回与 fallback:仅当 API 字段有效(非空)时覆盖,避免后端漏配某字段导致 UI 空白。
function mergeWithFallback(config: Partial<PlatformRulesData>): 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<PlatformRulesData> {
const payload = await serverRequest<PlatformRulesPayload>(
"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<void> {
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;
}
+16 -402
View File
@@ -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×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"],
},
];
// 业务数据由后端 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<string, string> = {
"英文": "英文",
"中文": "中文",
"英语": "英文",
"日语": "日文",
"日文": "日文",
"德语": "德文",
"德文": "德文",
"法语": "法文",
"法文": "法文",
"韩语": "韩文",
"韩文": "韩文",
"西文": "西班牙语",
"西班牙语": "西班牙语",
"葡文": "葡萄牙语",
"葡萄牙语": "葡萄牙语",
"印尼语": "印度尼西亚语",
"印度尼西亚语": "印度尼西亚语",
"菲律宾语": "菲律宾语(他加禄语)",
"菲律宾语(他加禄语)": "菲律宾语(他加禄语)",
};
export const languageAliases: Record<string, string> = 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<string, string> = {
"淘宝/天猫": "淘宝/天猫",
"京东": "京东",
"拼多多": "拼多多",
"抖音电商": "抖音电商",
"亚马逊Amazon": "亚马逊 Amazon",
"速卖通": "速卖通",
};
export const legacyPlatformAliases: Record<string, string> = 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}`;
};