diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index b4c4cf0..2d72959 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -1,9 +1,11 @@ -import { +import { AppstoreOutlined, CloudUploadOutlined, CloseOutlined, FileImageOutlined, + FolderOpenOutlined, FrownOutlined, + GlobalOutlined, LoadingOutlined, MenuFoldOutlined, MenuUnfoldOutlined, @@ -11,8 +13,9 @@ import { ReloadOutlined, SettingOutlined, SkinOutlined, + TableOutlined, } from "@ant-design/icons"; -import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type MouseEvent as ReactMouseEvent, type ReactNode } from "react"; +import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; import "../../styles/pages/ecommerce.css"; import "../../styles/pages/local-theme-parity.css"; import { ossAssets } from "../../data/ossAssets"; @@ -35,6 +38,136 @@ import pinduoduoLogo from "../../assets/platform-logos/pinduoduo.webp"; import shopeeLogo from "../../assets/platform-logos/shopee.webp"; import taobaoLogo from "../../assets/platform-logos/taobao.webp"; import tiktokShopLogo from "../../assets/platform-logos/tiktok-shop.webp"; + +const smartCutoutColorPresets = [ + "#ffffff", + "#111111", + "#ff3131", + "#ff7a1a", + "#f7c600", + "#29b34a", + "#25a9e0", + "#438df5", + "#9029d9", + "#8aa3ad", + "#6b7b86", + "#f46f7b", + "#ff9451", + "#f7d34f", + "#55c66f", + "#73c7f3", + "#6dabf5", + "#b45adb", + "#bcc8ce", + "#aeb7bd", + "#ffbec4", + "#ffd1ac", + "#f8e69d", + "#91de9e", + "#b7e5fb", + "#b9d9fb", + "#d7abe8", + "#dfe5e8", + "#d7dde0", + "#ffe2e4", + "#ffe5d1", + "#f8efcf", + "#c9efcf", + "#d8f0fb", + "#d8eafa", + "#ead2f1", +]; + +const smartCutoutSizeOptions = [ + { key: "original", label: "原尺寸", icon: "image", frameWidth: "min(520px, 78%)", frameAspect: "auto", imageMaxWidth: "78%", imageMaxHeight: "310px" }, + { key: "trim", label: "裁剪到边缘", icon: "crop", frameWidth: "min(420px, 70%)", frameAspect: "auto", imageMaxWidth: "92%", imageMaxHeight: "360px" }, + { key: "taobao-1-1", label: "淘宝1:1主图", icon: "shop", frameWidth: "min(430px, 72%)", frameAspect: "1 / 1", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "taobao-3-4", label: "淘宝3:4主图", icon: "shop", frameWidth: "min(330px, 56%)", frameAspect: "3 / 4", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "pdd-main", label: "拼多多主图", icon: "pdd", frameWidth: "min(430px, 72%)", frameAspect: "1 / 1", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "xiaohongshu-cover", label: "小红书封面", icon: "text", frameWidth: "min(330px, 56%)", frameAspect: "3 / 4", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "one-inch", label: "一寸头像", icon: "portrait", frameWidth: "min(290px, 50%)", frameAspect: "25 / 35", imageMaxWidth: "86%", imageMaxHeight: "86%" }, + { key: "two-inch", label: "二寸头像", icon: "portrait", frameWidth: "min(320px, 54%)", frameAspect: "35 / 49", imageMaxWidth: "86%", imageMaxHeight: "86%" }, + { key: "ratio-1-1", label: "1:1", icon: "square", frameWidth: "min(430px, 72%)", frameAspect: "1 / 1", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "ratio-3-2", label: "3:2", icon: "landscape", frameWidth: "min(520px, 78%)", frameAspect: "3 / 2", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "ratio-2-3", label: "2:3", icon: "portrait-ratio", frameWidth: "min(330px, 56%)", frameAspect: "2 / 3", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "ratio-4-3", label: "4:3", icon: "landscape", frameWidth: "min(520px, 78%)", frameAspect: "4 / 3", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "ratio-3-4", label: "3:4", icon: "portrait-ratio", frameWidth: "min(330px, 56%)", frameAspect: "3 / 4", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "ratio-16-9", label: "16:9", icon: "wide", frameWidth: "min(560px, 82%)", frameAspect: "16 / 9", imageMaxWidth: "82%", imageMaxHeight: "82%" }, + { key: "ratio-9-16", label: "9:16", icon: "phone", frameWidth: "min(260px, 46%)", frameAspect: "9 / 16", imageMaxWidth: "82%", imageMaxHeight: "82%" }, +] as const; + +type SmartCutoutSizeKey = (typeof smartCutoutSizeOptions)[number]["key"]; + +const clampNumber = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + +const normalizeHexColor = (value: string) => { + const clean = value.trim().replace(/^#/, ""); + if (!/^[0-9a-fA-F]{6}$/.test(clean)) return null; + return `#${clean.toLowerCase()}`; +}; + +const hexToRgb = (value: string) => { + const normalized = normalizeHexColor(value); + if (!normalized) return null; + const numeric = Number.parseInt(normalized.slice(1), 16); + return { + r: (numeric >> 16) & 255, + g: (numeric >> 8) & 255, + b: numeric & 255, + }; +}; + +const rgbToHex = (r: number, g: number, b: number) => + `#${[r, g, b].map((item) => clampNumber(Math.round(item), 0, 255).toString(16).padStart(2, "0")).join("")}`; + +const hsvToRgb = (h: number, s: number, v: number) => { + const hue = ((h % 360) + 360) % 360; + const saturation = clampNumber(s, 0, 100) / 100; + const value = clampNumber(v, 0, 100) / 100; + const chroma = value * saturation; + const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1)); + const match = value - chroma; + const [red, green, blue] = + hue < 60 + ? [chroma, x, 0] + : hue < 120 + ? [x, chroma, 0] + : hue < 180 + ? [0, chroma, x] + : hue < 240 + ? [0, x, chroma] + : hue < 300 + ? [x, 0, chroma] + : [chroma, 0, x]; + return { + r: (red + match) * 255, + g: (green + match) * 255, + b: (blue + match) * 255, + }; +}; + +const hexToHsv = (value: string) => { + const rgb = hexToRgb(value) ?? { r: 255, g: 255, b: 255 }; + const red = rgb.r / 255; + const green = rgb.g / 255; + const blue = rgb.b / 255; + const max = Math.max(red, green, blue); + const min = Math.min(red, green, blue); + const delta = max - min; + const hue = + delta === 0 + ? 0 + : max === red + ? 60 * (((green - blue) / delta) % 6) + : max === green + ? 60 * ((blue - red) / delta + 2) + : 60 * ((red - green) / delta + 4); + return { + h: Math.round((hue + 360) % 360), + s: max === 0 ? 0 : Math.round((delta / max) * 100), + v: Math.round(max * 100), + }; +}; import tmallLogo from "../../assets/platform-logos/tmall.webp"; import { aiGenerationClient } from "../../api/aiGenerationClient"; import { ServerRequestError } from "../../api/serverConnection"; @@ -158,7 +291,7 @@ interface PlatformRatioGroup { const sideTools: Array<{ key: ProductKitToolKey; label: string; icon: ReactNode }> = [ { key: "set", label: "商品套图", icon: }, { key: "detail", label: "A+详情", icon: }, - { key: "wear", label: "服饰穿戴", icon: }, + { key: "wear", label: "服饰穿搭", icon: }, { key: "clone", label: "电商AI作图", icon: }, ]; @@ -173,324 +306,324 @@ const platformSpecOptions: Array<{ }> = [ { label: "淘宝/天猫", - ratios: ["淘宝主图 / SKU 图 800×800px", "详情页宽 750px", "详情页宽 790px"], - defaultRatio: "淘宝主图 / SKU 图 800×800px", + 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", + ratios: ["1000脳1000px\u00a0\u00a0\u00a01锛?", "800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1000脳1000px\u00a0\u00a0\u00a01锛?", }, 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", + "750脳1000px\u00a0\u00a0\u00a03锛?", + "790脳1053px\u00a0\u00a0\u00a03锛?", + "750脳1125px\u00a0\u00a0\u00a02锛?", + "790脳1185px\u00a0\u00a0\u00a02锛?", ], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, model: { - ratios: ["750×1000px\u00a0\u00a0\u00a03:4"], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + ratios: ["750脳1000px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, 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", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6", "1080脳1440px\u00a0\u00a0\u00a03锛?", "1080脳1080px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["主图 / SKU 图 800×800px,≤3MB", "详情页宽 750px 或 790px,单张高≤1546px"], - tip: "建议主图 200-400KB JPG,超过 500KB 会影响加载速度。", + specs: ["涓诲浘 / SKU 鍥?800脳800px锛屸墹3MB", "璇︽儏椤靛 750px 鎴?790px锛屽崟寮犻珮鈮?546px"], + tip: "寤鸿涓诲浘 200-400KB JPG锛岃秴杩?500KB 浼氬奖鍝嶅姞杞介€熷害銆?", }, { label: "京东", - ratios: ["京东主图 / SKU 图 800×800px", "详情页宽 750px", "首图主体占比 ≥70%"], - defaultRatio: "京东主图 / SKU 图 800×800px", + ratios: ["浜笢涓诲浘 / SKU 鍥?800脳800px", "璇︽儏椤靛 750px", "棣栧浘涓讳綋鍗犳瘮 鈮?0%"], + defaultRatio: "浜笢涓诲浘 / SKU 鍥?800脳800px", ratioGroups: { set: { - ratios: ["1000×1000px\u00a0\u00a0\u00a01:1"], - defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1", + ratios: ["1000脳1000px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1000脳1000px\u00a0\u00a0\u00a01锛?", }, 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", + "750脳1000px\u00a0\u00a0\u00a03锛?", + "990脳1320px\u00a0\u00a0\u00a03锛?", + "750脳1125px\u00a0\u00a0\u00a02锛?", + "990脳1485px\u00a0\u00a0\u00a02锛?", ], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, model: { - ratios: ["750×1125px\u00a0\u00a0\u00a02:3", "990×1485px\u00a0\u00a0\u00a02:3"], - defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3", + ratios: ["750脳1125px\u00a0\u00a0\u00a02锛?", "990脳1485px\u00a0\u00a0\u00a02锛?"], + defaultRatio: "750脳1125px\u00a0\u00a0\u00a02锛?", }, video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1920×1080px\u00a0\u00a0\u00a016:9"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6", "1920脳1080px\u00a0\u00a0\u00a016锛?"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["主图 / SKU 图 800×800px,白底,≤1MB", "详情页宽 750px,首图主体占比 ≥70%"], + specs: ["涓诲浘 / SKU 鍥?800脳800px锛岀櫧搴曪紝鈮?MB", "璇︽儏椤靛 750px锛岄鍥句富浣撳崰姣?鈮?0%"], }, { label: "拼多多", - ratios: ["主图 750×352px", "主图 800×800px", "详情页宽 750px"], - defaultRatio: "主图 750×352px", + 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", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?", "750脳1000px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, detail: { - ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + ratios: ["750脳1000px\u00a0\u00a0\u00a03锛?", "750脳1125px\u00a0\u00a0\u00a02锛?"], + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, model: { - ratios: ["750×1000px\u00a0\u00a0\u00a03:4"], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + ratios: ["750脳1000px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["主图 750×352px 或 800×800px,≤1MB", "详情页宽 750px,要求纯白底、无水印、无拼接"], + specs: ["涓诲浘 750脳352px 鎴?800脳800px锛屸墹1MB", "璇︽儏椤靛 750px锛岃姹傜函鐧藉簳銆佹棤姘村嵃銆佹棤鎷兼帴"], }, { label: "抖音电商", - ratios: ["短视频 1080×1920px"], - defaultRatio: "短视频 1080×1920px", + ratios: ["鐭棰?1080脳1920px"], + defaultRatio: "鐭棰?1080脳1920px", ratioGroups: { video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["短视频 1080×1920px,9:16", "30s 内最佳"], + specs: ["鐭棰?1080脳1920px锛?:16", "30s 鍐呮渶浣?"], }, { label: "亚马逊 Amazon", - ratios: ["主图 ≥1600×1600px", "建议 2000×2000px+", "最小 500×500px"], - defaultRatio: "主图 ≥1600×1600px", + ratios: ["涓诲浘 鈮?600脳1600px", "寤鸿 2000脳2000px+", "鏈€灏?500脳500px"], + defaultRatio: "涓诲浘 鈮?600脳1600px", ratioGroups: { set: { - ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"], - defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1", + ratios: ["1600脳1600px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1600脳1600px\u00a0\u00a0\u00a01锛?", }, 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", + ratios: ["1600脳1600px\u00a0\u00a0\u00a01锛?", "1200脳1800px\u00a0\u00a0\u00a02锛?", "1200脳1600px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "1200脳1800px\u00a0\u00a0\u00a02锛?", }, model: { - ratios: ["1200×1800px\u00a0\u00a0\u00a02:3"], - defaultRatio: "1200×1800px\u00a0\u00a0\u00a02:3", + ratios: ["1200脳1800px\u00a0\u00a0\u00a02锛?"], + defaultRatio: "1200脳1800px\u00a0\u00a0\u00a02锛?", }, video: { - ratios: ["1920×1080px\u00a0\u00a0\u00a016:9"], - defaultRatio: "1920×1080px\u00a0\u00a0\u00a016:9", + ratios: ["1920脳1080px\u00a0\u00a0\u00a016锛?"], + defaultRatio: "1920脳1080px\u00a0\u00a0\u00a016锛?", }, hot: { - ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"], - defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1", + ratios: ["1600脳1600px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1600脳1600px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["主图 1600×1600px+,纯白底,≤10MB", "最小 500×500px,建议 2000px+ 以支持缩放"], - aliases: ["亚马逊"], + specs: ["涓诲浘 1600脳1600px+锛岀函鐧藉簳锛屸墹10MB", "鏈€灏?500脳500px锛屽缓璁?2000px+ 浠ユ敮鎸佺缉鏀?"], + aliases: ["浜氶┈閫?"], }, { label: "Shopee", - ratios: ["商品主图 1024×1024px", "基础主图 800×800px"], - defaultRatio: "商品主图 1024×1024px", + ratios: ["鍟嗗搧涓诲浘 1024脳1024px", "鍩虹涓诲浘 800脳800px"], + defaultRatio: "鍟嗗搧涓诲浘 1024脳1024px", ratioGroups: { set: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, detail: { - ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + ratios: ["750脳1000px\u00a0\u00a0\u00a03锛?", "750脳1125px\u00a0\u00a0\u00a02锛?"], + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, model: { - ratios: ["750×1000px\u00a0\u00a0\u00a03:4"], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + ratios: ["750脳1000px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["商品主图推荐 1024×1024px,基础 800×800px", "≤2MB,白底或浅色底"], - aliases: ["虾皮 Shopee/Lazada", "虾皮"], + specs: ["鍟嗗搧涓诲浘鎺ㄨ崘 1024脳1024px锛屽熀纭€ 800脳800px", "鈮?MB锛岀櫧搴曟垨娴呰壊搴?"], + aliases: ["铏剧毊 Shopee/Lazada", "铏剧毊"], }, { label: "Lazada", - ratios: ["商品主图 800×800px"], - defaultRatio: "商品主图 800×800px", + ratios: ["鍟嗗搧涓诲浘 800脳800px"], + defaultRatio: "鍟嗗搧涓诲浘 800脳800px", ratioGroups: { set: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, detail: { - ratios: ["750×1000px\u00a0\u00a0\u00a03:4", "750×1125px\u00a0\u00a0\u00a02:3"], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + ratios: ["750脳1000px\u00a0\u00a0\u00a03锛?", "750脳1125px\u00a0\u00a0\u00a02锛?"], + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, model: { - ratios: ["750×1000px\u00a0\u00a0\u00a03:4"], - defaultRatio: "750×1000px\u00a0\u00a0\u00a03:4", + ratios: ["750脳1000px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "750脳1000px\u00a0\u00a0\u00a03锛?", }, video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["商品主图 800×800px,1:1"], + specs: ["鍟嗗搧涓诲浘 800脳800px锛?:1"], }, { label: "Instagram", - ratios: ["帖子 1080×1350px", "帖子 1080×1080px", "Stories / Reels 1080×1920px", "头像 320×320px"], - defaultRatio: "帖子 1080×1350px", + 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", + ratios: ["1080脳1080px\u00a0\u00a0\u00a01锛?", "1080脳1350px\u00a0\u00a0\u00a04锛?"], + defaultRatio: "1080脳1080px\u00a0\u00a0\u00a01锛?", }, detail: { - ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"], - defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5", + ratios: ["1080脳1350px\u00a0\u00a0\u00a04锛?"], + defaultRatio: "1080脳1350px\u00a0\u00a0\u00a04锛?", }, model: { - ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"], - defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5", + ratios: ["1080脳1350px\u00a0\u00a0\u00a04锛?"], + defaultRatio: "1080脳1350px\u00a0\u00a0\u00a04锛?", }, video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1080×1350px\u00a0\u00a0\u00a04:5"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6", "1080脳1350px\u00a0\u00a0\u00a04锛?"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, }, - specs: ["帖子 1080×1350px 或 1080×1080px", "Stories / Reels 封面 1080×1920px,头像 320×320px"], - tip: "建议 ≤1MB JPG。", + specs: ["甯栧瓙 1080脳1350px 鎴?1080脳1080px", "Stories / Reels 灏侀潰 1080脳1920px锛屽ご鍍?320脳320px"], + tip: "寤鸿 鈮?MB JPG銆?", aliases: ["Instagram Reels"], }, { label: "速卖通", - ratios: ["主图 800×800px", "主图 1000×1000px+"], - defaultRatio: "主图 800×800px", + ratios: ["涓诲浘 800脳800px", "涓诲浘 1000脳1000px+"], + defaultRatio: "涓诲浘 800脳800px", ratioGroups: { set: { - ratios: ["1000×1000px\u00a0\u00a0\u00a01:1"], - defaultRatio: "1000×1000px\u00a0\u00a0\u00a01:1", + ratios: ["1000脳1000px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1000脳1000px\u00a0\u00a0\u00a01锛?", }, detail: { - ratios: ["750×1125px\u00a0\u00a0\u00a02:3", "750×1000px\u00a0\u00a0\u00a03:4"], - defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3", + ratios: ["750脳1125px\u00a0\u00a0\u00a02锛?", "750脳1000px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "750脳1125px\u00a0\u00a0\u00a02锛?", }, model: { - ratios: ["750×1125px\u00a0\u00a0\u00a02:3"], - defaultRatio: "750×1125px\u00a0\u00a0\u00a02:3", + ratios: ["750脳1125px\u00a0\u00a0\u00a02锛?"], + defaultRatio: "750脳1125px\u00a0\u00a0\u00a02锛?", }, video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16", "1920×1080px\u00a0\u00a0\u00a016:9"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6", "1920脳1080px\u00a0\u00a0\u00a016锛?"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["主图建议 800×800px 或更高,1:1", "适合跨境电商主图、SKU 图和场景图"], + specs: ["涓诲浘寤鸿 800脳800px 鎴栨洿楂橈紝1:1", "閫傚悎璺ㄥ鐢靛晢涓诲浘銆丼KU 鍥惧拰鍦烘櫙鍥?"], }, { label: "eBay", - ratios: ["商品图 1:1", "白底多角度展示图 1:1"], - defaultRatio: "商品图 1:1", + ratios: ["鍟嗗搧鍥?1:1", "鐧藉簳澶氳搴﹀睍绀哄浘 1:1"], + defaultRatio: "鍟嗗搧鍥?1:1", ratioGroups: { set: { - ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"], - defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1", + ratios: ["1600脳1600px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1600脳1600px\u00a0\u00a0\u00a01锛?", }, detail: { - ratios: ["1000×1500px\u00a0\u00a0\u00a02:3", "1200×1600px\u00a0\u00a0\u00a03:4"], - defaultRatio: "1000×1500px\u00a0\u00a0\u00a02:3", + ratios: ["1000脳1500px\u00a0\u00a0\u00a02锛?", "1200脳1600px\u00a0\u00a0\u00a03锛?"], + defaultRatio: "1000脳1500px\u00a0\u00a0\u00a02锛?", }, model: { - ratios: ["1000×1500px\u00a0\u00a0\u00a02:3"], - defaultRatio: "1000×1500px\u00a0\u00a0\u00a02:3", + ratios: ["1000脳1500px\u00a0\u00a0\u00a02锛?"], + defaultRatio: "1000脳1500px\u00a0\u00a0\u00a02锛?", }, video: { - ratios: ["1920×1080px\u00a0\u00a0\u00a016:9", "1080×1920px\u00a0\u00a0\u00a09:16"], - defaultRatio: "1920×1080px\u00a0\u00a0\u00a016:9", + ratios: ["1920脳1080px\u00a0\u00a0\u00a016锛?", "1080脳1920px\u00a0\u00a0\u00a09锛?6"], + defaultRatio: "1920脳1080px\u00a0\u00a0\u00a016锛?", }, hot: { - ratios: ["1600×1600px\u00a0\u00a0\u00a01:1"], - defaultRatio: "1600×1600px\u00a0\u00a0\u00a01:1", + ratios: ["1600脳1600px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1600脳1600px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["商品图建议 1:1,主体清晰居中", "适合白底主图和多角度展示图"], + specs: ["鍟嗗搧鍥惧缓璁?1:1锛屼富浣撴竻鏅板眳涓?", "閫傚悎鐧藉簳涓诲浘鍜屽瑙掑害灞曠ず鍥?"], }, { label: "TikTok Shop", - ratios: ["商品主图 1:1", "短视频 / 竖版封面 9:16"], - defaultRatio: "商品主图 1:1", + ratios: ["鍟嗗搧涓诲浘 1:1", "鐭棰?/ 绔栫増灏侀潰 9:16"], + defaultRatio: "鍟嗗搧涓诲浘 1:1", ratioGroups: { set: { - ratios: ["1280×1280px\u00a0\u00a0\u00a01:1"], - defaultRatio: "1280×1280px\u00a0\u00a0\u00a01:1", + ratios: ["1280脳1280px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "1280脳1280px\u00a0\u00a0\u00a01锛?", }, detail: { - ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"], - defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5", + ratios: ["1080脳1350px\u00a0\u00a0\u00a04锛?"], + defaultRatio: "1080脳1350px\u00a0\u00a0\u00a04锛?", }, model: { - ratios: ["1080×1350px\u00a0\u00a0\u00a04:5"], - defaultRatio: "1080×1350px\u00a0\u00a0\u00a04:5", + ratios: ["1080脳1350px\u00a0\u00a0\u00a04锛?"], + defaultRatio: "1080脳1350px\u00a0\u00a0\u00a04锛?", }, video: { - ratios: ["1080×1920px\u00a0\u00a0\u00a09:16"], - defaultRatio: "1080×1920px\u00a0\u00a0\u00a09:16", + ratios: ["1080脳1920px\u00a0\u00a0\u00a09锛?6"], + defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, hot: { - ratios: ["800×800px\u00a0\u00a0\u00a01:1"], - defaultRatio: "800×800px\u00a0\u00a0\u00a01:1", + ratios: ["800脳800px\u00a0\u00a0\u00a01锛?"], + defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["商品主图建议 1:1", "短视频/竖版封面建议 9:16"], + specs: ["鍟嗗搧涓诲浘寤鸿 1:1", "鐭棰?绔栫増灏侀潰寤鸿 9:16"], }, ]; const platformOptions = platformSpecOptions.map((option) => option.label); const getPlatformLogoSources = (value: string) => { const normalized = value.toLowerCase(); - if (value.includes("淘宝") || value.includes("天猫")) return [taobaoLogo, tmallLogo]; - if (value.includes("京东")) return [jdLogo]; - if (value.includes("拼多多")) return [pinduoduoLogo]; - if (value.includes("抖音")) return [douyinLogo]; + if (value.includes("淘宝") || value.includes("天猫") || value.includes("娣樺疂") || value.includes("澶╃尗")) return [taobaoLogo, tmallLogo]; + if (value.includes("京东") || value.includes("浜笢")) return [jdLogo]; + if (value.includes("拼多多") || value.includes("鎷煎澶")) return [pinduoduoLogo]; + if (value.includes("抖音") || value.includes("鎶栭煶")) return [douyinLogo]; if (normalized.includes("amazon")) return [amazonLogo]; if (normalized.includes("shopee")) return [shopeeLogo]; if (normalized.includes("lazada")) return [lazadaLogo]; if (normalized.includes("instagram")) return [instagramLogo]; - if (value.includes("速卖通")) return [aliexpressLogo]; + if (value.includes("速卖通") || value.includes("閫熷崠閫")) return [aliexpressLogo]; if (normalized.includes("ebay")) return [ebayLogo]; if (normalized.includes("tiktok")) return [tiktokShopLogo]; return []; @@ -535,20 +668,38 @@ const marketLanguageOptions: Array<{ country: string; languages: string[] }> = [ const marketOptions = marketLanguageOptions.map((option) => option.country); const languageOptions = Array.from(new Set(marketLanguageOptions.flatMap((option) => option.languages))); const languageAliases: Record = { - 英语: "英文", - 日语: "日文", - 德语: "德文", - 法语: "法文", - 韩语: "韩文", - 西文: "西班牙语", - 葡文: "葡萄牙语", - 印尼语: "印度尼西亚语", - 菲律宾语: "菲律宾语(他加禄语)", + "鑻辨枃": "英文", + "涓枃": "中文", + "鑻辫": "英文", + "鏃ヨ": "日文", + "鏃ユ枃": "日文", + "寰疯": "德文", + "寰锋枃": "德文", + "娉曡": "法文", + "娉曟枃": "法文", + "闊╄": "韩文", + "闊╂枃": "韩文", + "瑗挎枃": "西班牙语", + "瑗跨彮鐗欒": "西班牙语", + "钁℃枃": "葡萄牙语", + "钁¤悇鐗欒": "葡萄牙语", + "鍗板凹璇?": "印度尼西亚语", + "鍗板害灏艰タ浜氳": "印度尼西亚语", + "鑿插緥瀹捐": "菲律宾语(他加禄语)", + "鑿插緥瀹捐锛堜粬鍔犵璇級": "菲律宾语(他加禄语)", }; const defaultPlatformSpec = platformSpecOptions[0]!; const getPlatformSpec = (value: string) => platformSpecOptions.find((option) => option.label === value || option.aliases?.includes(value)) ?? defaultPlatformSpec; -const normalizePlatform = (value: string) => getPlatformSpec(value).label; +const legacyPlatformAliases: Record = { + "娣樺疂/澶╃尗": "淘宝/天猫", + "浜笢": "京东", + "鎷煎澶?": "拼多多", + "鎶栭煶鐢靛晢": "抖音电商", + "浜氶┈閫?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)); @@ -562,7 +713,7 @@ const getPlatformRatioGroup = (value: string, mode?: PlatformRatioModeKey): Plat 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 normalizeRatioToken = (value: string) => value.replace(/:/g, ":").trim(); +const normalizeRatioToken = (value: string) => value.replaceAll("锛?", ":").replaceAll(":", ":").replaceAll("脳", "×").trim(); const normalizeRatioForPlatform = (platformValue: string, ratioValue: string, mode?: PlatformRatioModeKey) => { const platformRatios = getPlatformRatioOptions(platformValue, mode); if (platformRatios.includes(ratioValue)) return ratioValue; @@ -571,20 +722,31 @@ const normalizeRatioForPlatform = (platformValue: string, ratioValue: string, mo return matchedRatio ?? getPlatformDefaultRatio(platformValue, mode); }; const formatRatioDisplayValue = (value: string) => { - if (!value.includes("套图")) return value; - const size = value.match(/\d+\s*×\s*\d+\s*px?/u)?.[0]?.replace(/\s+/g, "") ?? ""; - const ratio = value.match(/\d+(?:\.\d+)?\s*[::]\s*\d+(?:\.\d+)?/u)?.[0]?.replace(/\s+/g, "").replace(/:/g, ":") ?? ""; - return size && ratio ? `${size}\u00a0\u00a0\u00a0${ratio}` : value.replace(/^套图[::]\s*/, ""); + const normalizedValue = normalizeRatioToken(value); + const sizeMatch = normalizedValue.match(/(\d+)\s*[×xX]\s*(\d+)\s*px?/u); + if (sizeMatch) { + const width = Number(sizeMatch[1]); + const height = Number(sizeMatch[2]); + return `${width}×${height}px\u00a0\u00a0\u00a0${formatAspectRatio(width, height)}`; + } + return normalizedValue + .replace("娣樺疂涓诲浘 / SKU 鍥?", "淘宝主图 / SKU 图 ") + .replace("浜笢涓诲浘 / SKU 鍥?", "京东主图 / SKU 图 ") + .replace("璇︽儏椤靛", "详情页宽") + .replace("鐭棰?", "短视频") + .replace("涓诲浘", "主图") + .replace("鍟嗗搧涓诲浘", "商品主图") + .replace("鍟嗗搧鍥?", "商品图"); }; /** Extract CSS aspect-ratio from a ratio string like "1000x1000px 1:1" -> "1 / 1" */ const parseRatioToAspectCss = (ratioStr: string): string => { - const match = ratioStr.match(/(\d+)\s*[::]\s*(\d+)/u); + const match = ratioStr.match(/(\d+)\D+(\d+)/u); if (!match) return "1 / 1"; return `${match[1]} / ${match[2]}`; }; -/** Normalize ratio display string ("1000×1000px 1:1") to API format ("1:1") */ +/** Normalize ratio display string ("1000脳1000px 1锛?") to API format ("1:1") */ const normalizeRatioForApi = (ratioStr: string): string => { - const match = ratioStr.match(/(\d+)\s*[::]\s*(\d+)/u); + const match = ratioStr.match(/(\d+)\D+(\d+)/u); if (!match) return "1:1"; return `${match[1]}:${match[2]}`; }; @@ -643,9 +805,9 @@ const cloneSetCountOptions: Array<{ title: string; desc: string; }> = [ - { key: "selling", title: "卖点图", desc: "展示商品的核心卖点及细节特写" }, + { key: "selling", title: "卖点图", desc: "展示商品核心卖点和细节特写" }, { key: "white", title: "白底图", desc: "白底主图,多角度呈现商品细节" }, - { key: "scene", title: "场景图", desc: "展示商品的生活使用场景和人物搭配" }, + { key: "scene", title: "场景图", desc: "展示商品生活使用场景和人物搭配" }, ]; const cloneSetCountKeys = cloneSetCountOptions.map((option) => option.key); const defaultCloneSetCounts: Record = { @@ -671,7 +833,7 @@ const cloneVideoQualityOptions: Array<{ key: CloneVideoQualityKey; label: string ]; const cloneReplicateLevelOptions: Array<{ key: CloneReplicateLevelKey; title: string; desc: string }> = [ { key: "style", title: "参考风格", desc: "参考整体风格和结构,自动调整色彩和重构场景。" }, - { key: "high", title: "高度复刻", desc: "参照参考图视觉结构替换产品和文案,场景细节略有差异。" }, + { key: "high", title: "高度复刻", desc: "参考视觉结构替换产品和文案,保留主要场景细节。" }, ]; const tryOnRatioOptions = ["3:4", "1:1", "9:16"]; const tryOnScenes = ["纯色棚拍", "都市街头", "街角咖啡", "自然草坪", "度假海滩", "温馨居家", "艺术展馆"]; @@ -693,29 +855,29 @@ const sampleResults = [ ]; const productSetAssets = ossAssets.ecommerce.productSet; const productSetPreviewCards = [ - { id: "main", label: "01 主图 (白底/合规)", src: productSetAssets.main }, - { id: "scene", label: "02 场景展示", src: productSetAssets.scene }, - { id: "model", label: "03 模特场景图", src: productSetAssets.model }, - { id: "detail", label: "04 细节说明", src: productSetAssets.detail }, - { id: "selling", label: "05 卖点详解", src: productSetAssets.selling }, + { id: "main", label: "01 涓诲浘 (鐧藉簳/鍚堣)", src: productSetAssets.main }, + { id: "scene", label: "02 鍦烘櫙灞曠ず", src: productSetAssets.scene }, + { id: "model", label: "03 妯$壒鍦烘櫙鍥?", src: productSetAssets.model }, + { id: "detail", label: "04 缁嗚妭璇存槑", src: productSetAssets.detail }, + { id: "selling", label: "05 鍗栫偣璇﹁В", src: productSetAssets.selling }, ]; const tryOnAssets = ossAssets.ecommerce.tryOn; const tryOnCards = [ { - title: "多件混搭自动融合", + title: "澶氫欢娣锋惌鑷姩铻嶅悎", tone: "red", inputs: [tryOnAssets.dressA, tryOnAssets.dressB, tryOnAssets.modelWoman], results: [tryOnAssets.tryA, tryOnAssets.tryB], }, { - title: "一件也能出大片", + title: "涓€浠朵篃鑳藉嚭澶х墖", tone: "brown", inputs: [tryOnAssets.jacket, tryOnAssets.modelMan], results: [tryOnAssets.jacketResultA, tryOnAssets.jacketResultB], }, { - title: "鞋帽饰品完美适配", + title: "闉嬪附楗板搧瀹岀編閫傞厤", tone: "gold", inputs: [tryOnAssets.hat, tryOnAssets.modelAsian], results: [tryOnAssets.hatResultA, tryOnAssets.hatResultB], @@ -739,7 +901,7 @@ const detailModules = [ { id: "series", title: "系列展示图", desc: "多色或多SKU展示" }, { id: "ingredient", title: "商品成分图", desc: "展示配方/材质/成分" }, { id: "service", title: "售后保障图", desc: "说明质保退换政策" }, - { id: "tips", title: "使用建议图", desc: "商品使用的注意事项" }, + { id: "tips", title: "使用建议图", desc: "商品使用注意事项" }, ]; const defaultDetailModuleIds: string[] = []; const defaultCloneDetailModuleIds = ["hero", "selling", "usage", "angle", "scene", "detail"]; @@ -767,7 +929,7 @@ const blobToDataUrl = (blob: Blob): Promise => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); - reader.onerror = () => reject(reader.error || new Error("文件读取失败")); + reader.onerror = () => reject(reader.error || new Error("鏂囦欢璇诲彇澶辫触")); reader.readAsDataURL(blob); }); @@ -962,6 +1124,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const setInputRef = useRef(null); const productInputRef = useRef(null); const cloneReferenceInputRef = useRef(null); + const smartCutoutInputRef = useRef(null); + const smartCutoutTransitionTimeoutRef = useRef(null); + const smartCutoutPendingUrlsRef = useRef([]); + const smartCutoutPaletteRef = useRef(null); + const smartCutoutToolsRef = useRef(null); + const composerMenuCloseTimeoutRef = useRef(null); const requirementTextareaRef = useRef(null); const commandComposerWrapRef = useRef(null); const garmentInputRef = useRef(null); @@ -996,12 +1164,28 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<{ src: string; label: string } | null>(null); const [showHostingModal, setShowHostingModal] = useState(false); const [productImages, setProductImages] = useState([]); + const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | null>(null); + const [smartCutoutImage, setSmartCutoutImage] = useState<{ src: string; name: string } | null>(null); + const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<{ src: string; name: string }[]>([]); + const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff"); + const [smartCutoutBackgroundAlpha, setSmartCutoutBackgroundAlpha] = useState(100); + const [smartCutoutHexDraft, setSmartCutoutHexDraft] = useState("#ffffff"); + const [isSmartCutoutPaletteOpen, setIsSmartCutoutPaletteOpen] = useState(false); + const [smartCutoutSizeKey, setSmartCutoutSizeKey] = useState("original"); + const [isSmartCutoutDragging, setIsSmartCutoutDragging] = useState(false); + const [isSmartCutoutTransitioning, setIsSmartCutoutTransitioning] = useState(false); + const [smartCutoutTransitionMessage, setSmartCutoutTransitionMessage] = useState({ + title: "正在切换页面", + subtitle: "请稍候", + }); const [isProductUploadDragging, setIsProductUploadDragging] = useState(false); const [cloneOutput, setCloneOutput] = useState(defaultCloneOutput); const [videoHistoryVisible, setVideoHistoryVisible] = useState(false); const [videoPlanTrigger, setVideoPlanTrigger] = useState(0); const [openCloneBasicSelect, setOpenCloneBasicSelect] = useState(null); const [composerMenu, setComposerMenu] = useState(null); + const [visibleComposerMenu, setVisibleComposerMenu] = useState(null); + const [isComposerMenuClosing, setIsComposerMenuClosing] = useState(false); const [composerPopoverLeft, setComposerPopoverLeft] = useState(0); const [isCommandHistoryCollapsed, setIsCommandHistoryCollapsed] = useState(false); const [openCloneModelSelect, setOpenCloneModelSelect] = useState(null); @@ -1192,7 +1376,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const [requirement, setRequirement] = useState(""); const [requirementImageMentionQuery, setRequirementImageMentionQuery] = useState(null); - const [cloneSettingName, setCloneSettingName] = useState("新建创作"); + const [cloneSettingName, setCloneSettingName] = useState("鏂板缓鍒涗綔"); const [platform, setPlatform] = useState(defaultEcommercePlatform); const [market, setMarket] = useState(marketOptions[0]); const [language, setLanguage] = useState(getPlatformDefaultLanguage(defaultEcommercePlatform, marketOptions[0])); @@ -1293,8 +1477,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!; const cloneRequirementPlaceholder = cloneOutput === "model" - ? "建议包含以下信息:产品名称、核心卖点、期望场景、模特外貌描写(如小麦色皮肤、齐刘海、眼角有泪痣)、具体参数" - : "建议包含以下信息,产品名称,核心卖点,期望场景,具体参数"; + ? "建议包含以下信息:产品名称、核心卖点、期望场景、模特外貌描述(如小麦色皮肤、齐刘海、眼角有泪痣)、具体参数" + : "建议包含以下信息:产品名称、核心卖点、期望场景、具体参数"; const productSetPreviewReady = productSetStatus === "done"; const cloneSetTotal = useMemo( () => Object.values(cloneSetCounts).reduce((sum, value) => sum + value, 0), @@ -1367,7 +1551,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); setProductSetStatus("ready"); } catch (err) { - toast.error(err instanceof Error ? err.message : "商品图上传失败"); + toast.error(err instanceof Error ? err.message : "鍟嗗搧鍥句笂浼犲け璐?"); } }; @@ -1385,6 +1569,219 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { if (files.length) void addSetImages(files); }; + const revokeSmartCutoutItems = (items: { src: string }[]) => { + items.forEach((item) => URL.revokeObjectURL(item.src)); + }; + + const clearSmartCutoutTransition = () => { + if (smartCutoutTransitionTimeoutRef.current !== null) { + window.clearTimeout(smartCutoutTransitionTimeoutRef.current); + smartCutoutTransitionTimeoutRef.current = null; + } + if (smartCutoutPendingUrlsRef.current.length) { + smartCutoutPendingUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)); + smartCutoutPendingUrlsRef.current = []; + } + setIsSmartCutoutTransitioning(false); + }; + + const runSmartCutoutPageTransition = (message: { title: string; subtitle: string }, action: () => void, delay = 460) => { + clearSmartCutoutTransition(); + setSmartCutoutTransitionMessage(message); + setIsSmartCutoutTransitioning(true); + smartCutoutTransitionTimeoutRef.current = window.setTimeout(() => { + smartCutoutTransitionTimeoutRef.current = null; + action(); + setIsSmartCutoutTransitioning(false); + }, delay); + }; + + const openSmartCutoutUpload = () => { + clearSmartCutoutTransition(); + setSmartCutoutTransitionMessage({ + title: "正在进入智能抠图", + subtitle: "为你打开图片处理工具", + }); + setActiveQuickTool("cutout"); + setSmartCutoutBatchImages((current) => { + revokeSmartCutoutItems(current); + return []; + }); + setSmartCutoutImage((current) => { + if (current?.src) URL.revokeObjectURL(current.src); + return null; + }); + setComposerMenu(null); + }; + + const closeSmartCutoutTool = () => { + runSmartCutoutPageTransition( + { + title: "正在返回首页", + subtitle: "回到电商智能体", + }, + () => { + setSmartCutoutBatchImages((current) => { + revokeSmartCutoutItems(current); + return []; + }); + setSmartCutoutImage((current) => { + if (current?.src) URL.revokeObjectURL(current.src); + return null; + }); + setActiveQuickTool(null); + setComposerMenu(null); + }, + ); + }; + + const goSmartCutoutPrevious = () => { + if (!smartCutoutImage) { + closeSmartCutoutTool(); + return; + } + runSmartCutoutPageTransition( + { + title: "正在返回上一页", + subtitle: "回到图片上传页", + }, + () => { + setSmartCutoutBatchImages((current) => { + revokeSmartCutoutItems(current); + return []; + }); + setSmartCutoutImage((current) => { + if (current?.src) URL.revokeObjectURL(current.src); + return null; + }); + }, + ); + }; + + const addSmartCutoutImage = (files: File[]) => { + const imageFiles = files.filter((file) => file.type.startsWith("image/")); + if (!imageFiles.length) { + toast.error("请上传图片文件"); + return; + } + clearSmartCutoutTransition(); + setSmartCutoutBatchImages((current) => { + revokeSmartCutoutItems(current); + return []; + }); + setSmartCutoutImage((current) => { + if (current?.src) URL.revokeObjectURL(current.src); + return null; + }); + const nextImages = imageFiles.map((file) => ({ src: URL.createObjectURL(file), name: file.name })); + smartCutoutPendingUrlsRef.current = nextImages.map((item) => item.src); + setActiveQuickTool("cutout"); + setSmartCutoutSizeKey("original"); + setSmartCutoutTransitionMessage({ + title: imageFiles.length > 1 ? "正在批量抠图" : "正在智能抠图", + subtitle: imageFiles.length > 1 ? `正在处理 ${imageFiles.length} 张图片` : "即将进入图片编辑室", + }); + setIsSmartCutoutTransitioning(true); + smartCutoutTransitionTimeoutRef.current = window.setTimeout(() => { + smartCutoutTransitionTimeoutRef.current = null; + smartCutoutPendingUrlsRef.current = []; + setSmartCutoutBatchImages(nextImages); + setSmartCutoutImage(nextImages[0]); + setIsSmartCutoutTransitioning(false); + }, 620); + }; + + const handleSmartCutoutUpload = (event: ChangeEvent) => { + const files = event.target.files; + if (!files?.length) return; + addSmartCutoutImage(Array.from(files)); + event.target.value = ""; + }; + + const handleSmartCutoutDrop = (event: DragEvent) => { + event.preventDefault(); + setIsSmartCutoutDragging(false); + const files = Array.from(event.dataTransfer.files); + if (files.length) addSmartCutoutImage(files); + }; + + const smartCutoutBackgroundValue = useMemo(() => { + const rgb = hexToRgb(smartCutoutBackgroundColor) ?? { r: 255, g: 255, b: 255 }; + if (smartCutoutBackgroundAlpha >= 100) return smartCutoutBackgroundColor; + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${Math.round(smartCutoutBackgroundAlpha) / 100})`; + }, [smartCutoutBackgroundAlpha, smartCutoutBackgroundColor]); + + const smartCutoutColorHsv = useMemo(() => hexToHsv(smartCutoutBackgroundColor), [smartCutoutBackgroundColor]); + + const selectedSmartCutoutSize = useMemo( + () => smartCutoutSizeOptions.find((option) => option.key === smartCutoutSizeKey) ?? smartCutoutSizeOptions[0], + [smartCutoutSizeKey], + ); + + const smartCutoutFrameStyle = useMemo( + () => ({ + "--smart-cutout-bg": smartCutoutBackgroundValue, + "--smart-cutout-frame-width": selectedSmartCutoutSize.frameWidth, + "--smart-cutout-frame-aspect": selectedSmartCutoutSize.frameAspect, + "--smart-cutout-image-max-width": selectedSmartCutoutSize.imageMaxWidth, + "--smart-cutout-image-max-height": selectedSmartCutoutSize.imageMaxHeight, + } as CSSProperties), + [selectedSmartCutoutSize, smartCutoutBackgroundValue], + ); + + const applySmartCutoutHsv = (h: number, s: number, v: number) => { + const rgb = hsvToRgb(h, s, v); + setSmartCutoutBackgroundColor(rgbToHex(rgb.r, rgb.g, rgb.b)); + }; + + const updateSmartCutoutColorFromPoint = (element: HTMLElement, clientX: number, clientY: number) => { + const rect = element.getBoundingClientRect(); + const saturation = clampNumber(((clientX - rect.left) / rect.width) * 100, 0, 100); + const value = clampNumber(100 - ((clientY - rect.top) / rect.height) * 100, 0, 100); + applySmartCutoutHsv(smartCutoutColorHsv.h, saturation, value); + }; + + const handleSmartCutoutColorPlanePointer = (event: ReactPointerEvent) => { + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + updateSmartCutoutColorFromPoint(event.currentTarget, event.clientX, event.clientY); + }; + + const handleSmartCutoutColorPlaneMove = (event: ReactPointerEvent) => { + if (event.buttons !== 1) return; + updateSmartCutoutColorFromPoint(event.currentTarget, event.clientX, event.clientY); + }; + + const handleSmartCutoutHexChange = (value: string) => { + const nextValue = value.startsWith("#") ? value : `#${value}`; + if (!/^#[0-9a-fA-F]{0,6}$/.test(nextValue)) return; + setSmartCutoutHexDraft(nextValue); + const normalized = normalizeHexColor(nextValue); + if (normalized) setSmartCutoutBackgroundColor(normalized); + }; + + const scrollSmartCutoutTools = (direction: -1 | 1) => { + smartCutoutToolsRef.current?.scrollBy({ + left: direction * 340, + behavior: "smooth", + }); + }; + + useEffect(() => { + setSmartCutoutHexDraft(smartCutoutBackgroundColor); + }, [smartCutoutBackgroundColor]); + + useEffect(() => { + if (!isSmartCutoutPaletteOpen) return undefined; + const handlePointerDown = (event: PointerEvent) => { + if (!smartCutoutPaletteRef.current?.contains(event.target as Node)) { + setIsSmartCutoutPaletteOpen(false); + } + }; + document.addEventListener("pointerdown", handlePointerDown); + return () => document.removeEventListener("pointerdown", handlePointerDown); + }, [isSmartCutoutPaletteOpen]); + const removeSetImage = (imageId: string) => { setSetImages((current) => { const next = current.filter((item) => item.id !== imageId); @@ -1405,7 +1802,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setStatus("ready"); setResults([]); } catch (err) { - toast.error(err instanceof Error ? err.message : "商品图上传失败"); + toast.error(err instanceof Error ? err.message : "鍟嗗搧鍥句笂浼犲け璐?"); } }; @@ -1426,7 +1823,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setStatus("ready"); setResults([]); } - if (unsupportedCount > 0) toast.info("仅支持上传图片或视频文件"); + if (unsupportedCount > 0) toast.info("浠呮敮鎸佷笂浼犲浘鐗囨垨瑙嗛鏂囦欢"); }; const handleComposerAssetUpload = (event: ChangeEvent) => { @@ -1480,7 +1877,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); hydrateCloneReferenceImageMeta(nextImages); } catch (err) { - toast.error(err instanceof Error ? err.message : "参考图上传失败"); + toast.error(err instanceof Error ? err.message : "鍙傝€冨浘涓婁紶澶辫触"); } }; @@ -1583,7 +1980,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const normalizedPlatform = normalizePlatform(nextPlatform); setPlatform(normalizedPlatform); setRatio((current) => - cloneOutput === "hot" && current.startsWith("上传图片") && hotUploadedRatioOption + cloneOutput === "hot" && current.startsWith("涓婁紶鍥剧墖") && hotUploadedRatioOption ? hotUploadedRatioOption : normalizeRatioForPlatform(normalizedPlatform, current, cloneOutput), ); @@ -1593,7 +1990,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleCloneOutputChange = (nextOutput: CloneOutputKey) => { setCloneOutput(nextOutput); setRatio((current) => - nextOutput === "hot" && current.startsWith("上传图片") && hotUploadedRatioOption + nextOutput === "hot" && current.startsWith("涓婁紶鍥剧墖") && hotUploadedRatioOption ? hotUploadedRatioOption : normalizeRatioForPlatform(platform, current, nextOutput), ); @@ -1734,7 +2131,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setRatio((current) => { const platformRatios = getPlatformRatioOptions(platform, cloneOutput); const availableRatios = hotUploadedRatioOption ? getUniqueRatioOptions([...platformRatios, hotUploadedRatioOption]) : platformRatios; - if (current.startsWith("上传图片") && hotUploadedRatioOption) return hotUploadedRatioOption; + if (current.startsWith("涓婁紶鍥剧墖") && hotUploadedRatioOption) return hotUploadedRatioOption; if (availableRatios.includes(current)) return current; const normalizedRatio = normalizeRatioToken(current); const matchedRatio = availableRatios.find((option) => normalizeRatioToken(option).includes(normalizedRatio)); @@ -1840,6 +2237,34 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return () => document.removeEventListener("pointerdown", handlePointerDown); }, [composerMenu, isCommandComposerCompact, status]); + useEffect(() => { + if (composerMenuCloseTimeoutRef.current !== null) { + window.clearTimeout(composerMenuCloseTimeoutRef.current); + composerMenuCloseTimeoutRef.current = null; + } + if (composerMenu) { + setVisibleComposerMenu(composerMenu); + setIsComposerMenuClosing(false); + return; + } + if (!visibleComposerMenu) return; + setIsComposerMenuClosing(true); + composerMenuCloseTimeoutRef.current = window.setTimeout(() => { + composerMenuCloseTimeoutRef.current = null; + setVisibleComposerMenu(null); + setIsComposerMenuClosing(false); + }, 220); + }, [composerMenu, visibleComposerMenu]); + + useEffect( + () => () => { + if (composerMenuCloseTimeoutRef.current !== null) { + window.clearTimeout(composerMenuCloseTimeoutRef.current); + } + }, + [], + ); + useEffect(() => { if (!openCloneModelSelect) return undefined; @@ -1878,7 +2303,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setGarmentImages((current) => [...current, ...nextImages].slice(0, 5)); setTryOnStatus("ready"); } catch (err) { - toast.error(err instanceof Error ? err.message : "服饰图上传失败"); + toast.error(err instanceof Error ? err.message : "鏈嶉グ鍥句笂浼犲け璐?"); } })(); event.target.value = ""; @@ -1898,7 +2323,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setDetailProductImages((current) => [...current, ...nextImages].slice(0, 3)); setDetailStatus("ready"); } catch (err) { - toast.error(err instanceof Error ? err.message : "详情图上传失败"); + toast.error(err instanceof Error ? err.message : "璇︽儏鍥句笂浼犲け璐?"); } })(); event.target.value = ""; @@ -1908,7 +2333,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); - reader.onerror = () => reject(reader.error || new Error("文件读取失败")); + reader.onerror = () => reject(reader.error || new Error("鏂囦欢璇诲彇澶辫触")); reader.readAsDataURL(blob); }); @@ -1917,7 +2342,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { for (const item of images) { try { if (!item.file && item.src.startsWith("blob:")) { - throw new Error("本地预览图缺少原始文件,无法上传"); + throw new Error("鏈湴棰勮鍥剧己灏戝師濮嬫枃浠讹紝鏃犳硶涓婁紶"); } const rawBlob = item.file ?? (item.src.startsWith("data:") ? null : await (await fetch(item.src)).blob()); const mimeType = normalizeEcommerceImageMime( @@ -1937,9 +2362,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const IMAGE_MODEL = "gpt-image-2"; const setCountLabels: Record = { - 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" }, + 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 => { @@ -1969,10 +2394,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { 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(`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."); + parts.push("Must comply with platform image guidelines 鈥?proper margins, no watermark, professional quality."); return parts.join(" "); }; @@ -1987,7 +2412,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { 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."); + 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."); @@ -2079,7 +2504,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { imageGen.updateTask(storeId, { status: "completed", progress: 100, resultUrl: persistedUrl }); } else { generatedUrls.push(""); - imageGen.updateTask(storeId, { status: "failed", error: "生成未返回结果" }); + imageGen.updateTask(storeId, { status: "failed", error: "鐢熸垚鏈繑鍥炵粨鏋?" }); } } } @@ -2097,9 +2522,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { } if (err instanceof ServerRequestError && err.status === 402) { setResultFn([]); - toast.error("余额不足,请充值后继续"); + toast.error("浣欓涓嶈冻锛岃鍏呭€煎悗缁х画"); } else { - const msg = err instanceof Error ? err.message : "生成失败"; + const msg = err instanceof Error ? err.message : "鐢熸垚澶辫触"; toast.error(msg); } setStatusFn("failed"); @@ -2167,7 +2592,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { imageGen.updateTask(storeId, { status: "completed", progress: 100, resultUrl: persistedUrl }); } else { statusFn?.("idle"); - imageGen.updateTask(storeId, { status: "failed", error: "生成未返回结果" }); + imageGen.updateTask(storeId, { status: "failed", error: "鐢熸垚鏈繑鍥炵粨鏋?" }); } } catch (err) { if (imageAbortRef.current.current) { @@ -2175,10 +2600,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return; } if (err instanceof ServerRequestError && err.status === 402) { - resultFn?.([{ id: `ecommerce-error-402`, src: "", label: "余额不足,请充值后继续" }]); - toast.error("余额不足,请充值后继续"); + resultFn?.([{ id: `ecommerce-error-402`, src: "", label: "浣欓涓嶈冻锛岃鍏呭€煎悗缁х画" }]); + toast.error("浣欓涓嶈冻锛岃鍏呭€煎悗缁х画"); } else { - const msg = err instanceof Error ? err.message : "生成失败"; + const msg = err instanceof Error ? err.message : "鐢熸垚澶辫触"; toast.error(msg); } statusFn?.("failed"); @@ -2192,7 +2617,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const readAsDataUrl = (file: File): Promise => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); - reader.onerror = () => reject(new Error("文件读取失败")); + reader.onerror = () => reject(new Error("鏂囦欢璇诲彇澶辫触")); reader.readAsDataURL(file); }); @@ -2231,7 +2656,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return; } if (resultUrl) { - setResults([{ id: crypto.randomUUID(), src: resultUrl, label: "换装视频" }]); + setResults([{ id: crypto.randomUUID(), src: resultUrl, label: "鎹㈣瑙嗛" }]); } setStatus("done"); } catch (err) { @@ -2240,7 +2665,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return; } setStatus("failed"); - toast.error(err instanceof Error ? err.message : "视频换装生成失败"); + toast.error(err instanceof Error ? err.message : "瑙嗛鎹㈣鐢熸垚澶辫触"); } }; @@ -2248,12 +2673,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { if (!canGenerate) return; if ((appUsage?.balanceCents ?? 0) <= 0) { - toast.error("积分不足,请充值后继续"); + toast.error("绉垎涓嶈冻锛岃鍏呭€煎悗缁х画"); return; } if (cloneOutput === "set" && cloneSetTotal > 5) { - if (!window.confirm(`将生成 ${cloneSetTotal} 张图片,可能消耗较多积分,是否继续?`)) return; + if (!window.confirm("将生成 " + String(cloneSetTotal) + " 张图片,可能消耗较多积分,是否继续?")) return; } imageAbortRef.current = { current: false }; @@ -2359,7 +2784,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleDetailAiWrite = () => { setDetailRequirement( - "1.产品名称:无线降噪蓝牙耳机\n2.核心卖点:主动降噪、24H续航、低延迟连接、舒适佩戴\n3.适用人群:通勤、办公、运动和旅行用户\n4.期望场景:地铁通勤、居家办公、户外运动\n5.具体参数:蓝牙5.3、IPX4防水、快充10分钟使用2小时", + "1.浜у搧鍚嶇О锛氭棤绾块檷鍣摑鐗欒€虫満\n2.鏍稿績鍗栫偣锛氫富鍔ㄩ檷鍣€?4H缁埅銆佷綆寤惰繜杩炴帴銆佽垝閫備僵鎴碶n3.閫傜敤浜虹兢锛氶€氬嫟銆佸姙鍏€佽繍鍔ㄥ拰鏃呰鐢ㄦ埛\n4.鏈熸湜鍦烘櫙锛氬湴閾侀€氬嫟銆佸眳瀹跺姙鍏€佹埛澶栬繍鍔╘n5.鍏蜂綋鍙傛暟锛氳摑鐗?.3銆両PX4闃叉按銆佸揩鍏?0鍒嗛挓浣跨敤2灏忔椂", ); }; @@ -2411,7 +2836,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setCloneReferenceImages([]); setCloneReplicateLevel("high"); setRequirement(""); - setCloneSettingName("新建创作"); + setCloneSettingName("鏂板缓鍒涗綔"); setResults([]); setStatus("idle"); setGarmentImages([]); @@ -2433,19 +2858,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const isDetail = activeTool === "detail"; const isTryOn = activeTool === "wear"; const isCloneTool = activeTool === "clone"; - const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/详情页" : isTryOn ? "AI服饰穿戴" : activeToolMeta?.label || "商品工具"; + const isSmartCutoutTool = isCloneTool && activeQuickTool === "cutout"; + const pageLabel = isSetTool ? "鍟嗗搧濂楀浘" : isDetail ? "A+/璇︽儏椤?" : isTryOn ? "AI鏈嶉グ绌挎埓" : activeToolMeta?.label || "鍟嗗搧宸ュ叿"; const setPrimaryLabel = setImages.length === 0 - ? `请先上传商品原图` - : productSetStatus === "generating" - ? "生成中..." - : `生成${selectedProductSetOutput.label}`; + ? "璇峰厛涓婁紶鍟嗗搧鍘熷浘" + : productSetStatus === "generating" + ? "鐢熸垚涓?.." + : "鐢熸垚" + selectedProductSetOutput.label; const tryOnPrimaryLabel = - garmentImages.length === 0 ? "请先上传服装图片" : tryOnStatus === "generating" ? "生成中..." : "生成服饰穿戴图"; + garmentImages.length === 0 ? "璇峰厛涓婁紶鏈嶈鍥剧墖" : tryOnStatus === "generating" ? "鐢熸垚涓?.." : "鐢熸垚鏈嶉グ绌挎埓鍥?"; const detailPrimaryLabel = - detailProductImages.length === 0 ? "请上传产品图" : detailStatus === "generating" ? "生成中..." : "生成A+详情页"; + detailProductImages.length === 0 ? "璇蜂笂浼犱骇鍝佸浘" : detailStatus === "generating" ? "鐢熸垚涓?.." : "鐢熸垚A+璇︽儏椤?"; const clonePrimaryLabel = - productImages.length === 0 ? "请先上传商品原图" : status === "generating" ? "生成中..." : `生成${selectedCloneOutput.label}`; + productImages.length === 0 ? "璇峰厛涓婁紶鍟嗗搧鍘熷浘" : status === "generating" ? "鐢熸垚涓?.." : "鐢熸垚" + selectedCloneOutput.label; const setPreviewCards: CloneResult[] = []; let setIndex = 0; for (const countKey of cloneSetCountKeys) { @@ -2453,9 +2879,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const info = setCountLabels[countKey]; for (let i = 0; i < count; i++) { setPreviewCards.push({ - id: `${countKey}-${i}`, + id: String(countKey) + "-" + String(i), src: productSetResultImages[setIndex] || productSetPreviewCards[setIndex % productSetPreviewCards.length]?.src || "", - label: `${info.label}${count > 1 ? ` ${i + 1}` : ""}`, + label: info.label + (count > 1 ? " " + String(i + 1) : ""), }); setIndex++; } @@ -2468,9 +2894,9 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const info = setCountLabels[countKey]; for (let i = 0; i < count; i++) { clonePreviewCards.push({ - id: `${countKey}-${i}`, + id: String(countKey) + "-" + String(i), src: productSetResultImages[cloneIndex] || productSetPreviewCards[cloneIndex % productSetPreviewCards.length]?.src || "", - label: `${info.label}${count > 1 ? ` ${i + 1}` : ""}`, + label: info.label + (count > 1 ? " " + String(i + 1) : ""), }); cloneIndex++; } @@ -2480,7 +2906,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { cloneOutput === "set" ? productSetResultImages .filter(Boolean) - .map((src, index) => ({ id: `history-set-${index}`, src, label: clonePreviewCards[index]?.label || `套图 ${index + 1}` })) + .map((src, index) => ({ id: "history-set-" + String(index), src, label: clonePreviewCards[index]?.label || "濂楀浘 " + String(index + 1) })) : results.filter((item) => item.src); const buildHistorySignature = (output: CloneOutputKey, prompt: string, historyResults: CloneResult[], sourceImages: CloneImageItem[]) => @@ -2496,10 +2922,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const minute = 60 * 1000; const hour = 60 * minute; const day = 24 * hour; - if (diff < minute) return "刚刚"; - if (diff < hour) return `${Math.floor(diff / minute)} 分钟前`; - if (diff < day) return `${Math.floor(diff / hour)} 小时前`; - return `${Math.floor(diff / day)} 天前`; + if (diff < minute) return "鍒氬垰"; + if (diff < hour) return String(Math.floor(diff / minute)) + " 分钟前"; + if (diff < day) return String(Math.floor(diff / hour)) + " 小时前"; + return String(Math.floor(diff / day)) + " 天前"; }; const saveCurrentEcommerceHistory = () => { @@ -2509,8 +2935,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { if (lastSavedHistorySignatureRef.current === signature && activeHistoryRecordId) return activeHistoryRecordId; const createdAt = Date.now(); - const outputLabel = cloneOutputOptions.find((option) => option.key === cloneOutput)?.label || "生成记录"; - const title = requirement.trim() || `${outputLabel} ${new Date(createdAt).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" })}`; + const outputLabel = cloneOutputOptions.find((option) => option.key === cloneOutput)?.label || "鐢熸垚璁板綍"; + const title = requirement.trim() || outputLabel + " " + new Date(createdAt).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); const record: EcommerceHistoryRecord = { id: crypto.randomUUID(), title, @@ -2579,7 +3005,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { if (historyRefreshLockRef.current) return; historyRefreshLockRef.current = true; setIsHistoryRefreshing(true); - setHistoryRefreshMessage("刷新中..."); + setHistoryRefreshMessage("鍒锋柊涓?.."); setHistoryRefreshStamp(Date.now()); window.setTimeout(() => { @@ -2594,7 +3020,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { writeEcommerceHistoryRecords(mergedRecords); setHistoryRefreshTick((tick) => tick + 1); setEcommerceHistoryRecords(mergedRecords); - setHistoryRefreshMessage(mergedRecords.length ? `已刷新 ${mergedRecords.length} 条记录` : "暂无可刷新记录"); + setHistoryRefreshMessage(mergedRecords.length ? "已刷新 " + String(mergedRecords.length) + " 条记录" : "暂无可刷新记录"); setHistoryRefreshStamp(Date.now()); setIsHistoryRefreshing(false); historyRefreshLockRef.current = false; @@ -2622,7 +3048,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }> = [ { key: "platform", label: "平台", value: platform, options: platformOptions, onChange: handleClonePlatformChange }, { key: "market", label: "国家", value: market, options: marketOptions, onChange: handleCloneMarketChange }, - { key: "language", label: "语言", value: language, options: cloneLanguageOptions, onChange: setLanguage }, + { key: "language", label: "语种", value: language, options: cloneLanguageOptions, onChange: setLanguage }, { key: "ratio", label: "尺寸/比例", value: ratio, options: cloneRatioOptions, onChange: setRatio }, ]; @@ -2820,7 +3246,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { {activeToolMeta?.icon} {activeToolMeta?.label} - 该工具页面正在接入,当前可使用电商AI作图、商品套图、A+详情与服饰穿戴。 + 该工具页面正在接入,当前可使用电商 AI 作图、商品套图、A+ 详情与服饰穿搭。
该工具页面正在接入,当前可使用电商AI作图、商品套图、A+详情与服饰穿戴。
该工具页面正在接入,当前可使用电商 AI 作图、商品套图、A+ 详情与服饰穿搭。