Merge pull request 'feat: enhance ecommerce UI — sidebar, transitions, ratio options, cancel buttons, i18n fixes' (#6) from codex/ecommerce-ui-current into main
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
Generated
+23
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<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",
|
||||
"閫熷崠閫?": "速卖通",
|
||||
};
|
||||
@@ -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<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);
|
||||
});
|
||||
|
||||
@@ -1260,6 +1261,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");
|
||||
@@ -1519,7 +1522,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]));
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
@@ -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<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" },
|
||||
};
|
||||
|
||||
@@ -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<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);
|
||||
});
|
||||
|
||||
@@ -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 = {}) {
|
||||
<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}
|
||||
@@ -4006,7 +4027,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>
|
||||
@@ -4246,7 +4267,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 ? (
|
||||
@@ -4260,7 +4281,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">
|
||||
@@ -4270,7 +4291,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Branches 鈥?鐢熸垚璺緞鍒嗘敮 */}
|
||||
{/* Branches —生成路径分支 */}
|
||||
{status === "done" ? (
|
||||
<div className="clone-ai-flow-branches">
|
||||
{results[0]?.src ? (
|
||||
@@ -4333,14 +4354,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" ? (
|
||||
@@ -4350,7 +4371,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
<span>请检查网络后点击下方重试</span>
|
||||
{lastFailedActionRef.current ? (
|
||||
<button type="button" className="clone-ai-retry-btn" onClick={lastFailedActionRef.current}>
|
||||
<ReloadOutlined /> 閲嶈瘯
|
||||
<ReloadOutlined /> 重试
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
@@ -4397,7 +4418,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 + "。"
|
||||
@@ -4575,14 +4596,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
|
||||
@@ -5326,13 +5347,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">
|
||||
@@ -5494,6 +5515,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>
|
||||
@@ -5651,6 +5673,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>
|
||||
@@ -5908,6 +5931,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
|
||||
@@ -5958,7 +5995,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}
|
||||
@@ -5969,10 +6006,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 />
|
||||
<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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user