diff --git a/package-lock.json b/package-lock.json index 90c5b9b..e19f643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@ant-design/icons": "5.3.0", "@xyflow/react": "12.10.2", + "iconv-lite": "^0.7.2", "react": "18.2.0", "react-dom": "18.2.0", "zustand": "5.0.13" @@ -2210,6 +2211,22 @@ "node": ">=6.9.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2493,6 +2510,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", diff --git a/package.json b/package.json index 9cdec5e..5ba6785 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@ant-design/icons": "5.3.0", "@xyflow/react": "12.10.2", + "iconv-lite": "^0.7.2", "react": "18.2.0", "react-dom": "18.2.0", "zustand": "5.0.13" diff --git a/src/App.tsx b/src/App.tsx index c1145a3..fb6a1c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -184,6 +184,7 @@ function App() { const [sessionNotice, setSessionNotice] = useState(null); const [profileMenuOpen, setProfileMenuOpen] = useState(false); const [currentPage, setCurrentPage] = useState<"workspace" | "profile">("workspace"); + const [workspaceKey, setWorkspaceKey] = useState(0); useEffect(() => { void loadDarkGreenTheme(); @@ -354,6 +355,7 @@ function App() { const handleOpenWorkspace = () => { setProfileMenuOpen(false); setCurrentPage("workspace"); + setWorkspaceKey((k) => k + 1); }; const handleBugFeedback = () => { @@ -461,6 +463,7 @@ function App() { } > undefined} diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index 9c2a02c..8ccaa68 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -15,6 +15,7 @@ SettingOutlined, SkinOutlined, TableOutlined, + ThunderboltOutlined, } from "@ant-design/icons"; import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent, type DragEvent, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react"; import { useTypewriter } from "../../hooks/useTypewriter"; @@ -366,8 +367,8 @@ 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锛?", "800脳800px\u00a0\u00a0\u00a01锛?"], @@ -395,13 +396,13 @@ const platformSpecOptions: Array<{ defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["涓诲浘 / SKU 鍥?800脳800px锛屸墹3MB", "璇︽儏椤靛 750px 鎴?790px锛屽崟寮犻珮鈮?546px"], - tip: "寤鸿涓诲浘 200-400KB JPG锛岃秴杩?500KB 浼氬奖鍝嶅姞杞介€熷害銆?", + specs: ["主图 / SKU 鍥?800脳800px锛屸墹3MB", "详情页宽 750px 或?790px锛屽崟寮犻珮鈮?546px"], + tip: "建议主图 200-400KB JPG锛岃秴杩?500KB 浼氬奖鍝嶅姞杞介€熷害銆?", }, { label: "京东", - ratios: ["浜笢涓诲浘 / SKU 鍥?800脳800px", "璇︽儏椤靛 750px", "棣栧浘涓讳綋鍗犳瘮 鈮?0%"], - defaultRatio: "浜笢涓诲浘 / SKU 鍥?800脳800px", + ratios: ["京东主图 / SKU 鍥?800脳800px", "详情页宽 750px", "首图主体占比 鈮?0%"], + defaultRatio: "京东主图 / SKU 鍥?800脳800px", ratioGroups: { set: { ratios: ["1000脳1000px\u00a0\u00a0\u00a01锛?"], @@ -429,12 +430,12 @@ const platformSpecOptions: Array<{ defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["涓诲浘 / SKU 鍥?800脳800px锛岀櫧搴曪紝鈮?MB", "璇︽儏椤靛 750px锛岄鍥句富浣撳崰姣?鈮?0%"], + 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锛?", "750脳1000px\u00a0\u00a0\u00a03锛?"], @@ -457,7 +458,7 @@ const platformSpecOptions: Array<{ defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["涓诲浘 750脳352px 鎴?800脳800px锛屸墹1MB", "璇︽儏椤靛 750px锛岃姹傜函鐧藉簳銆佹棤姘村嵃銆佹棤鎷兼帴"], + specs: ["主图 750脳352px 或?800脳800px锛屸墹1MB", "详情页宽 750px,要求纯白底、无水印、无拼接"], }, { label: "抖音电商", @@ -473,12 +474,12 @@ const platformSpecOptions: Array<{ defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["鐭棰?1080脳1920px锛?:16", "30s 鍐呮渶浣?"], + specs: ["鐭棰?1080脳1920px锛?:16", "30s 图呮渶浣?"], }, { label: "亚马逊 Amazon", - ratios: ["涓诲浘 鈮?600脳1600px", "寤鸿 2000脳2000px+", "鏈€灏?500脳500px"], - defaultRatio: "涓诲浘 鈮?600脳1600px", + ratios: ["主图 鈮?600脳1600px", "建议 2000脳2000px+", "鏈€灏?500脳500px"], + defaultRatio: "主图 鈮?600脳1600px", ratioGroups: { set: { ratios: ["1600脳1600px\u00a0\u00a0\u00a01锛?"], @@ -501,13 +502,13 @@ const platformSpecOptions: Array<{ defaultRatio: "1600脳1600px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["涓诲浘 1600脳1600px+锛岀函鐧藉簳锛屸墹10MB", "鏈€灏?500脳500px锛屽缓璁?2000px+ 浠ユ敮鎸佺缉鏀?"], + 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锛?"], @@ -530,13 +531,13 @@ const platformSpecOptions: Array<{ defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["鍟嗗搧涓诲浘鎺ㄨ崘 1024脳1024px锛屽熀纭€ 800脳800px", "鈮?MB锛岀櫧搴曟垨娴呰壊搴?"], - 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锛?"], @@ -559,12 +560,12 @@ const platformSpecOptions: Array<{ defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["鍟嗗搧涓诲浘 800脳800px锛?: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锛?", "1080脳1350px\u00a0\u00a0\u00a04锛?"], @@ -583,14 +584,14 @@ const platformSpecOptions: Array<{ defaultRatio: "1080脳1920px\u00a0\u00a0\u00a09锛?6", }, }, - specs: ["甯栧瓙 1080脳1350px 鎴?1080脳1080px", "Stories / Reels 灏侀潰 1080脳1920px锛屽ご鍍?320脳320px"], - tip: "寤鸿 鈮?MB 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锛?"], @@ -613,11 +614,11 @@ const platformSpecOptions: Array<{ defaultRatio: "800脳800px\u00a0\u00a0\u00a01锛?", }, }, - specs: ["涓诲浘寤鸿 800脳800px 鎴栨洿楂橈紝1:1", "閫傚悎璺ㄥ鐢靛晢涓诲浘銆丼KU 鍥惧拰鍦烘櫙鍥?"], + specs: ["主图建议 800脳800px 或更高,1:1", "适合跨境电商主图、SKU 鍥惧拰鍦烘櫙鍥?"], }, { label: "eBay", - ratios: ["鍟嗗搧鍥?1:1", "鐧藉簳澶氳搴﹀睍绀哄浘 1:1"], + ratios: ["鍟嗗搧鍥?1:1", "白底多角度展示图 1:1"], defaultRatio: "鍟嗗搧鍥?1:1", ratioGroups: { set: { @@ -641,12 +642,12 @@ const platformSpecOptions: Array<{ 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锛?"], @@ -669,16 +670,16 @@ const platformSpecOptions: Array<{ 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("天猫") || value.includes("娣樺疂") || value.includes("澶╃尗")) return [taobaoLogo, tmallLogo]; - if (value.includes("京东") || value.includes("浜笢")) return [jdLogo]; + 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 (value.includes("抖音") || value.includes("抖音")) return [douyinLogo]; if (normalized.includes("amazon")) return [amazonLogo]; if (normalized.includes("shopee")) return [shopeeLogo]; if (normalized.includes("lazada")) return [lazadaLogo]; @@ -728,34 +729,34 @@ 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 legacyPlatformAliases: Record = { - "娣樺疂/澶╃尗": "淘宝/天猫", - "浜笢": "京东", + "淘宝/天猫": "淘宝/天猫", + "京东": "京东", "鎷煎澶?": "拼多多", - "鎶栭煶鐢靛晢": "抖音电商", + "抖音电商": "抖音电商", "浜氶┈閫?Amazon": "亚马逊 Amazon", "閫熷崠閫?": "速卖通", }; @@ -773,7 +774,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.replaceAll("锛?", ":").replaceAll(":", ":").replaceAll("脳", "×").trim(); +const normalizeRatioToken = (value: string) => value.replaceAll(":", ":").replaceAll("×", "×").trim(); const normalizeRatioForPlatform = (platformValue: string, ratioValue: string, mode?: PlatformRatioModeKey) => { const platformRatios = getPlatformRatioOptions(platformValue, mode); if (platformRatios.includes(ratioValue)) return ratioValue; @@ -781,7 +782,7 @@ const normalizeRatioForPlatform = (platformValue: string, ratioValue: string, mo const matchedRatio = platformRatios.find((option) => normalizeRatioToken(option).includes(normalizedRatio)); return matchedRatio ?? getPlatformDefaultRatio(platformValue, mode); }; -const quickSetRatioOptions = ["1:1", "3:4", "9:16", "16:9"]; +const quickSetRatioOptions = ["1:1", "3:4", "4:3", "9:16", "16:9"]; const getQuickSetRatioValue = (value: string) => { const normalizedValue = normalizeRatioToken(value); if (quickSetRatioOptions.includes(normalizedValue)) return normalizedValue; @@ -810,12 +811,12 @@ const formatRatioDisplayValue = (value: string) => { return `${width}×${height}px\u00a0\u00a0\u00a0${formatAspectRatio(width, height)}`; } return normalizedValue - .replace("娣樺疂涓诲浘 / SKU 鍥?", "淘宝主图 / SKU 图 ") - .replace("浜笢涓诲浘 / SKU 鍥?", "京东主图 / SKU 图 ") - .replace("璇︽儏椤靛", "详情页宽") + .replace("淘宝主图 / SKU 鍥?", "淘宝主图 / SKU 图 ") + .replace("京东主图 / SKU 鍥?", "京东主图 / SKU 图 ") + .replace("详情页宽", "详情页宽") .replace("鐭棰?", "短视频") - .replace("涓诲浘", "主图") - .replace("鍟嗗搧涓诲浘", "商品主图") + .replace("主图", "主图") + .replace("商品主图", "商品主图") .replace("鍟嗗搧鍥?", "商品图"); }; /** Extract CSS aspect-ratio from a ratio string like "1000x1000px 1:1" -> "1 / 1" */ @@ -935,29 +936,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: "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: "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], @@ -1010,7 +1011,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); }); @@ -1260,6 +1261,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const [showHostingModal, setShowHostingModal] = useState(false); const [productImages, setProductImages] = useState([]); const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "set" | "detail" | "watermark" | "image-edit" | "hot-video" | null>(null); + const [quickPageTransition, setQuickPageTransition] = useState<{ title: string; subtitle: string } | null>(null); + const quickPageTransitionTimeoutRef = useRef | null>(null); const [smartCutoutImage, setSmartCutoutImage] = useState(null); const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState([]); const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff"); @@ -1519,7 +1522,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])); @@ -1723,6 +1726,24 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { items.forEach(revokeSmartCutoutItem); }; + const clearQuickPageTransition = () => { + if (quickPageTransitionTimeoutRef.current !== null) { + window.clearTimeout(quickPageTransitionTimeoutRef.current); + quickPageTransitionTimeoutRef.current = null; + } + setQuickPageTransition(null); + }; + + const runQuickPageTransition = (message: { title: string; subtitle: string }, action: () => void, delay = 400) => { + clearQuickPageTransition(); + setQuickPageTransition(message); + quickPageTransitionTimeoutRef.current = window.setTimeout(() => { + quickPageTransitionTimeoutRef.current = null; + action(); + setQuickPageTransition(null); + }, delay); + }; + const clearSmartCutoutTransition = () => { if (smartCutoutTransitionTimeoutRef.current !== null) { window.clearTimeout(smartCutoutTransitionTimeoutRef.current); @@ -2435,7 +2456,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setStatus("ready"); setResults([]); } - if (unsupportedCount > 0) toast.info("浠呮敮鎸佷笂浼犲浘鐗囨垨瑙嗛鏂囦欢"); + if (unsupportedCount > 0) toast.info("仅支持上传图片或视频文件"); }; const handleComposerAssetUpload = (event: ChangeEvent) => { @@ -2495,7 +2516,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { }); hydrateCloneReferenceImageMeta(nextImages); } catch (err) { - toast.error(err instanceof Error ? err.message : "鍙傝€冨浘涓婁紶澶辫触"); + toast.error(err instanceof Error ? err.message : "参考图上传失败"); } }; @@ -2603,7 +2624,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), ); @@ -2613,7 +2634,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), ); @@ -2759,7 +2780,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)); @@ -2965,7 +2986,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); }); @@ -2990,7 +3011,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( @@ -3011,7 +3032,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { 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" }, + 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" }, }; @@ -3042,10 +3063,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(" "); }; @@ -3060,7 +3081,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."); @@ -3152,7 +3173,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: "生成鏈繑鍥炵粨鏋?" }); } } } @@ -3170,9 +3191,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"); @@ -3240,7 +3261,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) { @@ -3248,10 +3269,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"); @@ -3265,7 +3286,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); }); @@ -3304,7 +3325,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) { @@ -3313,7 +3334,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return; } setStatus("failed"); - toast.error(err instanceof Error ? err.message : "瑙嗛鎹㈣鐢熸垚澶辫触"); + toast.error(err instanceof Error ? err.message : "视频换装生成失败"); } }; @@ -3321,7 +3342,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { if (!canGenerate) return; if ((appUsage?.balanceCents ?? 0) <= 0) { - toast.error("绉垎涓嶈冻锛岃鍏呭€煎悗缁х画"); + toast.error("积分不足,请充值后继续"); return; } @@ -3437,7 +3458,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const handleDetailAiWrite = () => { setDetailRequirement( - "1.浜у搧鍚嶇О锛氭棤绾块檷鍣摑鐗欒€虫満\n2.鏍稿績鍗栫偣锛氫富鍔ㄩ檷鍣€?4H缁埅銆佷綆寤惰繜杩炴帴銆佽垝閫備僵鎴碶n3.閫傜敤浜虹兢锛氶€氬嫟銆佸姙鍏€佽繍鍔ㄥ拰鏃呰鐢ㄦ埛\n4.鏈熸湜鍦烘櫙锛氬湴閾侀€氬嫟銆佸眳瀹跺姙鍏€佹埛澶栬繍鍔╘n5.鍏蜂綋鍙傛暟锛氳摑鐗?.3銆両PX4闃叉按銆佸揩鍏?0鍒嗛挓浣跨敤2灏忔椂", + "1.产品名称:无线降噪蓝牙耳机\n2.鏍稿績鍗栫偣锛氫富鍔ㄩ檷鍣€?4H续航、低延迟连接、舒适佩戴\n3.适用人群:通勤、办公、运动和旅行用户\n4.期望场景:地铁通勤、居家办公、户外运动\n5.鍏蜂綋参数锛氳摑鐗?.3銆両PX4闃叉按銆佸揩鍏?0分钟使用2小时", ); }; @@ -3489,7 +3510,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { setCloneReferenceImages([]); setCloneReplicateLevel("high"); setRequirement(""); - setCloneSettingName("鏂板缓鍒涗綔"); + setCloneSettingName("新建创作"); setResults([]); setStatus("idle"); setGarmentImages([]); @@ -3517,19 +3538,19 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const isWatermarkTool = isCloneTool && activeQuickTool === "watermark"; const isImageEditTool = isCloneTool && activeQuickTool === "image-edit"; const isHotVideoTool = isCloneTool && activeQuickTool === "hot-video"; - const pageLabel = isSetTool ? "鍟嗗搧濂楀浘" : isDetail ? "A+/璇︽儏椤?" : isTryOn ? "AI鏈嶉グ绌挎埓" : activeToolMeta?.label || "鍟嗗搧宸ュ叿"; + const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/璇︽儏椤?" : isTryOn ? "AI服饰穿戴" : activeToolMeta?.label || "商品工具"; const setPrimaryLabel = setImages.length === 0 - ? "璇峰厛涓婁紶鍟嗗搧鍘熷浘" + ? "请先上传商品原图" : productSetStatus === "generating" - ? "鐢熸垚涓?.." - : "鐢熸垚" + selectedProductSetOutput.label; + ? "生成涓?.." + : "生成" + 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) { @@ -3564,7 +3585,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { cloneOutput === "set" ? productSetResultImages .filter(Boolean) - .map((src, index) => ({ id: "history-set-" + String(index), src, label: clonePreviewCards[index]?.label || "濂楀浘 " + String(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[]) => @@ -3580,7 +3601,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const minute = 60 * 1000; const hour = 60 * minute; const day = 24 * hour; - if (diff < minute) return "鍒氬垰"; + 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)) + " 天前"; @@ -3593,7 +3614,7 @@ 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 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(), @@ -3977,23 +3998,23 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
{productSetStatus === "generating" ? : } {productSetStatus === "generating" ? "正在生成" : "等待生成"} - {productSetStatus === "generating" ? : null} + {productSetStatus === "generating" ? : null} {productSetStatus === "generating" ? "AI 正在整理主图、场景、细节与卖点图。" : "上传商品原图并填写信息后,AI 将为您生成专业的电商商品图。"}
)} {productSetStatus === "done" ?

已生成{selectedProductSetOutput.label}预览

: null} -
+
- 淇℃伅璇︽儏 + 信息详情 {productSetRequirement.length}/500