Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb7b769155 | |||
| 0e24ccf7b1 | |||
| f8ccad52f9 | |||
| 57cf34b0d0 | |||
| ad38a4a0e3 | |||
| c7adbc153b | |||
| 17152efa2c | |||
| a605fad7e0 | |||
| 30222cd830 | |||
| 4ca2ab4a9c | |||
| 588da45902 | |||
| 5466036349 | |||
| 9869c0c5e6 | |||
| c38f056527 |
+35
-108
@@ -4,19 +4,16 @@ import {
|
||||
CheckCircleFilled,
|
||||
CloseOutlined,
|
||||
HomeOutlined,
|
||||
IdcardOutlined,
|
||||
LockOutlined,
|
||||
LoadingOutlined,
|
||||
LoginOutlined,
|
||||
LogoutOutlined,
|
||||
MailOutlined,
|
||||
MobileOutlined,
|
||||
PictureOutlined,
|
||||
SafetyOutlined,
|
||||
UserOutlined,
|
||||
VideoCameraOutlined,
|
||||
WalletOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { LocalAvatar } from "./components/LocalAvatar";
|
||||
import { Topbar } from "./components/Topbar";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import ToastContainer from "./components/toast/ToastContainer";
|
||||
import { toast } from "./components/toast/toastStore";
|
||||
@@ -40,6 +37,9 @@ const EcommercePage = lazy(() => import("./features/ecommerce/EcommercePage"));
|
||||
|
||||
type AuthMode = "login" | "register";
|
||||
type AuthMethod = "account" | "email" | "phone";
|
||||
type WorkspaceChromeState = {
|
||||
isToolPage: boolean;
|
||||
};
|
||||
|
||||
interface LocalProfilePageProps {
|
||||
session: WebUserSession;
|
||||
@@ -51,17 +51,6 @@ interface LocalProfilePageProps {
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
function LocalAvatar({ session, size = "md" }: { session: WebUserSession; size?: "sm" | "md" | "lg" }) {
|
||||
const displayName = session.user.displayName || session.user.username || "用户";
|
||||
const label = displayName.trim().slice(0, 1).toUpperCase() || "用";
|
||||
const avatarUrl = session.user.avatarUrl;
|
||||
return (
|
||||
<span className={`local-user-avatar local-user-avatar--${size}`}>
|
||||
{avatarUrl ? <img src={avatarUrl} alt={displayName} /> : <span>{label}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function LocalProfilePage({ session, balance, imageCount, videoCount, onBack, onBugFeedback, onLogout }: LocalProfilePageProps) {
|
||||
const displayName = session.user.displayName || session.user.username || "用户";
|
||||
const workCount = Math.max(imageCount + videoCount, 0);
|
||||
@@ -166,6 +155,9 @@ function App() {
|
||||
const [sessionNotice, setSessionNotice] = useState<string | null>(null);
|
||||
const [profileMenuOpen, setProfileMenuOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState<"workspace" | "profile">("workspace");
|
||||
const [workspaceChrome, setWorkspaceChrome] = useState<WorkspaceChromeState>({
|
||||
isToolPage: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void loadDarkGreenTheme();
|
||||
@@ -318,20 +310,6 @@ function App() {
|
||||
};
|
||||
|
||||
const balance = Math.max(usage.balanceCents, 0) / 100;
|
||||
const displayName = session?.user.displayName || session?.user.username || "用户";
|
||||
const actualWorkCount = Math.max(usage.imageUsed + usage.videoUsed, 0);
|
||||
const shownWorkCount = actualWorkCount;
|
||||
|
||||
const avatarMenuStats = useMemo(
|
||||
() => [
|
||||
{ icon: <IdcardOutlined />, label: "UID", value: session?.user.id ?? "-" },
|
||||
{ icon: <WalletOutlined />, label: "积分", value: `${balance.toFixed(2)} 积分` },
|
||||
{ icon: <PictureOutlined />, label: "图片", value: usage.imageUsed },
|
||||
{ icon: <VideoCameraOutlined />, label: "视频", value: usage.videoUsed },
|
||||
{ icon: <PictureOutlined />, label: "作品", value: shownWorkCount },
|
||||
],
|
||||
[balance, session?.user.id, shownWorkCount, usage.imageUsed, usage.videoUsed],
|
||||
);
|
||||
|
||||
const handleOpenProfile = () => {
|
||||
setProfileMenuOpen(false);
|
||||
@@ -349,86 +327,31 @@ function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ecommerce-standalone web-shell" data-theme="dark" data-ui-theme="dark-green" data-view="ecommerce">
|
||||
<header className="ecommerce-standalone__topbar">
|
||||
<button type="button" className="ecommerce-standalone__brand" onClick={handleOpenWorkspace}>
|
||||
<span className="ecommerce-standalone__logo" aria-hidden="true">
|
||||
<img src="https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png" alt="" />
|
||||
</span>
|
||||
<strong>OmniAI 电商智能体</strong>
|
||||
</button>
|
||||
<div className="ecommerce-standalone__account">
|
||||
{session ? (
|
||||
<div className="ecommerce-profile-menu">
|
||||
<span className="ecommerce-standalone__credits">
|
||||
{(Math.max(usage.balanceCents, 0) / 100).toFixed(2)} 积分
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ecommerce-profile-menu__trigger"
|
||||
onClick={() => setProfileMenuOpen((open) => !open)}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={profileMenuOpen}
|
||||
>
|
||||
<LocalAvatar session={session} size="sm" />
|
||||
<span>{displayName}</span>
|
||||
</button>
|
||||
{profileMenuOpen ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="ecommerce-profile-popover__backdrop"
|
||||
aria-label="关闭账户信息"
|
||||
onClick={() => setProfileMenuOpen(false)}
|
||||
/>
|
||||
<section className="ecommerce-profile-popover" role="dialog" aria-label="账户信息">
|
||||
<div className="ecommerce-profile-popover__head">
|
||||
<LocalAvatar session={session} size="md" />
|
||||
<div>
|
||||
<strong>{displayName}</strong>
|
||||
<span>{session.user.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl className="ecommerce-profile-popover__stats">
|
||||
{avatarMenuStats.map((item) => (
|
||||
<div key={item.label}>
|
||||
<dt>{item.icon}{item.label}</dt>
|
||||
<dd>{item.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="ecommerce-profile-popover__actions">
|
||||
<button type="button" className="is-primary" onClick={handleOpenProfile}>
|
||||
<UserOutlined />
|
||||
个人中心
|
||||
</button>
|
||||
<button type="button" onClick={handleBugFeedback}>
|
||||
<BugOutlined />
|
||||
Bug 反馈
|
||||
</button>
|
||||
<button type="button" className="is-danger" onClick={handleLogout}>
|
||||
<LogoutOutlined />
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<button type="button" onClick={() => openAuth("login")}>
|
||||
<LoginOutlined />
|
||||
<span>登录 / 注册</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
className="ecommerce-standalone web-shell"
|
||||
data-theme="dark"
|
||||
data-ui-theme="dark-green"
|
||||
data-view="ecommerce"
|
||||
data-workspace-tool-page={workspaceChrome.isToolPage ? "true" : "false"}
|
||||
>
|
||||
<Topbar
|
||||
session={session}
|
||||
usage={usage}
|
||||
profileMenuOpen={profileMenuOpen}
|
||||
onProfileMenuOpenChange={setProfileMenuOpen}
|
||||
onOpenWorkspace={handleOpenWorkspace}
|
||||
onOpenProfile={handleOpenProfile}
|
||||
onOpenAuth={openAuth}
|
||||
onLogout={handleLogout}
|
||||
onBugFeedback={handleBugFeedback}
|
||||
/>
|
||||
|
||||
<main className="ecommerce-standalone__content">
|
||||
{session ? (
|
||||
<div className="ecommerce-standalone__page" hidden={currentPage !== "profile"}>
|
||||
<div
|
||||
className="ecommerce-standalone__page ecommerce-standalone__page--profile"
|
||||
hidden={currentPage !== "profile"}
|
||||
>
|
||||
<LocalProfilePage
|
||||
session={session}
|
||||
balance={balance}
|
||||
@@ -442,7 +365,10 @@ function App() {
|
||||
) : null}
|
||||
{/* 工作台常驻挂载,仅用 hidden 切换。切到个人中心时不卸载,
|
||||
生成任务、进度动画、已上传图片等本地状态全部保留,切回即继续。 */}
|
||||
<div className="ecommerce-standalone__page" hidden={Boolean(session) && currentPage === "profile"}>
|
||||
<div
|
||||
className="ecommerce-standalone__page ecommerce-standalone__page--workspace"
|
||||
hidden={Boolean(session) && currentPage === "profile"}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<Suspense
|
||||
fallback={
|
||||
@@ -455,6 +381,7 @@ function App() {
|
||||
<EcommercePage
|
||||
projects={[]}
|
||||
isAuthenticated={Boolean(session)}
|
||||
onWorkspaceChromeChange={setWorkspaceChrome}
|
||||
onStartCreate={() => undefined}
|
||||
onOpenProject={() => undefined}
|
||||
onDeleteProject={() => undefined}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { WebUserSession } from "../types";
|
||||
|
||||
interface LocalAvatarProps {
|
||||
session: WebUserSession;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export function LocalAvatar({ session, size = "md" }: LocalAvatarProps) {
|
||||
const displayName = session.user.displayName || session.user.username || "用户";
|
||||
const label = displayName.trim().slice(0, 1).toUpperCase() || "用";
|
||||
const avatarUrl = session.user.avatarUrl;
|
||||
return (
|
||||
<span className={`local-user-avatar local-user-avatar--${size}`}>
|
||||
{avatarUrl ? <img src={avatarUrl} alt={displayName} /> : <span>{label}</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
BugOutlined,
|
||||
IdcardOutlined,
|
||||
LoginOutlined,
|
||||
LogoutOutlined,
|
||||
PictureOutlined,
|
||||
UserOutlined,
|
||||
VideoCameraOutlined,
|
||||
WalletOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { LocalAvatar } from "./LocalAvatar";
|
||||
import type { WebUserSession } from "../types";
|
||||
|
||||
interface TopbarProps {
|
||||
session: WebUserSession | null;
|
||||
usage: { balanceCents: number; imageUsed: number; videoUsed: number };
|
||||
profileMenuOpen: boolean;
|
||||
onProfileMenuOpenChange: (open: boolean) => void;
|
||||
onOpenWorkspace: () => void;
|
||||
onOpenProfile: () => void;
|
||||
onOpenAuth: (mode: "login" | "register") => void;
|
||||
onLogout: () => void;
|
||||
onBugFeedback: () => void;
|
||||
}
|
||||
|
||||
export function Topbar({
|
||||
session,
|
||||
usage,
|
||||
profileMenuOpen,
|
||||
onProfileMenuOpenChange,
|
||||
onOpenWorkspace,
|
||||
onOpenProfile,
|
||||
onOpenAuth,
|
||||
onLogout,
|
||||
onBugFeedback,
|
||||
}: TopbarProps) {
|
||||
const [isTopbarHidden, setIsTopbarHidden] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let restoreTimer: number | undefined;
|
||||
|
||||
const handleScroll = (event: Event) => {
|
||||
if (profileMenuOpen) return;
|
||||
const target = event.target;
|
||||
const activeWorkspace = document.querySelector<HTMLElement>(".ecommerce-standalone__page--workspace:not([hidden])");
|
||||
if (!activeWorkspace) return;
|
||||
const isWorkspacePreviewScroll =
|
||||
target instanceof HTMLElement && target.classList.contains("clone-ai-preview") && activeWorkspace.contains(target);
|
||||
const isPageScroll =
|
||||
target === document ||
|
||||
target === document.scrollingElement ||
|
||||
target === document.documentElement ||
|
||||
target === document.body;
|
||||
if (!isWorkspacePreviewScroll && !isPageScroll) return;
|
||||
|
||||
setIsTopbarHidden(true);
|
||||
if (restoreTimer) window.clearTimeout(restoreTimer);
|
||||
restoreTimer = window.setTimeout(() => {
|
||||
setIsTopbarHidden(false);
|
||||
}, 240);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { capture: true, passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll, { capture: true });
|
||||
if (restoreTimer) window.clearTimeout(restoreTimer);
|
||||
};
|
||||
}, [profileMenuOpen]);
|
||||
|
||||
const balance = Math.max(usage.balanceCents, 0) / 100;
|
||||
const displayName = session?.user.displayName || session?.user.username || "用户";
|
||||
const actualWorkCount = Math.max(usage.imageUsed + usage.videoUsed, 0);
|
||||
const shownWorkCount = actualWorkCount;
|
||||
|
||||
const avatarMenuStats = useMemo(
|
||||
() => [
|
||||
{ icon: <IdcardOutlined />, label: "UID", value: session?.user.id ?? "-" },
|
||||
{ icon: <WalletOutlined />, label: "积分", value: `${balance.toFixed(2)} 积分` },
|
||||
{ icon: <PictureOutlined />, label: "图片", value: usage.imageUsed },
|
||||
{ icon: <VideoCameraOutlined />, label: "视频", value: usage.videoUsed },
|
||||
{ icon: <PictureOutlined />, label: "作品", value: shownWorkCount },
|
||||
],
|
||||
[balance, session?.user.id, shownWorkCount, usage.imageUsed, usage.videoUsed],
|
||||
);
|
||||
|
||||
return (
|
||||
<header
|
||||
className="ecommerce-standalone__topbar"
|
||||
data-scroll-hidden={isTopbarHidden ? "true" : "false"}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
pointerEvents: "none",
|
||||
background: "transparent",
|
||||
border: 0,
|
||||
boxShadow: "none",
|
||||
backdropFilter: "none",
|
||||
WebkitBackdropFilter: "none",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="ecommerce-standalone__brand"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
onClick={onOpenWorkspace}
|
||||
>
|
||||
<span className="ecommerce-standalone__logo" aria-hidden="true">
|
||||
<img src="https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png" alt="" />
|
||||
</span>
|
||||
<strong>OmniAI 电商智能体</strong>
|
||||
</button>
|
||||
<div className="ecommerce-standalone__account">
|
||||
{session ? (
|
||||
<div className="ecommerce-profile-menu">
|
||||
<button
|
||||
type="button"
|
||||
className="ecommerce-profile-menu__trigger"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
onClick={() => onProfileMenuOpenChange(!profileMenuOpen)}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={profileMenuOpen}
|
||||
>
|
||||
<span className="ecommerce-standalone__credits">
|
||||
{(Math.max(usage.balanceCents, 0) / 100).toFixed(2)} 积分
|
||||
</span>
|
||||
<LocalAvatar session={session} size="sm" />
|
||||
<span className="ecommerce-profile-menu__name">{displayName}</span>
|
||||
</button>
|
||||
{profileMenuOpen ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="ecommerce-profile-popover__backdrop"
|
||||
aria-label="关闭账户信息"
|
||||
onClick={() => onProfileMenuOpenChange(false)}
|
||||
/>
|
||||
<section className="ecommerce-profile-popover" role="dialog" aria-label="账户信息">
|
||||
<div className="ecommerce-profile-popover__head">
|
||||
<LocalAvatar session={session} size="md" />
|
||||
<div>
|
||||
<strong>{displayName}</strong>
|
||||
<span>{session.user.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl className="ecommerce-profile-popover__stats">
|
||||
{avatarMenuStats.map((item) => (
|
||||
<div key={item.label}>
|
||||
<dt>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</dt>
|
||||
<dd>{item.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="ecommerce-profile-popover__actions">
|
||||
<button type="button" className="is-primary" onClick={onOpenProfile}>
|
||||
<UserOutlined />
|
||||
个人中心
|
||||
</button>
|
||||
<button type="button" onClick={onBugFeedback}>
|
||||
<BugOutlined />
|
||||
Bug 反馈
|
||||
</button>
|
||||
<button type="button" className="is-danger" onClick={onLogout}>
|
||||
<LogoutOutlined />
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="ecommerce-standalone__login-button"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
onClick={() => onOpenAuth("login")}
|
||||
>
|
||||
<LoginOutlined />
|
||||
<span>登录 / 注册</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -39,8 +39,10 @@ import EcommerceDetailPanel from "./panels/EcommerceDetailPanel";
|
||||
import EcommerceSetPanel from "./panels/EcommerceSetPanel";
|
||||
import EcommerceTryOnPanel from "./panels/EcommerceTryOnPanel";
|
||||
import EcommerceClonePanel from "./panels/EcommerceClonePanel";
|
||||
import EcommerceCopywritingPanel from "./panels/EcommerceCopywritingPanel";
|
||||
import { ecommerceOssScopes, saveUnifiedEcommerceGenerationRecord, deleteEcommerceGenerationRecord } from "./ecommerceGenerationPersistence";
|
||||
import { downloadResultAsset } from "../workbench/workbenchDownload";
|
||||
import type { CloneOutputKey, ProductSetOutputKey } from "./utils/platformRules";
|
||||
|
||||
const smartCutoutColorPresets = [
|
||||
"#ffffff",
|
||||
@@ -270,8 +272,6 @@ interface ProductClonePageProps {
|
||||
}
|
||||
|
||||
type ProductCloneStatus = "idle" | "ready" | "generating" | "done" | "failed";
|
||||
type ProductSetOutputKey = "set" | "detail" | "model" | "video";
|
||||
type CloneOutputKey = ProductSetOutputKey | "hot";
|
||||
type CommerceScenarioKey = "popular" | "poster" | "mainImage" | "scene" | "festival" | "model" | "background" | "retouch" | "salesVideo";
|
||||
type CloneSetCountKey = "selling" | "white" | "scene";
|
||||
type CloneModelPanelTab = "scene" | "model";
|
||||
@@ -1712,7 +1712,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [selectedProductSetPreview, setSelectedProductSetPreview] = useState<ProductSetPreviewSelection | null>(null);
|
||||
const [showHostingModal, setShowHostingModal] = useState(false);
|
||||
const [productImages, setProductImages] = useState<CloneImageItem[]>([]);
|
||||
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | "translate" | "hot" | null>(null);
|
||||
const [activeQuickTool, setActiveQuickTool] = useState<"cutout" | "detail" | "watermark" | "image-edit" | "translate" | "hot" | "quick-set" | "copywriting" | null>(null);
|
||||
const [smartCutoutImage, setSmartCutoutImage] = useState<SmartCutoutImageItem | null>(null);
|
||||
const [smartCutoutBatchImages, setSmartCutoutBatchImages] = useState<SmartCutoutImageItem[]>([]);
|
||||
const [smartCutoutBackgroundColor, setSmartCutoutBackgroundColor] = useState("#ffffff");
|
||||
@@ -1786,6 +1786,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const [cloneVideoSmart, setCloneVideoSmart] = useState(true);
|
||||
const [isCloneSettingsCollapsed, setIsCloneSettingsCollapsed] = useState(false);
|
||||
const [isCloneConversationCollapsed, setIsCloneConversationCollapsed] = useState(false);
|
||||
const [quickSetStatus, setQuickSetStatus] = useState<"idle" | "generating" | "done" | "failed">("idle");
|
||||
const [quickSetResultUrls, setQuickSetResultUrls] = useState<string[]>([]);
|
||||
const [quickSetProgress, setQuickSetProgress] = useState(0);
|
||||
const [quickSetRequirement, setQuickSetRequirement] = useState("");
|
||||
const quickSetProgressRef = useRef<number | null>(null);
|
||||
const [previewZoom, setPreviewZoom] = useState(1);
|
||||
const quickSetSelectTimerRef = useRef<number | null>(null);
|
||||
const openQuickSetSelectRef = useRef<CloneBasicSelectKey | null>(null);
|
||||
@@ -2271,6 +2276,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const canGenerateTryOn = garmentImages.length > 0 && tryOnStatus !== "generating" && tryOnStatus !== "modeling";
|
||||
const canGenerateDetail = detailProductImages.length > 0 && detailStatus !== "generating";
|
||||
const canGenerateHot = cloneReferenceImages.length > 0 && hotStatus !== "generating";
|
||||
const canGenerateQuickSet = productImages.length > 0 && quickSetStatus !== "generating";
|
||||
const cloneVideoDurationProgress =
|
||||
((cloneVideoDuration - cloneVideoDurationMin) / (cloneVideoDurationMax - cloneVideoDurationMin)) * 100;
|
||||
const cloneVideoDurationStyle: CSSProperties = useMemo(
|
||||
@@ -4608,6 +4614,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleQuickSetAiWrite = () => {
|
||||
setQuickSetRequirement(
|
||||
"1.产品名称:无线降噪蓝牙耳机\n2.核心卖点:主动降噪、24H续航、低延迟连接、舒适佩戴\n3.适用人群:通勤、办公、运动和旅行用户\n4.期望场景:地铁通勤、居家办公、户外运动\n5.具体参数:蓝牙5.3、IPX4防水、快充10分钟使用2小时",
|
||||
);
|
||||
};
|
||||
|
||||
const stopHotProgress = () => {
|
||||
if (hotProgressRef.current !== null) {
|
||||
window.clearInterval(hotProgressRef.current);
|
||||
@@ -4615,6 +4627,27 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const stopQuickSetProgress = () => {
|
||||
if (quickSetProgressRef.current !== null) {
|
||||
window.clearInterval(quickSetProgressRef.current);
|
||||
quickSetProgressRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startQuickSetProgress = () => {
|
||||
stopQuickSetProgress();
|
||||
setQuickSetProgress(0);
|
||||
quickSetProgressRef.current = window.setInterval(() => {
|
||||
setQuickSetProgress((prev) => {
|
||||
if (prev >= 90) {
|
||||
stopQuickSetProgress();
|
||||
return 90;
|
||||
}
|
||||
return prev + (90 - prev) * 0.06;
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const startHotProgress = () => {
|
||||
stopHotProgress();
|
||||
setHotProgress(0);
|
||||
@@ -4652,6 +4685,42 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
);
|
||||
};
|
||||
|
||||
const handleQuickSetGenerate = () => {
|
||||
if (!canGenerateQuickSet) return;
|
||||
imageAbortRef.current = { current: false };
|
||||
lastFailedActionRef.current = null;
|
||||
startQuickSetProgress();
|
||||
setQuickSetStatus("generating");
|
||||
void generateSetImages(
|
||||
productImages, cloneSetCounts, quickSetRequirement,
|
||||
platform, ratio, language, market,
|
||||
(s) => {
|
||||
setQuickSetStatus(s as ProductCloneStatus);
|
||||
if (s === "done") {
|
||||
stopQuickSetProgress();
|
||||
setQuickSetProgress(100);
|
||||
} else if (s === "failed") {
|
||||
stopQuickSetProgress();
|
||||
setQuickSetProgress(0);
|
||||
}
|
||||
},
|
||||
(urls) => {
|
||||
setQuickSetResultUrls(urls);
|
||||
const validUrls = urls.filter(Boolean);
|
||||
if (validUrls.length) {
|
||||
setQuickSetStatus("done");
|
||||
stopQuickSetProgress();
|
||||
setQuickSetProgress(100);
|
||||
} else {
|
||||
setQuickSetStatus("failed");
|
||||
stopQuickSetProgress();
|
||||
setQuickSetProgress(0);
|
||||
}
|
||||
},
|
||||
);
|
||||
lastFailedActionRef.current = () => handleQuickSetGenerate();
|
||||
};
|
||||
|
||||
const handleHotMaterialMouseEnter = (src: string, event: ReactMouseEvent<HTMLElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const previewHalfWidth = 150;
|
||||
@@ -4716,6 +4785,37 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
setComposerMenu(null);
|
||||
};
|
||||
|
||||
const openQuickSetPage = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setActiveQuickTool("quick-set");
|
||||
setComposerMenu(null);
|
||||
setIsCloneSettingsCollapsed(false);
|
||||
setIsQuickPanelCollapsed(false);
|
||||
};
|
||||
|
||||
const closeQuickSetPage = () => {
|
||||
stopQuickSetProgress();
|
||||
setActiveQuickTool(null);
|
||||
setQuickSetStatus("idle");
|
||||
setQuickSetResultUrls([]);
|
||||
setQuickSetProgress(0);
|
||||
setQuickSetRequirement("");
|
||||
setComposerMenu(null);
|
||||
};
|
||||
|
||||
const openCopywritingPage = () => {
|
||||
clearSmartCutoutTransition();
|
||||
setActiveQuickTool("copywriting");
|
||||
setComposerMenu(null);
|
||||
setIsCloneSettingsCollapsed(false);
|
||||
setIsQuickPanelCollapsed(false);
|
||||
};
|
||||
|
||||
const closeCopywritingPage = () => {
|
||||
setActiveQuickTool(null);
|
||||
setComposerMenu(null);
|
||||
};
|
||||
|
||||
const resetTask = () => {
|
||||
setSetImages([]);
|
||||
setProductSetRequirement("");
|
||||
@@ -4779,6 +4879,8 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
const isImageEditTool = isCloneTool && activeQuickTool === "image-edit";
|
||||
const isTranslateTool = isCloneTool && activeQuickTool === "translate";
|
||||
const isHotCloneTool = isCloneTool && activeQuickTool === "hot";
|
||||
const isQuickSetTool = isCloneTool && activeQuickTool === "quick-set";
|
||||
const isCopywritingTool = isCloneTool && activeQuickTool === "copywriting";
|
||||
const pageLabel = isSetTool ? "商品套图" : isDetail ? "A+/详情页" : isTryOn ? "AI服饰穿戴" : activeToolMeta?.label || "商品工具";
|
||||
const setPrimaryLabel =
|
||||
setImages.length === 0
|
||||
@@ -5204,6 +5306,19 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{ key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(hotRatio), options: quickSetRatioOptions, onChange: setHotRatio },
|
||||
];
|
||||
|
||||
const quickSetBasicSelects: Array<{
|
||||
key: CloneBasicSelectKey;
|
||||
label: string;
|
||||
value: string;
|
||||
options: string[];
|
||||
onChange: (value: string) => void;
|
||||
}> = [
|
||||
{ key: "platform", label: "平台", value: platform, options: platformOptions, onChange: setPlatform },
|
||||
{ key: "market", label: "国家", value: market, options: marketOptions, onChange: setMarket },
|
||||
{ key: "language", label: "语种", value: language, options: cloneLanguageOptions, onChange: setLanguage },
|
||||
{ key: "ratio", label: "尺寸/比例", value: getQuickSetRatioValue(ratio), options: quickSetRatioOptions, onChange: setRatio },
|
||||
];
|
||||
|
||||
const cloneModelSelects: Array<{
|
||||
key: CloneModelSelectKey;
|
||||
label: string;
|
||||
@@ -6229,15 +6344,23 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{ label: "智能抠图", tone: "cutout", icon: <ScissorOutlined />, onClick: openSmartCutoutUpload },
|
||||
{ label: "去除水印", tone: "watermark", icon: <ClearOutlined />, onClick: openWatermarkRemovalPage },
|
||||
{ label: "图片翻译", tone: "translate", icon: <GlobalOutlined />, onClick: openImageTranslatePage },
|
||||
{ label: "商品套图", tone: "product", icon: <AppstoreOutlined />, onClick: openQuickSetPage },
|
||||
{ label: "一键文案", tone: "copywriting", icon: <EditOutlined />, onClick: openCopywritingPage },
|
||||
{ label: "更多功能", tone: "more", icon: <SettingOutlined />, disabled: true },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
className={`ecom-command-quick-card ecom-command-quick-card--${item.tone}`}
|
||||
disabled={item.disabled}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
item.onClick?.();
|
||||
if (item.disabled) {
|
||||
toast.info("更多功能即将上线,敬请期待!");
|
||||
} else {
|
||||
item.onClick?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true">{item.icon}</span>
|
||||
@@ -7243,6 +7366,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
const quickDetailVisibleSelect = quickDetailBasicSelects.find((item) => item.key === visibleQuickSetSelect) ?? null;
|
||||
const quickHotVisibleSelect = quickHotBasicSelects.find((item) => item.key === visibleQuickSetSelect) ?? null;
|
||||
const quickSetVisibleSelect = quickSetBasicSelects.find((item) => item.key === visibleQuickSetSelect) ?? null;
|
||||
|
||||
const quickDetailPreview = (
|
||||
<main key="quick-detail" className={`ecom-quick-set-page ecom-quick-detail-page ecom-tool-page-enter${isQuickPanelCollapsed ? " is-panel-collapsed" : ""}`} aria-label="A+详情页生成">
|
||||
@@ -7704,6 +7828,266 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</main>
|
||||
);
|
||||
|
||||
const quickSetGenPreview = (
|
||||
<main key="quick-set" className={`ecom-quick-set-page ecom-quick-hot-page ecom-tool-page-enter${isQuickPanelCollapsed ? " is-panel-collapsed" : ""}`} aria-label="电商套图生成">
|
||||
<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={closeQuickSetPage}>首页</button>
|
||||
<button type="button" className="ecom-quick-set-back" onClick={closeQuickSetPage}>上一页</button>
|
||||
</header>
|
||||
<section>
|
||||
<strong><FileImageOutlined /> 上传商品原图</strong>
|
||||
{productImages.length ? (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`ecom-quick-set-upload ecom-quick-hot-material has-images${isProductUploadDragging ? " is-dragging" : ""}`}
|
||||
onClick={() => productInputRef.current?.click()}
|
||||
onKeyDown={(event) => openQuickUploadWithKeyboard(event, productInputRef)}
|
||||
onDragOver={(event) => { event.preventDefault(); event.stopPropagation(); if (event.dataTransfer.types.includes("Files")) setIsProductUploadDragging(true); }}
|
||||
onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) setIsProductUploadDragging(false); }}
|
||||
onDrop={(event) => { event.preventDefault(); event.stopPropagation(); setIsProductUploadDragging(false); const files = Array.from(event.dataTransfer.files); if (files.length) addProductImages(files); }}
|
||||
>
|
||||
{renderQuickUploadThumbs(productImages, removeProductImage)}
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-quick-hot-add-btn"
|
||||
aria-label="添加更多素材"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
productInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`ecom-quick-set-upload ecom-quick-hot-material${isProductUploadDragging ? " is-dragging" : ""}`}
|
||||
onClick={() => productInputRef.current?.click()}
|
||||
onKeyDown={(event) => openQuickUploadWithKeyboard(event, productInputRef)}
|
||||
onDragOver={(event) => { event.preventDefault(); event.stopPropagation(); if (event.dataTransfer.types.includes("Files")) setIsProductUploadDragging(true); }}
|
||||
onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); if (event.currentTarget === event.target || !event.currentTarget.contains(event.relatedTarget as Node)) setIsProductUploadDragging(false); }}
|
||||
onDrop={(event) => { event.preventDefault(); event.stopPropagation(); setIsProductUploadDragging(false); const files = Array.from(event.dataTransfer.files); if (files.length) addProductImages(files); }}
|
||||
>
|
||||
<FileImageOutlined />
|
||||
<span>拖拽或点击上传</span>
|
||||
<em>同一产品,最多 7 张</em>
|
||||
<b>+ 上传图片</b>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={productInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="ecom-command-hidden-file"
|
||||
onChange={handleProductUpload}
|
||||
aria-label="上传商品图片"
|
||||
/>
|
||||
</section>
|
||||
<section className="ecom-quick-set-basic-section">
|
||||
<span className="ecom-quick-set-label">基础设置</span>
|
||||
<div className="ecom-quick-set-select-anchor">
|
||||
<div className="ecom-quick-set-selects">
|
||||
{quickSetBasicSelects.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={openQuickSetSelect === item.key ? "is-active" : ""}
|
||||
onClick={() => toggleQuickSetSelect(item.key)}
|
||||
>
|
||||
<span>{item.label}</span><strong>{formatRatioDisplayValue(item.value)}</strong><em>⌄</em>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{quickSetVisibleSelect ? (
|
||||
<div
|
||||
className={`ecom-quick-set-dropdown ecom-quick-set-dropdown--${quickSetVisibleSelect.key}${isQuickSetSelectClosing ? " is-closing" : ""}`}
|
||||
role="listbox"
|
||||
aria-label={quickSetVisibleSelect.label}
|
||||
>
|
||||
{quickSetVisibleSelect.options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
type="button"
|
||||
className={quickSetVisibleSelect.value === option ? "is-active" : ""}
|
||||
onClick={() => {
|
||||
quickSetVisibleSelect.onChange(option);
|
||||
closeQuickSetSelect();
|
||||
}}
|
||||
>
|
||||
{formatRatioDisplayValue(option)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
<section className="ecom-quick-set-count-section">
|
||||
<span className="ecom-quick-set-label">图片数量</span>
|
||||
<p className="ecom-quick-set-hint">可自由调整各类型图片数量,总数 1-16 张</p>
|
||||
<div className="ecom-quick-set-counts">
|
||||
{cloneSetCountOptions.map((item) => {
|
||||
const count = cloneSetCounts[item.key];
|
||||
const decrementDisabled = count <= 0 || cloneSetTotal <= minCloneSetTotal;
|
||||
const incrementDisabled = cloneSetTotal >= maxCloneSetTotal;
|
||||
return (
|
||||
<div key={item.key} className="ecom-quick-set-count-row">
|
||||
<div className="ecom-quick-set-count-info">
|
||||
<strong>{item.title}</strong>
|
||||
<span>{item.desc}</span>
|
||||
</div>
|
||||
<div className="clone-ai-count-stepper" aria-label={`${item.title}数量`}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={decrementDisabled}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
startCloneSetCountHold(item.key, -1, decrementDisabled);
|
||||
}}
|
||||
onPointerUp={clearCloneSetCountHold}
|
||||
onPointerLeave={clearCloneSetCountHold}
|
||||
onPointerCancel={clearCloneSetCountHold}
|
||||
onBlur={clearCloneSetCountHold}
|
||||
aria-label={`减少${item.title}`}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<b>{count}</b>
|
||||
<button
|
||||
type="button"
|
||||
disabled={incrementDisabled}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
startCloneSetCountHold(item.key, 1, incrementDisabled);
|
||||
}}
|
||||
onPointerUp={clearCloneSetCountHold}
|
||||
onPointerLeave={clearCloneSetCountHold}
|
||||
onPointerCancel={clearCloneSetCountHold}
|
||||
onBlur={clearCloneSetCountHold}
|
||||
aria-label={`增加${item.title}`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
<section className="ecom-quick-hot-requirement">
|
||||
<div className="ecom-quick-hot-requirement__head">
|
||||
<strong>商品卖点 & 需求</strong>
|
||||
<button type="button" className="ecom-quick-hot-requirement__ai" onClick={handleQuickSetAiWrite}>AI 帮写</button>
|
||||
</div>
|
||||
<div className="ecom-quick-hot-requirement__input">
|
||||
<textarea
|
||||
value={quickSetRequirement}
|
||||
onChange={(event) => setQuickSetRequirement(event.target.value.slice(0, 500))}
|
||||
placeholder="建议包含以下信息:产品名称、核心卖点、期望场景、具体参数"
|
||||
maxLength={500}
|
||||
/>
|
||||
<span>{quickSetRequirement.length}/500</span>
|
||||
</div>
|
||||
</section>
|
||||
<div className="ecom-quick-hot-actions">
|
||||
<button type="button" className="ecom-quick-set-primary ecom-quick-hot-generate" onClick={handleQuickSetGenerate} disabled={!canGenerateQuickSet}>
|
||||
{quickSetStatus === "generating" ? <LoadingOutlined /> : "✦"} 开始生成
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`ecom-quick-set-primary ecom-quick-set-primary--cancel${quickSetStatus !== "generating" ? " is-disabled" : ""}`}
|
||||
onClick={quickSetStatus === "generating" ? handleCancelGenerate : undefined}
|
||||
disabled={quickSetStatus !== "generating"}
|
||||
>
|
||||
取消生成
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<section className="ecom-quick-set-stage">
|
||||
<header className="ecom-quick-set-preview-head">
|
||||
<h1>预览</h1>
|
||||
<p>上传商品图,AI 即刻生成 <span>符合多电商平台规范</span> 的高转化率商品套图。</p>
|
||||
<div>
|
||||
<button type="button" onClick={() => setPreviewZoom((value) => Math.max(0.25, value - 0.1))}>-</button>
|
||||
<strong>{Math.round(previewZoom * 100)}%</strong>
|
||||
<button type="button" onClick={() => setPreviewZoom((value) => Math.min(2, value + 0.1))}>+</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="ecom-quick-set-canvas" onWheel={handleQuickPreviewWheel}>
|
||||
{quickSetStatus === "done" && quickSetResultUrls.length > 0 ? (
|
||||
<section className="ecom-quick-detail-result" style={{ transform: `scale(${previewZoom})` }}>
|
||||
<div className="ecom-quick-set-result-grid">
|
||||
{quickSetResultUrls.map((url, index) => (
|
||||
<figure key={`quick-set-${index}`}>
|
||||
<img src={url} alt={`套图 ${index + 1}`} />
|
||||
<span>套图 {index + 1}</span>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-quick-detail-download"
|
||||
onClick={() => {
|
||||
quickSetResultUrls.forEach((url, index) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `电商套图-${index + 1}-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<CloudUploadOutlined />
|
||||
保存本地
|
||||
</button>
|
||||
</section>
|
||||
) : quickSetStatus === "generating" ? (
|
||||
<section className="ecom-quick-set-generating">
|
||||
<LoadingOutlined />
|
||||
<strong>正在生成电商套图</strong>
|
||||
<span>AI 正在根据您的商品图和设置生成套图素材,请稍候...</span>
|
||||
<div className="ecom-quick-set-progress">
|
||||
<div className="ecom-quick-set-progress-bar" style={{ width: `${Math.round(quickSetProgress)}%` }} />
|
||||
</div>
|
||||
<em className="ecom-quick-set-progress-text">{Math.round(quickSetProgress)}%</em>
|
||||
</section>
|
||||
) : quickSetStatus === "failed" ? (
|
||||
<section className="ecom-quick-set-failed">
|
||||
<FrownOutlined />
|
||||
<strong>生成失败</strong>
|
||||
<span>请检查网络或重试,如余额不足请先充值。</span>
|
||||
<button type="button" onClick={handleQuickSetGenerate} disabled={!canGenerateQuickSet}>重新生成</button>
|
||||
</section>
|
||||
) : productImages.length ? (
|
||||
<section className="ecom-quick-detail-preview-card" style={{ transform: `scale(${previewZoom})` }}>
|
||||
{detailGridSamples.slice(0, 6).map((src, index) => (
|
||||
<figure key={src}>
|
||||
<img src={src} alt={`套图预览 ${index + 1}`} />
|
||||
<span>{detailModules[index]?.title ?? "套图模块"}</span>
|
||||
</figure>
|
||||
))}
|
||||
</section>
|
||||
) : (
|
||||
<section className="ecom-quick-set-empty">
|
||||
<FileImageOutlined />
|
||||
<strong>等待生成</strong>
|
||||
<span>上传商品原图并填写信息后,AI 将在这里展示生成结果。</span>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<button type="button" className="ecom-quick-set-help" aria-label="帮助" onClick={() => toast.info("上传商品图后,选择平台和套图数量即可生成电商套图。")}>?</button>
|
||||
</main>
|
||||
);
|
||||
|
||||
const detailPreview = (
|
||||
<main className="product-clone-preview product-clone-preview--detail" aria-label="A+详情预览" onWheel={handlePreviewWheel}>
|
||||
<div className="product-clone-preview__headline">
|
||||
@@ -7799,6 +8183,12 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
</main>
|
||||
);
|
||||
|
||||
const copywritingPreview = (
|
||||
<div key="copywriting" className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||
<EcommerceCopywritingPanel onClose={closeCopywritingPage} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const activePreview = isSetTool
|
||||
? setPreview
|
||||
: isDetail
|
||||
@@ -7826,9 +8216,17 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
{hotClonePreview}
|
||||
</div>
|
||||
)
|
||||
: clonePreview
|
||||
: isQuickSetTool
|
||||
? (
|
||||
<div key={`quick-${activeQuickTool}`} className="ecom-quick-page-wrap ecom-tool-page-enter">
|
||||
{quickSetGenPreview}
|
||||
</div>
|
||||
)
|
||||
: isCopywritingTool
|
||||
? copywritingPreview
|
||||
: clonePreview
|
||||
: placeholderPreview;
|
||||
const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool;
|
||||
const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isQuickSetTool && !isCopywritingTool;
|
||||
const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean(activeHistoryRecordId);
|
||||
const currentResultCount = canvasNodes.reduce((count, node) => count + node.results.length, 0);
|
||||
const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords.find((record) => record.id === activeHistoryRecordId) : null;
|
||||
@@ -7864,7 +8262,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isRecordDetailWorkspace && isCloneConversationCollapsed ? " is-conversation-collapsed" : ""}${isRecordDetailWorkspace ? " is-history-detail" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}${isHotCloneTool ? " is-hot-clone-page" : ""}`}
|
||||
className={`product-clone-page page-motion${isCloneTool && isCloneSettingsCollapsed ? " is-settings-collapsed" : ""}${isCloneTool && isCommandHistoryCollapsed ? " is-history-collapsed" : ""}${isRecordDetailWorkspace && isCloneConversationCollapsed ? " is-conversation-collapsed" : ""}${isRecordDetailWorkspace ? " is-history-detail" : ""}${isSmartCutoutTool ? " is-smart-cutout-page" : ""}${isQuickDetailTool ? " is-quick-set-page" : ""}${isWatermarkTool ? " is-watermark-page" : ""}${isTranslateTool ? " is-translate-page" : ""}${isImageEditTool ? " is-image-workbench-page" : ""}${isHotCloneTool ? " is-hot-clone-page" : ""}${isQuickSetTool ? " is-quick-set-page" : ""}${isCopywritingTool ? " is-copywriting-page" : ""}`}
|
||||
data-tool={activeTool}
|
||||
aria-label={pageLabel}
|
||||
>
|
||||
@@ -7885,10 +8283,10 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
|
||||
aria-label={`${pageLabel}参数`}
|
||||
aria-hidden={isCloneTool && isCloneSettingsCollapsed ? true : undefined}
|
||||
>
|
||||
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCloneTool ? clonePanel : placeholderPanel}
|
||||
{isSetTool ? setPanel : isDetail ? detailPanel : isTryOn ? tryOnPanel : isCopywritingTool ? placeholderPanel : isCloneTool ? clonePanel : placeholderPanel}
|
||||
</aside>
|
||||
|
||||
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isHotCloneTool ? (
|
||||
{isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isHotCloneTool && !isQuickSetTool && !isCopywritingTool ? (
|
||||
<button
|
||||
type="button"
|
||||
className="clone-ai-settings-toggle"
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
CopyOutlined,
|
||||
EditOutlined,
|
||||
FileTextOutlined,
|
||||
FireOutlined,
|
||||
GlobalOutlined,
|
||||
MessageOutlined,
|
||||
SmileOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export type CopywritingType =
|
||||
| "self-media"
|
||||
| "universal"
|
||||
| "original"
|
||||
| "imitate"
|
||||
| "wechat"
|
||||
| "crossborder"
|
||||
| "emoji"
|
||||
| "more";
|
||||
|
||||
interface CopywritingTypeOption {
|
||||
key: CopywritingType;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const copywritingTypes: CopywritingTypeOption[] = [
|
||||
{ key: "self-media", label: "自媒体文案", icon: <MessageOutlined />, description: "小红书/抖音/公众号风格" },
|
||||
{ key: "universal", label: "万能写作", icon: <EditOutlined />, description: "通用场景长文短句" },
|
||||
{ key: "original", label: "一键原创", icon: <ThunderboltOutlined />, description: "快速改写去重" },
|
||||
{ key: "imitate", label: "文案仿写", icon: <CopyOutlined />, description: "参照爆款风格重写" },
|
||||
{ key: "wechat", label: "微信营销文案", icon: <FileTextOutlined />, description: "朋友圈/社群转化文案" },
|
||||
{ key: "crossborder", label: "跨境商品文案", icon: <GlobalOutlined />, description: "Amazon/Temu 卖点描述" },
|
||||
{ key: "emoji", label: "文案加Emoji", icon: <SmileOutlined />, description: "自动插入表情符号" },
|
||||
{ key: "more", label: "更多场景", icon: <AppstoreOutlined />, description: "持续更新中" },
|
||||
];
|
||||
|
||||
const wordCountOptions = ["不限", "100字", "300字", "500字", "800字"];
|
||||
|
||||
const exampleResults: Record<CopywritingType, Array<{ title: string; body: string; points: string[] }>> = {
|
||||
"self-media": [
|
||||
{
|
||||
title: "超值干发神器,吸水力 MAX!",
|
||||
body: "家人们,我发现了一款干发帽,双层加厚吸水力超强!而且只要个位数就能到手啊!",
|
||||
points: [
|
||||
"超强吸水力:这款干发帽采用微纤维材质,轻轻一裹,水分立马被吸走,头发快速告别湿漉漉。",
|
||||
"柔软亲肤:触感超级柔软,对皮肤和头发都是温柔的抚摸,不会有摩擦伤害哦。",
|
||||
"加厚设计:比普通干发帽更厚实,吸水效果自然更胜一筹,长发妹子的福音!",
|
||||
"方便携带:轻巧不占空间,不论是去健身房还是旅行,携带都毫无负担。",
|
||||
],
|
||||
},
|
||||
],
|
||||
universal: [
|
||||
{
|
||||
title: "直接抄作业!科学的减重方法必试!",
|
||||
body: "姐妹们冲鸭!有很多科学有效的方式可以帮助我们实现理想体重,今天就来分享一下必试的方法!",
|
||||
points: [
|
||||
"快乐有氧运动:科学的减重方式,通过有氧运动如慢跑、游泳等,能够促进脂肪燃烧,让身体更健康!",
|
||||
"均衡饮食规划:摄入足够的蛋白质、蔬果以及谷物,避免过多的高糖和高脂食物,帮助达到减重目标!",
|
||||
"科学计算热量:了解自己每日所需的卡路里摄入量,合理安排每餐的热量搭配,控制总摄入量。",
|
||||
"坚持低强度运动:逐渐增加日常活动量,如步行、瑜伽等,通过持续的轻度运动,加速代谢!",
|
||||
"合理休息调节:不要忽视睡眠的重要性,保证每晚充足的睡眠时间,帮助恢复体力和新陈代谢。",
|
||||
],
|
||||
},
|
||||
],
|
||||
original: [
|
||||
{
|
||||
title: "原创种草|这款干发帽真的值得入!",
|
||||
body: "洗完头最烦的就是湿哒哒滴水?试试这条双层加厚干发帽,吸水速度真的惊艳到我。",
|
||||
points: [
|
||||
"加厚材质,吸水更快更彻底",
|
||||
"柔软不勒头,长发短发都能用",
|
||||
"轻便好收纳,差旅党必备",
|
||||
"性价比超高,入手不亏",
|
||||
],
|
||||
},
|
||||
],
|
||||
imitate: [
|
||||
{
|
||||
title: "仿写爆款|让头发速干的小心机",
|
||||
body: "姐妹们有没有发现,最近超火的干发帽真的太好用了!轻轻一裹,几分钟头发就半干了。",
|
||||
points: [
|
||||
"双层加厚,吸水力翻倍",
|
||||
"柔软亲肤,不伤发质",
|
||||
"小巧便携,出门也能带",
|
||||
"颜值在线,多色可选",
|
||||
],
|
||||
},
|
||||
],
|
||||
wechat: [
|
||||
{
|
||||
title: "朋友圈文案|个位数到手的干发神器",
|
||||
body: "今天必须给大家安利这个干发帽!双层加厚,吸水超强,个位数就能到手,真的不冲吗?",
|
||||
points: [
|
||||
"微纤维材质,轻柔速干",
|
||||
"加厚设计,吸水更彻底",
|
||||
"小巧便携,旅行出差都能带",
|
||||
"限时好价,手慢无",
|
||||
],
|
||||
},
|
||||
],
|
||||
crossborder: [
|
||||
{
|
||||
title: "Amazon Listing|Super Absorbent Hair Turban",
|
||||
body: "Made with ultra-soft microfiber, this double-layer hair turban dries hair quickly while protecting delicate strands.",
|
||||
points: [
|
||||
"Double-layer microfiber for maximum absorbency",
|
||||
"Gentle on hair and skin, no frizz or breakage",
|
||||
"Lightweight and travel-friendly design",
|
||||
"Secure button closure stays in place",
|
||||
],
|
||||
},
|
||||
],
|
||||
emoji: [
|
||||
{
|
||||
title: "✨个位数到手的干发神器,吸水力 MAX!",
|
||||
body: "家人们👋,我发现了一款超棒的干发帽💧,双层加厚吸水力超强!而且只要个位数就能到手啊🛒!",
|
||||
points: [
|
||||
"💦 超强吸水力:微纤维材质,轻轻一裹水分吸走",
|
||||
"☁️ 柔软亲肤:触感温柔,不伤头发和皮肤",
|
||||
"🎒 方便携带:轻巧不占空间,旅行健身都能带",
|
||||
"💰 超值价格:个位数到手,性价比拉满",
|
||||
],
|
||||
},
|
||||
],
|
||||
more: [
|
||||
{
|
||||
title: "更多场景示例",
|
||||
body: "选择左侧具体文案类型即可生成对应场景内容,更多场景持续更新中。",
|
||||
points: ["选择合适的文案类型", "填写内容需求", "选择生成字数", "点击开始生成"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export interface EcommerceCopywritingPanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function EcommerceCopywritingPanel({ onClose }: EcommerceCopywritingPanelProps) {
|
||||
const [selectedType, setSelectedType] = useState<CopywritingType>("self-media");
|
||||
const [requirement, setRequirement] = useState("");
|
||||
const [wordCount, setWordCount] = useState("不限");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [results, setResults] = useState<typeof exampleResults["self-media"]>([]);
|
||||
|
||||
const handleGenerate = () => {
|
||||
setLoading(true);
|
||||
setResults([]);
|
||||
// 模拟生成延迟
|
||||
window.setTimeout(() => {
|
||||
setResults(exampleResults[selectedType]);
|
||||
setLoading(false);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const selectedTypeLabel = copywritingTypes.find((item) => item.key === selectedType)?.label ?? "文案";
|
||||
|
||||
return (
|
||||
<main className="ecom-copywriting-page ecom-tool-page-enter" aria-label="一键文案">
|
||||
<div className="ecom-copywriting-body">
|
||||
<aside className="ecom-copywriting-panel" aria-label="文案设置">
|
||||
<header className="ecom-copywriting-panel-head">
|
||||
<strong className="ecom-copywriting-page-title">一键文案</strong>
|
||||
<button type="button" className="ecom-copywriting-back" onClick={onClose}>
|
||||
首页
|
||||
</button>
|
||||
<button type="button" className="ecom-copywriting-back" onClick={onClose}>
|
||||
上一页
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section className="ecom-copywriting-section">
|
||||
<strong className="ecom-copywriting-section-title">选择文案类型</strong>
|
||||
<div className="ecom-copywriting-type-grid">
|
||||
{copywritingTypes.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={`ecom-copywriting-type-card${selectedType === item.key ? " is-active" : ""}`}
|
||||
onClick={() => setSelectedType(item.key)}
|
||||
>
|
||||
<span className="ecom-copywriting-type-icon" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="ecom-copywriting-type-label">{item.label}</span>
|
||||
<span className="ecom-copywriting-type-desc">{item.description}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="ecom-copywriting-section">
|
||||
<strong className="ecom-copywriting-section-title">内容需求</strong>
|
||||
<textarea
|
||||
className="ecom-copywriting-textarea"
|
||||
value={requirement}
|
||||
onChange={(event) => setRequirement(event.target.value)}
|
||||
placeholder="例如:主题、核心卖点、适用人群、期望场景等"
|
||||
rows={5}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="ecom-copywriting-section">
|
||||
<strong className="ecom-copywriting-section-title">生成字数</strong>
|
||||
<div className="ecom-copywriting-wordcount">
|
||||
{wordCountOptions.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
className={wordCount === item ? "is-active" : ""}
|
||||
onClick={() => setWordCount(item)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="ecom-copywriting-generate"
|
||||
onClick={handleGenerate}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="ecom-copywriting-spinner" />
|
||||
生成中…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ThunderboltOutlined />
|
||||
开始生成
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<section className="ecom-copywriting-stage" aria-label="生成文案预览">
|
||||
<header className="ecom-copywriting-preview-head">
|
||||
<h1>生成文案</h1>
|
||||
<p>
|
||||
基于 <span>{selectedTypeLabel}</span> 风格,AI 为你生成高转化文案。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="ecom-copywriting-results">
|
||||
{results.length === 0 && !loading ? (
|
||||
<div className="ecom-copywriting-empty">
|
||||
<FileTextOutlined />
|
||||
<strong>等待生成</strong>
|
||||
<em>填写需求后点击「开始生成」即可查看文案结果</em>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<div className="ecom-copywriting-loading">
|
||||
<span className="ecom-copywriting-spinner" />
|
||||
<span>AI 正在生成文案,请稍候…</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{results.map((item, index) => (
|
||||
<article key={index} className="ecom-copywriting-result-card">
|
||||
<header>
|
||||
<span>示例 {index + 1}</span>
|
||||
<strong>{item.title}</strong>
|
||||
</header>
|
||||
<p className="ecom-copywriting-result-body">{item.body}</p>
|
||||
<ul className="ecom-copywriting-result-points">
|
||||
{item.points.map((point, pointIndex) => (
|
||||
<li key={pointIndex}>
|
||||
<span>{pointIndex + 1}</span>
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user