feat: enhance ecommerce UI — sidebar, transitions, ratio options, cancel buttons, i18n fixes

This commit is contained in:
Codex
2026-06-11 20:38:35 +08:00
parent 31c0e79e2e
commit 9b9a276014
10 changed files with 555 additions and 196 deletions
+23
View File
@@ -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",
+1
View File
@@ -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"
+3
View File
@@ -184,6 +184,7 @@ function App() {
const [sessionNotice, setSessionNotice] = useState<string | null>(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() {
}
>
<EcommercePage
key={workspaceKey}
projects={[]}
isAuthenticated={Boolean(session)}
onStartCreate={() => undefined}
+163 -126
View File
@@ -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";
@@ -324,8 +325,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锛?"],
@@ -353,13 +354,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锛?"],
@@ -387,12 +388,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锛?"],
@@ -415,7 +416,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: "抖音电商",
@@ -431,12 +432,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锛?"],
@@ -459,13 +460,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锛?"],
@@ -488,13 +489,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锛?"],
@@ -517,12 +518,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锛?"],
@@ -541,14 +542,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锛?"],
@@ -571,11 +572,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: {
@@ -599,12 +600,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锛?"],
@@ -627,16 +628,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];
@@ -686,34 +687,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<string, string> = {
"鑻辨枃": "英文",
"涓枃": "中文",
"鑻辫": "英文",
"鏃ヨ": "日文",
"鏃ユ枃": "日文",
"寰疯": "德文",
"寰锋枃": "德文",
"娉曡": "法文",
"娉曟枃": "法文",
"闊╄": "韩文",
"闊╂枃": "韩文",
"瑗挎枃": "西班牙语",
"瑗跨彮鐗欒": "西班牙语",
"钁℃枃": "葡萄牙语",
"钁¤悇鐗欒": "葡萄牙语",
"英文": "英文",
"中文": "中文",
"英语": "英文",
"日语": "日文",
"日文": "日文",
"德语": "德文",
"德文": "德文",
"法语": "法文",
"法文": "法文",
"韩语": "韩文",
"韩文": "韩文",
"西文": "西班牙语",
"西班牙语": "西班牙语",
"葡文": "葡萄牙语",
"葡萄牙语": "葡萄牙语",
"鍗板凹璇?": "印度尼西亚语",
"鍗板害灏艰タ浜氳": "印度尼西亚语",
"鑿插緥瀹捐": "菲律宾语(他加禄语)",
"鑿插緥瀹捐锛堜粬鍔犵璇級": "菲律宾语(他加禄语)",
"印度尼西亚语": "印度尼西亚语",
"菲律宾语": "菲律宾语(他加禄语)",
"菲律宾语(他加禄语)": "菲律宾语(他加禄语)",
};
const defaultPlatformSpec = platformSpecOptions[0]!;
const getPlatformSpec = (value: string) =>
platformSpecOptions.find((option) => option.label === value || option.aliases?.includes(value)) ?? defaultPlatformSpec;
const legacyPlatformAliases: Record<string, string> = {
"娣樺疂/澶╃尗": "淘宝/天猫",
"浜笢": "京东",
"淘宝/天猫": "淘宝/天猫",
"京东": "京东",
"鎷煎澶?": "拼多多",
"鎶栭煶鐢靛晢": "抖音电商",
"抖音电商": "抖音电商",
"浜氶┈閫?Amazon": "亚马逊 Amazon",
"閫熷崠閫?": "速卖通",
};
@@ -731,7 +732,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;
@@ -739,7 +740,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;
@@ -768,12 +769,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" */
@@ -893,29 +894,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],
@@ -968,7 +969,7 @@ const blobToDataUrl = (blob: Blob): Promise<string> =>
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);
});
@@ -1218,6 +1219,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [showHostingModal, setShowHostingModal] = useState(false);
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
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<ReturnType<typeof setTimeout> | null>(null);
const [smartCutoutImage, setSmartCutoutImage] = useState<SmartCutoutImageItem | null>(null);
const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<SmartCutoutImageItem[]>([]);
const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff");
@@ -1477,7 +1480,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [requirement, setRequirement] = useState("");
const [requirementImageMentionQuery, setRequirementImageMentionQuery] = useState<string | null>(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]));
@@ -1681,6 +1684,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);
@@ -2393,7 +2414,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setStatus("ready");
setResults([]);
}
if (unsupportedCount > 0) toast.info("浠呮敮鎸佷笂浼犲浘鐗囨垨瑙嗛鏂囦欢");
if (unsupportedCount > 0) toast.info("仅支持上传图片或视频文件");
};
const handleComposerAssetUpload = (event: ChangeEvent<HTMLInputElement>) => {
@@ -2453,7 +2474,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
});
hydrateCloneReferenceImageMeta(nextImages);
} catch (err) {
toast.error(err instanceof Error ? err.message : "鍙傝€冨浘涓婁紶澶辫触");
toast.error(err instanceof Error ? err.message : "参考图上传失败");
}
};
@@ -2561,7 +2582,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),
);
@@ -2571,7 +2592,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),
);
@@ -2717,7 +2738,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));
@@ -2923,7 +2944,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);
});
@@ -2948,7 +2969,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(
@@ -2969,7 +2990,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const setCountLabels: Record<CloneSetCountKey, { label: string; promptDesc: string }> = {
selling: { label: "鍗栫偣鍥?", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" },
white: { label: "鐧藉簳鍥?", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" },
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" },
};
@@ -3000,10 +3021,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(" ");
};
@@ -3018,7 +3039,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.");
@@ -3110,7 +3131,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: "生成鏈繑鍥炵粨鏋?" });
}
}
}
@@ -3128,9 +3149,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");
@@ -3198,7 +3219,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) {
@@ -3206,10 +3227,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");
@@ -3223,7 +3244,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const readAsDataUrl = (file: File): Promise<string> => 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);
});
@@ -3262,7 +3283,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) {
@@ -3271,7 +3292,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
return;
}
setStatus("failed");
toast.error(err instanceof Error ? err.message : "瑙嗛鎹㈣鐢熸垚澶辫触");
toast.error(err instanceof Error ? err.message : "视频换装生成失败");
}
};
@@ -3279,7 +3300,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
if (!canGenerate) return;
if ((appUsage?.balanceCents ?? 0) <= 0) {
toast.error("绉垎涓嶈冻锛岃鍏呭€煎悗缁х画");
toast.error("积分不足,请充值后继续");
return;
}
@@ -3395,7 +3416,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小时",
);
};
@@ -3447,7 +3468,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setCloneReferenceImages([]);
setCloneReplicateLevel("high");
setRequirement("");
setCloneSettingName("鏂板缓鍒涗綔");
setCloneSettingName("新建创作");
setResults([]);
setStatus("idle");
setGarmentImages([]);
@@ -3475,19 +3496,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) {
@@ -3522,7 +3543,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[]) =>
@@ -3538,7 +3559,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)) + " 天前";
@@ -3551,7 +3572,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(),
@@ -3935,23 +3956,23 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<section className="product-set-empty-preview" aria-live="polite">
{productSetStatus === "generating" ? <LoadingOutlined /> : <FileImageOutlined />}
<strong>{productSetStatus === "generating" ? "正在生成" : "等待生成"}</strong>
{productSetStatus === "generating" ? <EcommerceProgressBar status="generating" label="商品套图" /> : null}
{productSetStatus === "generating" ? <EcommerceProgressBar status="generating" onCancel={handleCancelGenerate} label="商品套图" /> : null}
<span>{productSetStatus === "generating" ? "AI 正在整理主图、场景、细节与卖点图。" : "上传商品原图并填写信息后,AI 将为您生成专业的电商商品图。"}</span>
</section>
)}
{productSetStatus === "done" ? <p className="product-set-generated-note">{selectedProductSetOutput.label}</p> : null}
<section className="product-set-floating-detail" aria-label="淇℃伅璇︽儏">
<section className="product-set-floating-detail" aria-label="信息详情">
<div className="product-set-floating-detail__head">
<strong></strong>
<strong></strong>
<span>{productSetRequirement.length}/500</span>
</div>
<textarea
value={productSetRequirement}
onChange={(event) => setProductSetRequirement(event.target.value)}
maxLength={500}
placeholder="寤鸿鍖呭惈浠ヤ笅淇℃伅锛氫骇鍝佸悕绉般€佹牳蹇冨崠鐐广€佹湡鏈涘満鏅€佸叿浣撳弬鏁?"
placeholder="建议鍖呭惈浠ヤ笅淇℃伅锛氫骇鍝佸悕绉般€佹牳蹇冨崠鐐广€佹湡鏈涘満鏅€佸叿浣撳弬鏁?"
/>
<button type="button" className="product-set-floating-submit" disabled={!canGenerateSet} onClick={handleSetGenerate}>
{productSetStatus === "generating" ? <LoadingOutlined /> : null}
@@ -3964,7 +3985,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
) : null}
</section>
<button type="button" className="product-clone-help" aria-label="甯姪">
<button type="button" className="product-clone-help" aria-label="帮助">
<QuestionCircleOutlined />
</button>
</main>
@@ -4197,7 +4218,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
{cloneOutput === "video" ? (
<>
<section className="clone-ai-flow-pipeline" aria-label="生成流程">
{/* Source Node 鈥?鍘熷浘绱犳潗 */}
{/* Source Node —原图素材 */}
<div className="clone-ai-flow-source">
<div className="clone-ai-flow-node clone-ai-flow-node--source">
{productImages[0]?.src ? (
@@ -4211,7 +4232,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<span className="clone-ai-flow-node__label"></span>
</div>
{/* Connector 鈥?鍒嗘敮杩炴帴绾?*/}
{/* Connector 鍒嗘敮杩炴帴绾?*/}
<div className="clone-ai-flow-connector" aria-hidden="true">
<div className="clone-ai-flow-connector__trunk" />
<div className="clone-ai-flow-connector__branches">
@@ -4221,7 +4242,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
</div>
</div>
{/* Branches 鈥?鐢熸垚璺緞鍒嗘敮 */}
{/* Branches —生成路径分支 */}
{status === "done" ? (
<div className="clone-ai-flow-branches">
{results[0]?.src ? (
@@ -4284,14 +4305,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
)}
</section>
{/* Status Overlay 鈥?鐢熸垚鐘舵€佽鐩栧眰 */}
{/* Status Overlay —生成状态覆盖层 */}
{status === "generating" || status === "failed" ? (
<section className="clone-ai-flow-status" aria-live="polite">
{status === "generating" ? (
<>
<LoadingOutlined style={{ fontSize: 28 }} />
<strong></strong>
<EcommerceProgressBar status="generating" label={`${selectedCloneOutput.label}生成`} />
<EcommerceProgressBar status="generating" onCancel={handleCancelGenerate} label={`${selectedCloneOutput.label}生成`} />
<span>AI {platform} / {market} {selectedCloneOutput.label}</span>
</>
) : status === "failed" ? (
@@ -4301,7 +4322,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<span></span>
{lastFailedActionRef.current ? (
<button type="button" className="clone-ai-retry-btn" onClick={lastFailedActionRef.current}>
<ReloadOutlined />
<ReloadOutlined />
</button>
) : null}
</>
@@ -4348,7 +4369,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<section className="clone-ai-empty-state" aria-live="polite">
{status === "generating" ? <LoadingOutlined /> : status === "failed" ? <FrownOutlined /> : <FileImageOutlined />}
<strong>{status === "generating" ? "正在生成" : status === "failed" ? "生成失败" : "等待生成"}</strong>
{status === "generating" ? <EcommerceProgressBar status="generating" label={`${selectedCloneOutput.label}生成`} /> : null}
{status === "generating" ? <EcommerceProgressBar status="generating" onCancel={handleCancelGenerate} label={`${selectedCloneOutput.label}生成`} /> : null}
<span>
{status === "generating"
? "AI 正在为 " + platform + " / " + market + " 整理" + selectedCloneOutput.label + "。"
@@ -4526,14 +4547,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
{ label: "智能抠图", icon: <FileImageOutlined />, onClick: openSmartCutoutUpload },
{ label: "商品套图", icon: <AppstoreOutlined />, onClick: openQuickProductSetPage },
{ label: "图片修改", icon: <SettingOutlined />, onClick: openImageWorkbenchPage },
{ label: "增/去水印", icon: <FileImageOutlined />, onClick: openWatermarkRemovalPage },
{ label: "去水印", icon: <FileImageOutlined />, onClick: openWatermarkRemovalPage },
{ label: "图片批处理", icon: <FolderOpenOutlined /> },
{ label: "一键翻译", icon: <FileImageOutlined /> },
{ label: "A+/详情页", icon: <FileImageOutlined />, onClick: openQuickDetailPage },
{ label: "变清晰", icon: <FileImageOutlined /> },
{ label: "AI消除", icon: <SettingOutlined /> },
{ label: "证件照", icon: <SkinOutlined /> },
{ label: "爆款视频", icon: <CloudUploadOutlined />, onClick: openHotVideoPage },
{ label: "广告视频", icon: <CloudUploadOutlined />, onClick: openHotVideoPage },
{ label: "拼图", icon: <TableOutlined /> },
].map((item) => (
<button
@@ -5238,13 +5259,13 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
);
const hotVideoPreview = (
<main className="ecom-hot-video-page" aria-label="爆款视频">
<main className="ecom-hot-video-page" aria-label="广告视频">
<nav className="ecom-hot-video-nav">
<button type="button" className="ecom-hot-video-back" onClick={closeHotVideoPage}>
</button>
<div className="ecom-hot-video-nav-title">
<h1></h1>
<h1>广</h1>
<span>AI智能策划 · </span>
</div>
<div className="ecom-hot-video-nav-meta">
@@ -5406,6 +5427,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<div className="ecom-quick-set-body">
<aside className="ecom-quick-set-panel" aria-label="商品套图设置" onWheel={handleQuickPanelWheel}>
<header className="ecom-quick-set-panel-head">
<strong className="ecom-quick-set-page-title"></strong>
<button type="button" className="ecom-quick-set-back" onClick={() => setActiveQuickTool(null)}>
</button>
@@ -5563,6 +5585,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
<div className="ecom-quick-set-body">
<aside className="ecom-quick-set-panel" aria-label="A+详情页设置" onWheel={handleQuickPanelWheel}>
<header className="ecom-quick-set-panel-head">
<strong className="ecom-quick-set-page-title">A+/</strong>
<button type="button" className="ecom-quick-set-back" onClick={() => setActiveQuickTool(null)}>
</button>
@@ -5820,6 +5843,20 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
</main>
);
const quickPageSidebar = (isQuickSetTool || isQuickDetailTool || isHotVideoTool) ? (
<aside className="ecom-quick-page-sidebar" aria-label="快捷页面切换">
<button type="button" className={isQuickSetTool ? "is-active" : ""} onClick={openQuickProductSetPage} title="商品套图" aria-label="商品套图">
<AppstoreOutlined /><span></span>
</button>
<button type="button" className={isQuickDetailTool ? "is-active" : ""} onClick={openQuickDetailPage} title="A+/详情" aria-label="A+/详情">
<FileImageOutlined /><span>A+/</span>
</button>
<button type="button" className={isHotVideoTool ? "is-active" : ""} onClick={openHotVideoPage} title="广告视频" aria-label="广告视频">
<ThunderboltOutlined /><span>广</span>
</button>
</aside>
) : null;
const activePreview = isSetTool
? setPreview
: isDetail
@@ -5870,7 +5907,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
id={isCloneTool ? "ecommerce-clone-settings-panel" : undefined}
className={`product-clone-panel tool-panel-enter`}
key={activeTool}
aria-label={`${pageLabel}鍙傛暟`}
aria-label={`${pageLabel}参数`}
aria-hidden={isCloneTool && isCloneSettingsCollapsed ? true : undefined}
>
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel}
@@ -5881,10 +5918,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
type="button"
className="clone-ai-settings-toggle"
onClick={() => setIsCloneSettingsCollapsed((current) => !current)}
aria-label={isCloneSettingsCollapsed ? "灞曞紑璁剧疆闈㈡澘" : "鏀惰捣璁剧疆闈㈡澘"}
aria-label={isCloneSettingsCollapsed ? "展开设置面板" : "收起设置面板"}
aria-controls="ecommerce-clone-settings-panel"
aria-expanded={!isCloneSettingsCollapsed}
title={isCloneSettingsCollapsed ? "灞曞紑璁剧疆闈㈡澘" : "鏀惰捣璁剧疆闈㈡澘"}
title={isCloneSettingsCollapsed ? "展开设置面板" : "收起设置面板"}
>
{isCloneSettingsCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</button>
@@ -1,8 +1,10 @@
import { useSmoothedProgress } from "../../hooks/useSmoothedProgress";
import type { ReactNode } from "react";
interface EcommerceProgressBarProps {
status: "idle" | "generating" | "done" | "failed" | string;
label?: string;
onCancel?: () => void;
}
function mapStatus(status: string): "running" | "completed" | "failed" {
@@ -12,7 +14,7 @@ function mapStatus(status: string): "running" | "completed" | "failed" {
return "running";
}
export function EcommerceProgressBar({ status, label }: EcommerceProgressBarProps) {
export function EcommerceProgressBar({ status, label, onCancel }: EcommerceProgressBarProps) {
const progress = mapStatus(status) === "running" ? 50 : 100;
const smoothed = useSmoothedProgress(progress, mapStatus(status));
@@ -21,6 +23,17 @@ export function EcommerceProgressBar({ status, label }: EcommerceProgressBarProp
return (
<div className="ecommerce-progress-bar">
<span className="ecommerce-progress-bar__label">{label || "AI 正在生成"}</span>
{onCancel ? (
<button
type="button"
className="ecommerce-progress-bar__cancel"
onClick={(e) => { e.stopPropagation(); onCancel(); }}
title="取消生成"
aria-label="取消生成"
>
</button>
) : null}
<div className="ecommerce-progress-bar__track">
<div className="ecommerce-progress-bar__fill" style={{ width: `${smoothed}%` }} />
</div>
@@ -44,6 +44,7 @@ interface EcommerceVideoWorkspaceProps {
onRequestLogin?: () => void;
onOpenHistory?: () => void;
triggerPlan?: number;
saveRef?: { current: (() => void) | null };
}
const ALL_STEPS: PlanStep[] = [
@@ -108,6 +109,7 @@ export default function EcommerceVideoWorkspace({
onRequestLogin,
onOpenHistory,
triggerPlan,
saveRef,
}: EcommerceVideoWorkspaceProps) {
const [stage, setStage] = useState<EcommerceVideoStage>("idle");
const [planResult, setPlanResult] = useState<EcommerceVideoPlanResult | null>(null);
@@ -195,6 +197,32 @@ export default function EcommerceVideoWorkspace({
}).catch(() => {});
}, [stage, planResult, scenes, sourceImageUrls, platform, aspectRatio, durationSeconds, resolution]);
// ── Expose manual save via ref ──────────────────────────
const planResultRef = useRef(planResult);
planResultRef.current = planResult;
const scenesRef = useRef(scenes);
scenesRef.current = scenes;
const sourceImageUrlsRef = useRef(sourceImageUrls);
sourceImageUrlsRef.current = sourceImageUrls;
useEffect(() => {
if (!saveRef) return;
saveRef.current = () => {
const currentPlan = planResultRef.current;
const currentScenes = scenesRef.current;
const currentSources = sourceImageUrlsRef.current;
if (!currentPlan || !currentScenes.length) return;
const title = currentPlan.storyboard?.video_title || currentPlan.summary?.product_name || "电商广告视频";
saveVideoHistory({
title,
config: { platform, aspectRatio, durationSeconds, resolution },
plan: currentPlan as unknown as Record<string, unknown>,
scenes: currentScenes.map((s) => ({ sceneId: s.sceneId, prompt: s.prompt, imageUrl: s.imageUrl, videoUrl: s.resultUrl })),
sourceImageUrls: currentSources,
}).catch(() => {});
};
}, [saveRef, platform, aspectRatio, durationSeconds, resolution]);
// ── Keep-alive: resume polling for running tasks ──────────
useEffect(() => {
if (keepalivePollingStartedRef.current) return;
@@ -6,6 +6,7 @@ import {
HistoryOutlined,
LoadingOutlined,
PlayCircleOutlined,
SaveOutlined,
} from "@ant-design/icons";
import {
fetchVideoHistory,
@@ -16,11 +17,13 @@ import {
interface EcommerceVideoHistoryPanelProps {
visible: boolean;
onClose: () => void;
onSaveCurrent?: () => void;
}
export default function EcommerceVideoHistoryPanel({
visible,
onClose,
onSaveCurrent,
}: EcommerceVideoHistoryPanelProps) {
const [items, setItems] = useState<VideoHistoryItem[]>([]);
const [total, setTotal] = useState(0);
@@ -68,9 +71,16 @@ export default function EcommerceVideoHistoryPanel({
<div className="ecom-video-history-panel__header">
<HistoryOutlined />
<span></span>
<button className="ecom-video-history-panel__close" onClick={onClose}>
<CloseOutlined />
</button>
<div className="ecom-video-history-panel__actions">
{onSaveCurrent ? (
<button className="ecom-video-history-panel__save" onClick={onSaveCurrent} title="保存当前">
<SaveOutlined />
</button>
) : null}
<button className="ecom-video-history-panel__close" onClick={onClose} title="收起面板">
<HistoryOutlined />
</button>
</div>
</div>
<div className="ecom-video-history-panel__body">
+207 -7
View File
@@ -718,7 +718,7 @@
.ecommerce-auth-modal__panel {
border-color: rgba(16, 115, 204, 0.14) !important;
color: #10202c !important;
background: rgba(254, 255, 255, 0.96) !important;
background: #feffff !important;
box-shadow: 0 28px 90px rgba(16, 115, 204, 0.16), 0 12px 34px rgba(20, 80, 100, 0.08) !important;
animation: ecommerce-auth-panel-in 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
@@ -2701,6 +2701,10 @@
width: min(1036px, calc(100% - 56px)) !important;
margin-right: 0 !important;
margin-left: 0 !important;
;
transition: top 520ms cubic-bezier(0.16, 1, 0.3, 1),
transform 520ms cubic-bezier(0.16, 1, 0.3, 1),
width 520ms cubic-bezier(0.16, 1, 0.3, 1) !important;
}
.ecommerce-standalone .product-clone-page[data-tool="clone"] .ecom-command-composer-wrap.is-before-generate {
@@ -2709,7 +2713,7 @@
}
.ecommerce-standalone .product-clone-page[data-tool="clone"] .ecom-command-composer-wrap.has-generated {
top: 24px !important;
top: 48px !important;
transform: translateX(-50%) !important;
}
@@ -4123,7 +4127,127 @@
pointer-events: none !important;
}
/* ── Hot Video Page (爆款视频) ──────────────────────── */
/* ── Quick Page Sidebar — shared left nav for set / detail / hot-video ── */
.ecommerce-standalone .ecom-quick-page-wrap {
position: absolute !important;
inset: 0 !important;
display: flex !important;
min-height: 0 !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
gap: 4px !important;
flex: 0 0 88px !important;
width: 88px !important;
padding: 20px 10px !important;
border-right: 1px solid rgba(30, 189, 219, 0.1) !important;
background:
linear-gradient(180deg,
rgba(255, 255, 255, 0.96) 0%,
rgba(248, 250, 252, 0.94) 50%,
rgba(243, 247, 250, 0.92) 100%) !important;
backdrop-filter: blur(20px) !important;
-webkit-backdrop-filter: blur(20px) !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button {
position: relative !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
gap: 4px !important;
width: 100% !important;
min-height: 66px !important;
padding: 10px 6px !important;
border: 0 !important;
border-radius: 12px !important;
background: transparent !important;
color: #7c8a96 !important;
font-size: 10px !important;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important;
font-weight: 500 !important;
letter-spacing: 0.01em !important;
line-height: 1.2 !important;
cursor: pointer !important;
transition:
background 0.22s cubic-bezier(0.16, 1, 0.3, 1),
color 0.22s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.22s cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 0.22s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button .anticon {
font-size: 22px !important;
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button span:last-child {
display: block !important;
font-size: 10px !important;
font-weight: 500 !important;
letter-spacing: 0.02em !important;
line-height: 1.2 !important;
white-space: nowrap !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button:hover {
background: rgba(30, 189, 219, 0.07) !important;
color: #0d6bb8 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 12px rgba(16, 115, 204, 0.08) !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button:hover .anticon {
transform: scale(1.08) !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button:active {
transform: translateY(0) !important;
box-shadow: none !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button.is-active {
background:
linear-gradient(135deg,
rgba(16, 115, 204, 0.12) 0%,
rgba(30, 189, 219, 0.08) 100%) !important;
color: #0d6bb8 !important;
font-weight: 700 !important;
box-shadow:
0 2px 8px rgba(16, 115, 204, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button.is-active::before {
content: "" !important;
position: absolute !important;
left: 0 !important;
top: 14px !important;
bottom: 14px !important;
width: 3px !important;
border-radius: 0 3px 3px 0 !important;
background:
linear-gradient(180deg,
#1073cc 0%,
#1ebddb 100%) !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button.is-active .anticon {
color: #1073cc !important;
}
.ecommerce-standalone .ecom-quick-page-wrap > :not(.ecom-quick-page-sidebar) {
flex: 1 1 0% !important;
min-width: 0 !important;
min-height: 0 !important;
}
/* ── Hot Video Page (广告视频) ──────────────────────── */
.ecommerce-standalone .product-clone-page[data-tool="clone"].is-hot-video-page {
display: block !important;
overflow: hidden !important;
@@ -6417,6 +6541,14 @@
font-weight: 950 !important;
}
.ecommerce-standalone .ecom-quick-set-page-title {
margin-right: auto !important;
color: #10202c !important;
font-size: 17px !important;
font-weight: 950 !important;
letter-spacing: 0.02em !important;
}
.ecommerce-standalone .ecom-quick-set-panel-head strong {
color: #132435 !important;
font-size: 15px !important;
@@ -7017,12 +7149,76 @@
font-weight: 950 !important;
}
.ecommerce-standalone .ecom-quick-set-primary--cancel {
display: block !important;
position: static !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
width: 100% !important;
margin-top: 8px !important;
min-height: 40px !important;
color: #ff4d4f !important;
background: rgba(255, 77, 79, 0.06) !important;
border: 1px solid rgba(255, 77, 79, 0.18) !important;
border-radius: 12px !important;
box-shadow: none !important;
font-size: 14px !important;
font-weight: 700 !important;
cursor: pointer !important;
transition: background 180ms ease, color 180ms ease, border-color 180ms ease !important;
}
.ecommerce-standalone .ecom-quick-set-primary--cancel:hover {
color: #ffffff !important;
background: #ff4d4f !important;
border-color: #ff4d4f !important;
}
.ecommerce-standalone .ecom-quick-set-primary:disabled {
color: #8fa1af !important;
background: #eef2f5 !important;
box-shadow: none !important;
}
.ecommerce-standalone .ecom-quick-set-page .ecom-quick-set-primary {
position: static !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
width: 100% !important;
margin-top: auto !important;
flex: 0 0 auto !important;
}
.ecommerce-standalone .ecom-quick-set-page .ecom-quick-set-panel {
overflow-y: auto !important;
padding-bottom: 16px !important;
scrollbar-width: auto !important;
scrollbar-color: rgba(16, 115, 204, 0.56) rgba(16, 115, 204, 0.08) !important;
}
.ecommerce-standalone .ecom-quick-set-page .ecom-quick-set-panel::-webkit-scrollbar {
display: block !important;
width: 14px !important;
height: 14px !important;
}
.ecommerce-standalone .ecom-quick-set-page .ecom-quick-set-panel::-webkit-scrollbar-track {
border-radius: 999px !important;
background: rgba(16, 115, 204, 0.08) !important;
}
.ecommerce-standalone .ecom-quick-set-page .ecom-quick-set-panel::-webkit-scrollbar-thumb {
border: 3px solid rgba(248, 249, 250, 0.95) !important;
border-radius: 999px !important;
background: rgba(16, 115, 204, 0.56) !important;
}
.ecommerce-standalone .ecom-quick-set-page .ecom-quick-set-panel::-webkit-scrollbar-thumb:hover {
background: rgba(16, 115, 204, 0.72) !important;
}
.ecommerce-standalone .ecom-quick-detail-page .ecom-quick-set-primary {
position: static !important;
left: auto !important;
@@ -7384,7 +7580,8 @@
box-shadow: 0 8px 18px rgba(16, 115, 204, 0.18) !important;
}
.ecommerce-standalone .ecom-smart-cutout-transition {
.ecommerce-standalone .ecom-smart-cutout-transition,
.ecommerce-standalone .ecom-quick-page-transition {
position: fixed !important;
inset: 64px 0 0 !important;
z-index: 30 !important;
@@ -7399,7 +7596,8 @@
animation: ecom-smart-transition-in 260ms ease both !important;
}
.ecommerce-standalone .ecom-smart-cutout-transition span {
.ecommerce-standalone .ecom-smart-cutout-transition span,
.ecommerce-standalone .ecom-quick-page-transition span {
width: 56px !important;
height: 56px !important;
border: 4px solid rgba(30, 189, 219, 0.16) !important;
@@ -7409,14 +7607,16 @@
animation: ecom-smart-transition-spin 860ms linear infinite !important;
}
.ecommerce-standalone .ecom-smart-cutout-transition strong {
.ecommerce-standalone .ecom-smart-cutout-transition strong,
.ecommerce-standalone .ecom-quick-page-transition strong {
margin-top: 8px !important;
color: #10202c !important;
font-size: 20px !important;
font-weight: 800 !important;
}
.ecommerce-standalone .ecom-smart-cutout-transition em {
.ecommerce-standalone .ecom-smart-cutout-transition em,
.ecommerce-standalone .ecom-quick-page-transition em {
color: #6d7d88 !important;
font-size: 13px !important;
font-style: normal !important;
+81 -54
View File
@@ -37,11 +37,11 @@
display: grid;
grid-template-columns: auto minmax(96px, 1fr) auto;
align-items: center;
gap: 18px;
min-height: 58px;
gap: 14px;
min-height: 46px;
border-bottom: 1px solid #242a33;
background: #11151b;
padding: 10px 16px;
padding: 6px 16px;
}
.ecom-video-flowbar__title {
@@ -224,7 +224,7 @@
min-height: 0;
overflow: auto;
background: #101318;
padding: 32px 40px;
padding: 20px 40px;
display: flex;
align-items: flex-start;
justify-content: center;
@@ -1209,8 +1209,10 @@
width: 420px;
max-width: 90vw;
height: 100vh;
background: #1a1d24;
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5);
background:
radial-gradient(circle at 100% 0%, rgba(30, 189, 219, 0.04), transparent 18rem),
#fbfdfe;
box-shadow: -4px 0 24px rgba(20, 80, 100, 0.1);
animation: ecom-history-slide-in 0.25s ease-out;
}
@@ -1224,29 +1226,45 @@
align-items: center;
gap: 8px;
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
color: #fff;
border-bottom: 1px solid rgba(30, 189, 219, 0.15);
color: #10202c;
font-size: 15px;
font-weight: 500;
font-weight: 600;
}
.ecom-video-history-panel__close {
.ecom-video-history-panel__actions {
margin-left: auto;
display: grid;
width: 28px;
height: 28px;
place-items: center;
border: none;
border-radius: 6px;
background: transparent;
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.ecom-video-history-panel__close:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
.ecom-video-history-panel__close,
.ecom-video-history-panel__save {
display: inline-flex;
align-items: center;
gap: 4px;
height: 32px;
padding: 0 12px;
border: 1px solid rgba(30, 189, 219, 0.18);
border-radius: 8px;
background: rgba(30, 189, 219, 0.04);
color: #6b7c88;
font-size: 13px;
cursor: pointer;
transition: border-color 150ms, background 150ms, color 150ms;
}
.ecom-video-history-panel__close:hover,
.ecom-video-history-panel__save:hover {
border-color: rgba(30, 189, 219, 0.45);
background: rgba(30, 189, 219, 0.1);
color: #1073cc;
}
.ecom-video-history-panel__save {
color: #1073cc;
font-weight: 600;
}
.ecom-video-history-panel__body {
@@ -1265,15 +1283,20 @@
justify-content: center;
gap: 12px;
padding: 60px 20px;
color: rgba(255, 255, 255, 0.4);
color: #a0b8c8;
font-size: 14px;
}
.ecom-video-history-card {
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid rgba(30, 189, 219, 0.12);
border-radius: 10px;
background: rgba(255, 255, 255, 0.03);
background: #ffffff;
padding: 14px;
transition: border-color 150ms;
}
.ecom-video-history-card:hover {
border-color: rgba(30, 189, 219, 0.25);
}
.ecom-video-history-card__header {
@@ -1284,7 +1307,7 @@
}
.ecom-video-history-card__title {
color: #fff;
color: #10202c;
font-size: 13px;
font-weight: 500;
flex: 1;
@@ -1294,7 +1317,7 @@
}
.ecom-video-history-card__date {
color: rgba(255, 255, 255, 0.4);
color: #6b7c88;
font-size: 11px;
white-space: nowrap;
}
@@ -1307,14 +1330,15 @@
border: none;
border-radius: 4px;
background: transparent;
color: rgba(255, 255, 255, 0.35);
color: #a0b8c8;
font-size: 12px;
cursor: pointer;
transition: background 150ms, color 150ms;
}
.ecom-video-history-card__delete:hover {
background: rgba(255, 80, 80, 0.15);
color: #ff5050;
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.ecom-video-history-card__scenes {
@@ -1331,7 +1355,7 @@
height: 60px;
border-radius: 6px;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
background: rgba(30, 189, 219, 0.06);
}
.ecom-video-history-card__scene img {
@@ -1368,24 +1392,26 @@
justify-content: center;
gap: 12px;
padding: 12px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.6);
border-top: 1px solid rgba(30, 189, 219, 0.12);
color: #6b7c88;
font-size: 12px;
}
.ecom-video-history-panel__pager button {
padding: 4px 10px;
border: 1px solid rgba(255, 255, 255, 0.15);
border: 1px solid rgba(30, 189, 219, 0.18);
border-radius: 4px;
background: transparent;
color: rgba(255, 255, 255, 0.7);
background: rgba(30, 189, 219, 0.04);
color: #6b7c88;
font-size: 12px;
cursor: pointer;
transition: border-color 150ms, background 150ms, color 150ms;
}
.ecom-video-history-panel__pager button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.1);
color: #fff;
background: rgba(30, 189, 219, 0.1);
color: #1073cc;
border-color: rgba(30, 189, 219, 0.3);
}
.ecom-video-history-panel__pager button:disabled {
@@ -1401,7 +1427,7 @@
z-index: 9999;
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.6);
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
}
@@ -1412,23 +1438,23 @@
gap: 16px;
padding: 28px 32px;
border-radius: 12px;
background: #1e2128;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
background: #ffffff;
border: 1px solid rgba(30, 189, 219, 0.15);
box-shadow: 0 8px 32px rgba(20, 80, 100, 0.15);
max-width: 340px;
text-align: center;
}
.ecom-video-confirm-dialog__icon {
font-size: 36px;
color: #faad14;
color: #f59e0b;
}
.ecom-video-confirm-dialog__text {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.85);
color: #10202c;
}
.ecom-video-confirm-dialog__actions {
@@ -1439,26 +1465,27 @@
.ecom-video-confirm-dialog__actions button {
padding: 6px 20px;
border: 1px solid rgba(255, 255, 255, 0.15);
border: 1px solid rgba(30, 189, 219, 0.18);
border-radius: 6px;
background: transparent;
color: rgba(255, 255, 255, 0.8);
background: rgba(30, 189, 219, 0.04);
color: #6b7c88;
font-size: 13px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.ecom-video-confirm-dialog__actions button:hover {
background: rgba(255, 255, 255, 0.08);
background: rgba(30, 189, 219, 0.1);
color: #1073cc;
}
.ecom-video-confirm-dialog__actions button.is-danger {
background: #ff4d4f;
border-color: #ff4d4f;
background: #ef4444;
border-color: #ef4444;
color: #fff;
}
.ecom-video-confirm-dialog__actions button.is-danger:hover {
background: #ff7875;
border-color: #ff7875;
background: #f87171;
border-color: #f87171;
}
+22 -5
View File
@@ -43,6 +43,27 @@
transition: width 80ms linear;
}
.ecommerce-progress-bar__cancel {
flex: 0 0 auto;
padding: 3px 14px;
border: 1px solid rgba(255, 77, 79, 0.22);
border-radius: 999px;
background: rgba(255, 255, 255, 0.96);
color: #ff4d4f;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
box-shadow: 0 2px 10px rgba(255, 77, 79, 0.12);
transition: background 180ms ease, box-shadow 180ms ease;
}
.ecommerce-progress-bar__cancel:hover {
background: #ff4d4f;
color: #ffffff;
box-shadow: 0 4px 16px rgba(255, 77, 79, 0.24);
}
.ecommerce-progress-bar__value {
font-size: 12px;
font-weight: 900;
@@ -11215,12 +11236,8 @@
.ecommerce-standalone .product-clone-page[data-tool="clone"] .ecom-command-quick-board {
border: 1px solid rgba(30, 189, 219, 0.12) !important;
border-radius: 22px !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(249, 253, 255, 0.62)),
rgba(255, 255, 255, 0.58) !important;
background: #feffff !important;
box-shadow: 0 22px 58px rgba(16, 115, 204, 0.08) !important;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.ecommerce-standalone .product-clone-page[data-tool="clone"] .ecom-command-quick-board button {