feat: localize ecommerce quick tool pages
CI / verify (pull_request) Waiting to run

This commit is contained in:
Codex
2026-06-18 16:19:59 +08:00
parent 207f05ac86
commit d7e6f03157
4 changed files with 313 additions and 43 deletions
+138 -38
View File
@@ -65,8 +65,10 @@ import {
getPlatformDefaultRatio, getPlatformDefaultRatio,
getPlatformLanguageOptions, getPlatformLanguageOptions,
getPlatformRatioOptions, getPlatformRatioOptions,
languageOptions,
marketLanguageOptions, marketLanguageOptions,
marketOptions, marketOptions,
normalizeLanguage,
normalizeLanguageForPlatform, normalizeLanguageForPlatform,
normalizeMarket, normalizeMarket,
normalizePlatform, normalizePlatform,
@@ -167,6 +169,20 @@ type SmartCutoutImageItem = { src: string; name: string; originalSrc?: string };
const ecommerceInspirationTabs = ["最近打开", "一键同款", "海报模板", "热门", "商品图", "模特穿戴"]; const ecommerceInspirationTabs = ["最近打开", "一键同款", "海报模板", "热门", "商品图", "模特穿戴"];
const ecommerceInspirationAssets = ossAssets.ecommerce.inspiration; const ecommerceInspirationAssets = ossAssets.ecommerce.inspiration;
const getMarketsForLanguage = (languageValue: string) => {
const normalizedLanguage = normalizeLanguage(languageValue);
const matches = marketLanguageOptions
.filter((option) => option.languages.some((item) => normalizeLanguage(item) === normalizedLanguage))
.map((option) => option.country);
return matches.length ? matches : marketOptions;
};
const normalizeMarketForLanguage = (marketValue: string, languageValue: string) => {
const normalizedMarket = normalizeMarket(marketValue);
const languageMarkets = getMarketsForLanguage(languageValue);
return languageMarkets.includes(normalizedMarket) ? normalizedMarket : (languageMarkets[0] ?? marketOptions[0] ?? normalizedMarket);
};
const ecommerceInspirationRows = [ const ecommerceInspirationRows = [
{ {
title: "作品记录", title: "作品记录",
@@ -333,9 +349,6 @@ interface EcommerceImagePromptOptions {
} }
const sideTools: Array<{ key: ProductKitToolKey; label: string; icon: ReactNode }> = [ const sideTools: Array<{ key: ProductKitToolKey; label: string; icon: ReactNode }> = [
{ key: "set", label: "商品套图", icon: <AppstoreOutlined /> },
{ key: "detail", label: "A+详情", icon: <FileImageOutlined /> },
{ key: "wear", label: "服饰穿搭", icon: <SkinOutlined /> },
{ key: "clone", label: "电商AI作图", icon: <AppstoreOutlined /> }, { key: "clone", label: "电商AI作图", icon: <AppstoreOutlined /> },
]; ];
@@ -1131,6 +1144,18 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const skipInitialCloneAutoSaveRef = useRef(true); const skipInitialCloneAutoSaveRef = useRef(true);
const skipNextCloneAutoSaveRef = useRef(false); const skipNextCloneAutoSaveRef = useRef(false);
const [activeTool, setActiveTool] = useState<ProductKitToolKey>("clone"); const [activeTool, setActiveTool] = useState<ProductKitToolKey>("clone");
useEffect(() => {
if (activeTool === "set") {
setActiveTool("clone");
setActiveQuickTool("quick-set");
} else if (activeTool === "detail") {
setActiveTool("clone");
setActiveQuickTool("detail");
} else if (activeTool === "wear") {
setActiveTool("clone");
setActiveQuickTool(null);
}
}, [activeTool]);
useEffect(() => { useEffect(() => {
setPreviewZoom(1); setPreviewZoom(1);
setIsCommandComposerCompact(false); setIsCommandComposerCompact(false);
@@ -1675,7 +1700,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const [detailProgress, setDetailProgress] = useState(0); const [detailProgress, setDetailProgress] = useState(0);
const [hotRequirement, setHotRequirement] = useState(""); const [hotRequirement, setHotRequirement] = useState("");
const [isHotMaterialDragging, setIsHotMaterialDragging] = useState(false); const [isHotMaterialDragging, setIsHotMaterialDragging] = useState(false);
const [hotMaterialHoverZoom, setHotMaterialHoverZoom] = useState<{ src: string; x: number; y: number; placement: "above" | "below" } | null>(null); const [hotMaterialHoverZoom, setHotMaterialHoverZoom] = useState<{ src: string; x: number; y: number; placement: "right" | "left" } | null>(null);
const [hotPlatform, setHotPlatform] = useState(platformOptions[0]); const [hotPlatform, setHotPlatform] = useState(platformOptions[0]);
const [hotMarket, setHotMarket] = useState(marketOptions[0]); const [hotMarket, setHotMarket] = useState(marketOptions[0]);
const [hotLanguage, setHotLanguage] = useState(getPlatformDefaultLanguage(platformOptions[0], marketOptions[0])); const [hotLanguage, setHotLanguage] = useState(getPlatformDefaultLanguage(platformOptions[0], marketOptions[0]));
@@ -1720,6 +1745,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
() => getPlatformLanguageOptions(hotPlatform, hotMarket), () => getPlatformLanguageOptions(hotPlatform, hotMarket),
[hotMarket, hotPlatform], [hotMarket, hotPlatform],
); );
const languageMarketOptions = languageOptions;
const cloneMarketOptions = useMemo(() => getMarketsForLanguage(language), [language]);
const detailMarketOptions = useMemo(() => getMarketsForLanguage(detailLanguage), [detailLanguage]);
const hotMarketOptions = useMemo(() => getMarketsForLanguage(hotLanguage), [hotLanguage]);
const ecommerceMentionImages: MentionImageOption[] = [ const ecommerceMentionImages: MentionImageOption[] = [
...productImages.map((image, index) => ({ ...image, label: `商品图 ${index + 1}` })), ...productImages.map((image, index) => ({ ...image, label: `商品图 ${index + 1}` })),
...cloneReferenceImages.map((image, index) => ({ ...image, label: `参考图 ${index + 1}` })), ...cloneReferenceImages.map((image, index) => ({ ...image, label: `参考图 ${index + 1}` })),
@@ -1734,6 +1763,33 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
[productImages], [productImages],
); );
const quickPageSidebarItems: Array<{ key: NonNullable<typeof activeQuickTool>; label: string; icon: ReactNode }> = [
{ key: "quick-set", label: "商品套图", icon: <AppstoreAddOutlined /> },
{ key: "detail", label: "A+详情", icon: <LayoutOutlined /> },
{ key: "hot", label: "爆款复刻", icon: <FireOutlined /> },
{ key: "oneClickVideo", label: "一键视频", icon: <PlayCircleOutlined /> },
{ key: "image-edit", label: "图片修改", icon: <HighlightOutlined /> },
{ key: "watermark", label: "去除水印", icon: <ClearOutlined /> },
{ key: "copywriting", label: "一键文案", icon: <FileTextOutlined /> },
{ key: "translate", label: "图片翻译", icon: <TranslationOutlined /> },
];
const renderQuickPageSidebar = (activeKey: NonNullable<typeof activeQuickTool>) => (
<nav className="ecom-quick-page-sidebar" aria-label="快捷工具切换">
{quickPageSidebarItems.map((item) => (
<button
key={item.key}
type="button"
className={item.key === activeKey ? "is-active" : ""}
onClick={() => setActiveQuickTool(item.key)}
>
{item.icon}
<span>{item.label}</span>
</button>
))}
</nav>
);
const selectedProductSetOutput = const selectedProductSetOutput =
productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!; productSetOutputOptions.find((option) => option.key === productSetOutput) ?? productSetOutputOptions[0]!;
const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!; const selectedCloneOutput = cloneOutputOptions.find((option) => option.key === cloneOutput) ?? cloneOutputOptions[1]!;
@@ -2125,8 +2181,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const openImageTranslatePage = () => { const openImageTranslatePage = () => {
clearSmartCutoutTransition(); clearSmartCutoutTransition();
setActiveQuickTool("translate");
setComposerMenu(null); setComposerMenu(null);
toast.info("功能正在优化中");
}; };
const closeImageTranslatePage = () => { const closeImageTranslatePage = () => {
@@ -3171,7 +3227,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setRatio((current) => setRatio((current) =>
normalizeRatioForPlatform(normalizedPlatform, current, cloneOutput), normalizeRatioForPlatform(normalizedPlatform, current, cloneOutput),
); );
setLanguage(getPlatformDefaultLanguage(normalizedPlatform, market));
}; };
const handleCloneOutputChange = (nextOutput: CloneOutputKey) => { const handleCloneOutputChange = (nextOutput: CloneOutputKey) => {
@@ -3221,10 +3276,15 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setLanguage(getPlatformDefaultLanguage(platform, normalizedMarket)); setLanguage(getPlatformDefaultLanguage(platform, normalizedMarket));
}; };
const handleCloneLanguageChange = (nextLanguage: string) => {
const normalizedLanguage = normalizeLanguage(nextLanguage);
setLanguage(normalizedLanguage);
setMarket((current) => normalizeMarketForLanguage(current, normalizedLanguage));
};
const handleDetailPlatformChange = (nextPlatform: string) => { const handleDetailPlatformChange = (nextPlatform: string) => {
const normalizedPlatform = normalizePlatform(nextPlatform); const normalizedPlatform = normalizePlatform(nextPlatform);
setDetailPlatform(normalizedPlatform); setDetailPlatform(normalizedPlatform);
setDetailLanguage(getPlatformDefaultLanguage(normalizedPlatform, detailMarket));
setDetailRatio((current) => getQuickSetRatioValue(current)); setDetailRatio((current) => getQuickSetRatioValue(current));
}; };
@@ -3234,6 +3294,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setDetailLanguage(getPlatformDefaultLanguage(detailPlatform, normalizedMarket)); setDetailLanguage(getPlatformDefaultLanguage(detailPlatform, normalizedMarket));
}; };
const handleDetailLanguageChange = (nextLanguage: string) => {
const normalizedLanguage = normalizeLanguage(nextLanguage);
setDetailLanguage(normalizedLanguage);
setDetailMarket((current) => normalizeMarketForLanguage(current, normalizedLanguage));
};
const createCloneSettingSnapshot = (name: string, id = `clone-setting-${Date.now()}`): CloneSavedSetting => ({ const createCloneSettingSnapshot = (name: string, id = `clone-setting-${Date.now()}`): CloneSavedSetting => ({
id, id,
name, name,
@@ -4378,7 +4444,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const handleHotPlatformChange = (nextPlatform: string) => { const handleHotPlatformChange = (nextPlatform: string) => {
const normalizedPlatform = normalizePlatform(nextPlatform); const normalizedPlatform = normalizePlatform(nextPlatform);
setHotPlatform(normalizedPlatform); setHotPlatform(normalizedPlatform);
setHotLanguage(getPlatformDefaultLanguage(normalizedPlatform, hotMarket));
setHotRatio((current) => getQuickSetRatioValue(current)); setHotRatio((current) => getQuickSetRatioValue(current));
}; };
@@ -4388,6 +4453,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
setHotLanguage(getPlatformDefaultLanguage(hotPlatform, normalizedMarket)); setHotLanguage(getPlatformDefaultLanguage(hotPlatform, normalizedMarket));
}; };
const handleHotLanguageChange = (nextLanguage: string) => {
const normalizedLanguage = normalizeLanguage(nextLanguage);
setHotLanguage(normalizedLanguage);
setHotMarket((current) => normalizeMarketForLanguage(current, normalizedLanguage));
};
const handleHotAiWrite = () => { const handleHotAiWrite = () => {
setHotRequirement( setHotRequirement(
"1.产品名称:便携式咖啡保温杯\n2.核心卖点:316不锈钢内胆、12小时长效保温、防漏便携、大容量\n3.参考风格:极简日系、暖光氛围、生活场景\n4.期望场景:办公桌面、户外通勤、运动健身\n5.具体参数:容量500ml、口径4.5cm、高度22cm", "1.产品名称:便携式咖啡保温杯\n2.核心卖点:316不锈钢内胆、12小时长效保温、防漏便携、大容量\n3.参考风格:极简日系、暖光氛围、生活场景\n4.期望场景:办公桌面、户外通勤、运动健身\n5.具体参数:容量500ml、口径4.5cm、高度22cm",
@@ -4503,20 +4574,19 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
const handleHotMaterialMouseEnter = (src: string, event: ReactMouseEvent<HTMLElement>) => { const handleHotMaterialMouseEnter = (src: string, event: ReactMouseEvent<HTMLElement>) => {
const rect = event.currentTarget.getBoundingClientRect(); const rect = event.currentTarget.getBoundingClientRect();
const previewHalfWidth = 150; const previewWidth = 300;
const previewHeight = 360; const previewHeight = 190;
const gap = 12; const gap = 12;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth; const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight; const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const x = Math.min( const canShowRight = rect.right + gap + previewWidth <= viewportWidth - gap;
Math.max(rect.left + rect.width / 2, previewHalfWidth + gap), const placement: "right" | "left" = canShowRight ? "right" : "left";
Math.max(previewHalfWidth + gap, viewportWidth - previewHalfWidth - gap), const x = placement === "right" ? rect.right + gap : Math.max(gap, rect.left - gap);
const y = Math.min(
Math.max(rect.top + rect.height / 2, previewHeight / 2 + gap),
Math.max(previewHeight / 2 + gap, viewportHeight - previewHeight / 2 - gap),
); );
const showAbove = rect.top > previewHeight + gap; setHotMaterialHoverZoom({ src, x, y, placement });
const y = showAbove
? rect.top - gap
: Math.min(rect.bottom + gap, viewportHeight - gap);
setHotMaterialHoverZoom({ src, x, y, placement: showAbove ? "above" : "below" });
}; };
const handleHotMaterialMouseLeave = () => setHotMaterialHoverZoom(null); const handleHotMaterialMouseLeave = () => setHotMaterialHoverZoom(null);
@@ -4540,13 +4610,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onRemove(item.id); onRemove(item.id);
}} }}
> >
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M9 6V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1" />
<path d="M5 6h14" />
<path d="M8 6l1 14h6l1-14" />
<path d="M10.5 10v6" />
<path d="M13.5 10v6" />
</svg>
</button> </button>
</figure> </figure>
))} ))}
@@ -5173,8 +5237,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onChange: (value: string) => void; onChange: (value: string) => void;
}> = [ }> = [
{ key: "platform", label: "平台", value: platform, options: platformOptions, onChange: handleClonePlatformChange }, { key: "platform", label: "平台", value: platform, options: platformOptions, onChange: handleClonePlatformChange },
{ key: "market", label: "国家", value: market, options: marketOptions, onChange: handleCloneMarketChange }, { key: "market", label: "国家", value: market, options: cloneMarketOptions, onChange: handleCloneMarketChange },
{ key: "language", label: "语种", value: language, options: cloneLanguageOptions, onChange: setLanguage }, { key: "language", label: "语种", value: language, options: languageMarketOptions, onChange: handleCloneLanguageChange },
{ key: "ratio", label: "尺寸/比例", value: ratio, options: cloneRatioOptions, onChange: setRatio }, { key: "ratio", label: "尺寸/比例", value: ratio, options: cloneRatioOptions, onChange: setRatio },
]; ];
const quickDetailBasicSelects: Array<{ const quickDetailBasicSelects: Array<{
@@ -5185,8 +5249,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onChange: (value: string) => void; onChange: (value: string) => void;
}> = [ }> = [
{ key: "platform", label: "平台", value: detailPlatform, options: platformOptions, onChange: handleDetailPlatformChange }, { key: "platform", label: "平台", value: detailPlatform, options: platformOptions, onChange: handleDetailPlatformChange },
{ key: "market", label: "国家", value: detailMarket, options: marketOptions, onChange: handleDetailMarketChange }, { key: "market", label: "国家", value: detailMarket, options: detailMarketOptions, onChange: handleDetailMarketChange },
{ key: "language", label: "语种", value: detailLanguage, options: detailLanguageOptions, onChange: setDetailLanguage }, { key: "language", label: "语种", value: detailLanguage, options: languageMarketOptions, onChange: handleDetailLanguageChange },
{ key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(detailRatio), options: quickSetRatioOptions, onChange: setDetailRatio }, { key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(detailRatio), options: quickSetRatioOptions, onChange: setDetailRatio },
]; ];
@@ -5198,8 +5262,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onChange: (value: string) => void; onChange: (value: string) => void;
}> = [ }> = [
{ key: "platform", label: "平台", value: hotPlatform, options: platformOptions, onChange: handleHotPlatformChange }, { key: "platform", label: "平台", value: hotPlatform, options: platformOptions, onChange: handleHotPlatformChange },
{ key: "market", label: "国家", value: hotMarket, options: marketOptions, onChange: handleHotMarketChange }, { key: "market", label: "国家", value: hotMarket, options: hotMarketOptions, onChange: handleHotMarketChange },
{ key: "language", label: "语种", value: hotLanguage, options: hotLanguageOptions, onChange: setHotLanguage }, { key: "language", label: "语种", value: hotLanguage, options: languageMarketOptions, onChange: handleHotLanguageChange },
{ key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(hotRatio), options: quickSetRatioOptions, onChange: setHotRatio }, { key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(hotRatio), options: quickSetRatioOptions, onChange: setHotRatio },
]; ];
@@ -5211,8 +5275,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
onChange: (value: string) => void; onChange: (value: string) => void;
}> = [ }> = [
{ key: "platform", label: "平台", value: platform, options: platformOptions, onChange: setPlatform }, { key: "platform", label: "平台", value: platform, options: platformOptions, onChange: setPlatform },
{ key: "market", label: "国家", value: market, options: marketOptions, onChange: setMarket }, { key: "market", label: "国家", value: market, options: cloneMarketOptions, onChange: handleCloneMarketChange },
{ key: "language", label: "语种", value: language, options: cloneLanguageOptions, onChange: setLanguage }, { key: "language", label: "语种", value: language, options: languageMarketOptions, onChange: handleCloneLanguageChange },
{ key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(ratio), options: quickSetRatioOptions, onChange: setRatio }, { key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(ratio), options: quickSetRatioOptions, onChange: setRatio },
]; ];
@@ -7003,6 +7067,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
</aside> </aside>
<section className="ecom-image-workbench-stage"> <section className="ecom-image-workbench-stage">
<header className="ecom-visual-workspace-head ecom-copywriting-preview-head">
<h1></h1>
<p><span>AI</span> </p>
</header>
{!imageWorkbenchImage ? ( {!imageWorkbenchImage ? (
<div <div
className={`ecom-watermark-dropzone${isImageWorkbenchDragging ? " is-dragging" : ""}`} className={`ecom-watermark-dropzone${isImageWorkbenchDragging ? " is-dragging" : ""}`}
@@ -7261,6 +7329,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
</aside> </aside>
<section className="ecom-watermark-workspace"> <section className="ecom-watermark-workspace">
<header className="ecom-visual-workspace-head ecom-copywriting-preview-head">
<h1></h1>
<p><span>AI</span> </p>
</header>
{!translateImage ? ( {!translateImage ? (
<div <div
className={`ecom-watermark-dropzone${isTranslateDragging ? " is-dragging" : ""}`} className={`ecom-watermark-dropzone${isTranslateDragging ? " is-dragging" : ""}`}
@@ -8237,35 +8309,63 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
? tryOnPreview ? tryOnPreview
: isCloneTool : isCloneTool
? isWatermarkTool ? isWatermarkTool
? watermarkPreview ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("watermark")}
{watermarkPreview}
</div>
)
: isTranslateTool : isTranslateTool
? translatePreview ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("translate")}
{translatePreview}
</div>
)
: isImageEditTool : isImageEditTool
? imageWorkbenchPreview ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("image-edit")}
{imageWorkbenchPreview}
</div>
)
: isSmartCutoutTool : isSmartCutoutTool
? smartCutoutPreview ? smartCutoutPreview
: isQuickDetailTool : isQuickDetailTool
? ( ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter"> <div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("detail")}
{quickDetailPreview} {quickDetailPreview}
</div> </div>
) )
: isHotCloneTool : isHotCloneTool
? ( ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter"> <div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("hot")}
{hotClonePreview} {hotClonePreview}
</div> </div>
) )
: isQuickSetTool : isQuickSetTool
? ( ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter"> <div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("quick-set")}
{quickSetGenPreview} {quickSetGenPreview}
</div> </div>
) )
: isCopywritingTool : isCopywritingTool
? copywritingPreview ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("copywriting")}
{copywritingPreview}
</div>
)
: isOneClickVideoTool : isOneClickVideoTool
? oneClickVideoPreview ? (
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
{renderQuickPageSidebar("oneClickVideo")}
{oneClickVideoPreview}
</div>
)
: clonePreview : clonePreview
: placeholderPreview; : placeholderPreview;
const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isQuickSetTool && !isCopywritingTool && !isOneClickVideoTool; const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isQuickSetTool && !isCopywritingTool && !isOneClickVideoTool;
@@ -4,7 +4,8 @@ import {
ThunderboltOutlined, ThunderboltOutlined,
VideoCameraOutlined, VideoCameraOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useMemo, useRef, useState, type ChangeEvent, type DragEvent, type KeyboardEvent, type RefObject } from "react"; import { useMemo, useRef, useState, type ChangeEvent, type DragEvent, type KeyboardEvent, type MouseEvent as ReactMouseEvent, type RefObject } from "react";
import { createPortal } from "react-dom";
import EcommerceVideoWorkspace from "../EcommerceVideoWorkspace"; import EcommerceVideoWorkspace from "../EcommerceVideoWorkspace";
interface CloneImageItem { interface CloneImageItem {
@@ -97,6 +98,7 @@ export default function EcommerceOneClickVideoPanel({
}: EcommerceOneClickVideoPanelProps) { }: EcommerceOneClickVideoPanelProps) {
const [openSelect, setOpenSelect] = useState<"platform" | "ratio" | null>(null); const [openSelect, setOpenSelect] = useState<"platform" | "ratio" | null>(null);
const [planTrigger, setPlanTrigger] = useState(0); const [planTrigger, setPlanTrigger] = useState(0);
const [hoverZoom, setHoverZoom] = useState<{ src: string; x: number; y: number; placement: "right" | "left" } | null>(null);
const selectAnchorRef = useRef<HTMLDivElement>(null); const selectAnchorRef = useRef<HTMLDivElement>(null);
const productImageDataUrls = useMemo(() => productImages.map((img) => img.src), [productImages]); const productImageDataUrls = useMemo(() => productImages.map((img) => img.src), [productImages]);
@@ -126,19 +128,40 @@ export default function EcommerceOneClickVideoPanel({
setOpenSelect((current) => (current === key ? null : key)); setOpenSelect((current) => (current === key ? null : key));
}; };
const handleThumbMouseEnter = (src: string, event: ReactMouseEvent<HTMLElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
const previewWidth = 300;
const previewHeight = 190;
const gap = 12;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const canShowRight = rect.right + gap + previewWidth <= viewportWidth - gap;
const placement: "right" | "left" = canShowRight ? "right" : "left";
const x = placement === "right" ? rect.right + gap : Math.max(gap, rect.left - gap);
const y = Math.min(
Math.max(rect.top + rect.height / 2, previewHeight / 2 + gap),
Math.max(previewHeight / 2 + gap, viewportHeight - previewHeight / 2 - gap),
);
setHoverZoom({ src, x, y, placement });
};
const renderThumbs = () => ( const renderThumbs = () => (
<div className="ecom-quick-upload-thumbs" aria-label="已上传商品原图"> <div className="ecom-quick-upload-thumbs" aria-label="已上传商品原图">
{productImages.map((item) => ( {productImages.map((item) => (
<figure key={item.id} className="ecom-command-asset-thumb ecom-quick-upload-thumb"> <figure
key={item.id}
className="ecom-command-asset-thumb ecom-quick-upload-thumb"
onMouseEnter={(event) => handleThumbMouseEnter(item.src, event)}
onMouseLeave={() => setHoverZoom(null)}
>
<img src={item.src} alt={item.name} /> <img src={item.src} alt={item.name} />
<span className="ecom-command-asset-zoom" aria-hidden="true">
<img src={item.src} alt="" />
</span>
<button <button
type="button" type="button"
className="ecom-hot-material-delete"
aria-label="删除图片" aria-label="删除图片"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
setHoverZoom(null);
removeProductImage(item.id); removeProductImage(item.id);
}} }}
> >
@@ -386,6 +409,17 @@ export default function EcommerceOneClickVideoPanel({
</button> </button>
</div> </div>
</aside> </aside>
{hoverZoom && typeof document !== "undefined"
? createPortal(
<div
className={`ecom-hot-material-zoom-portal is-${hoverZoom.placement}`}
style={{ left: hoverZoom.x, top: hoverZoom.y }}
>
<img src={hoverZoom.src} alt="" />
</div>,
document.body,
)
: null}
<section className="ecom-quick-set-stage"> <section className="ecom-quick-set-stage">
<EcommerceVideoWorkspace <EcommerceVideoWorkspace
@@ -155,6 +155,10 @@ export default function WatermarkToolPage({
</aside> </aside>
<section className="ecom-watermark-workspace"> <section className="ecom-watermark-workspace">
<header className="ecom-visual-workspace-head ecom-copywriting-preview-head">
<h1></h1>
<p><span>AI</span> </p>
</header>
{!image ? ( {!image ? (
<div <div
className={`ecom-watermark-dropzone${isDragging ? " is-dragging" : ""}`} className={`ecom-watermark-dropzone${isDragging ? " is-dragging" : ""}`}
+132
View File
@@ -20993,3 +20993,135 @@ html body #root .ecommerce-standalone.ecommerce-standalone .product-clone-page[d
top: -10px !important; top: -10px !important;
} }
} }
/* Responsive coverage for the recently localized quick/visual tool pages. */
.ecom-hot-material-zoom-portal.is-right {
transform: translateY(-50%) !important;
}
.ecom-hot-material-zoom-portal.is-left {
transform: translate(-100%, -50%) !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-hot-material-zoom-portal.is-above,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-hot-material-zoom-portal.is-below {
transform: translateY(-50%) !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-quick-hot-material.has-images .ecom-command-asset-thumb.ecom-quick-upload-thumb > button.ecom-hot-material-delete {
position: absolute !important;
top: -8px !important;
right: -8px !important;
z-index: 20 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
min-height: 24px !important;
padding: 0 !important;
border: 1px solid rgba(239, 68, 68, 0.62) !important;
border-radius: 999px !important;
color: #ef4444 !important;
background: #ffffff !important;
box-shadow: 0 8px 18px rgba(239, 68, 68, 0.16) !important;
font-size: 16px !important;
font-weight: 700 !important;
line-height: 1 !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-quick-hot-material.has-images .ecom-command-asset-thumb.ecom-quick-upload-thumb > button.ecom-hot-material-delete:hover {
border-color: #dc2626 !important;
color: #dc2626 !important;
background: #fff1f2 !important;
box-shadow: 0 10px 22px rgba(220, 38, 38, 0.24) !important;
transform: scale(1.04) !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-quick-hot-material.has-images .ecom-command-asset-thumb.ecom-quick-upload-thumb > button.ecom-hot-material-delete svg {
display: none !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-quick-hot-add-btn,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-quick-hot-add-btn:hover {
color: #1073cc !important;
background: #ffffff !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-visual-workspace-head.ecom-copywriting-preview-head {
display: grid !important;
gap: 6px !important;
margin-bottom: 16px !important;
padding: 0 !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-visual-workspace-head.ecom-copywriting-preview-head h1 {
margin: 0 !important;
color: #172636 !important;
font-size: 21px !important;
font-weight: 950 !important;
line-height: 1.25 !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-visual-workspace-head.ecom-copywriting-preview-head p {
margin: 0 !important;
color: #657686 !important;
font-size: 13px !important;
font-weight: 750 !important;
line-height: 1.5 !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-visual-workspace-head.ecom-copywriting-preview-head p span {
color: #1073cc !important;
font-weight: 800 !important;
}
@media (max-width: 960px) {
.ecommerce-standalone .ecom-quick-page-wrap {
flex-direction: column !important;
overflow: hidden !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar {
flex: 0 0 auto !important;
width: 100% !important;
min-height: 68px !important;
flex-direction: row !important;
justify-content: flex-start !important;
gap: 6px !important;
padding: 8px 10px !important;
border-right: 0 !important;
border-bottom: 1px solid rgba(30, 189, 219, 0.1) !important;
overflow-x: auto !important;
}
.ecommerce-standalone .ecom-quick-page-sidebar button {
flex: 0 0 76px !important;
width: 76px !important;
min-height: 52px !important;
padding: 7px 6px !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-quick-set-body,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-copywriting-body,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-image-workbench-page,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-watermark-page {
grid-template-columns: 1fr !important;
grid-template-rows: auto minmax(0, 1fr) !important;
}
.ecommerce-standalone .ecom-quick-page-wrap .ecom-quick-set-panel,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-copywriting-panel,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-image-workbench-side,
.ecommerce-standalone .ecom-quick-page-wrap .ecom-watermark-side {
max-height: 46vh !important;
overflow-y: auto !important;
}
}
@media (max-width: 640px), (hover: none) {
.ecom-hot-material-zoom-portal {
display: none !important;
}
}