diff --git a/src/App.tsx b/src/App.tsx index 0707138..2bf0a6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( - - {avatarUrl ? : {label}} - - ); -} - 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(null); const [profileMenuOpen, setProfileMenuOpen] = useState(false); const [currentPage, setCurrentPage] = useState<"workspace" | "profile">("workspace"); + const [workspaceChrome, setWorkspaceChrome] = useState({ + 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: , label: "UID", value: session?.user.id ?? "-" }, - { icon: , label: "积分", value: `${balance.toFixed(2)} 积分` }, - { icon: , label: "图片", value: usage.imageUsed }, - { icon: , label: "视频", value: usage.videoUsed }, - { icon: , label: "作品", value: shownWorkCount }, - ], - [balance, session?.user.id, shownWorkCount, usage.imageUsed, usage.videoUsed], - ); const handleOpenProfile = () => { setProfileMenuOpen(false); @@ -349,106 +327,31 @@ function App() { }; return ( - - - - - - - OmniAI 电商智能体 - - - {session ? ( - - - {(Math.max(usage.balanceCents, 0) / 100).toFixed(2)} 积分 - - setProfileMenuOpen((open) => !open)} - aria-haspopup="dialog" - aria-expanded={profileMenuOpen} - > - - {displayName} - - {profileMenuOpen ? ( - <> - setProfileMenuOpen(false)} - /> - - - - - {displayName} - {session.user.username} - - - - - {avatarMenuStats.map((item) => ( - - {item.icon}{item.label} - {item.value} - - ))} - - - - - - 个人中心 - - - - Bug 反馈 - - - - 退出 - - - - > - ) : null} - - ) : ( - openAuth("login")}> - - 登录 / 注册 - - )} - - + + {session ? ( - + + undefined} onOpenProject={() => undefined} onDeleteProject={() => undefined} diff --git a/src/components/LocalAvatar.tsx b/src/components/LocalAvatar.tsx new file mode 100644 index 0000000..0bc7c6e --- /dev/null +++ b/src/components/LocalAvatar.tsx @@ -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 ( + + {avatarUrl ? : {label}} + + ); +} diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx new file mode 100644 index 0000000..44734b3 --- /dev/null +++ b/src/components/Topbar.tsx @@ -0,0 +1,201 @@ +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; + let cleanupScrollTarget: (() => void) | undefined; + + const bindScrollTarget = () => { + cleanupScrollTarget?.(); + const target = document.querySelector( + ".ecommerce-standalone__page--workspace:not([hidden]) .clone-ai-preview", + ); + if (!target) { + cleanupScrollTarget = undefined; + return; + } + + const handleScroll = () => { + if (profileMenuOpen) return; + setIsTopbarHidden(true); + if (restoreTimer) window.clearTimeout(restoreTimer); + restoreTimer = window.setTimeout(() => { + setIsTopbarHidden(false); + }, 240); + }; + + target.addEventListener("scroll", handleScroll, { passive: true }); + cleanupScrollTarget = () => target.removeEventListener("scroll", handleScroll); + }; + + bindScrollTarget(); + const observer = new MutationObserver(bindScrollTarget); + observer.observe(document.body, { attributes: true, childList: true, subtree: true }); + + return () => { + cleanupScrollTarget?.(); + observer.disconnect(); + 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: , label: "UID", value: session?.user.id ?? "-" }, + { icon: , label: "积分", value: `${balance.toFixed(2)} 积分` }, + { icon: , label: "图片", value: usage.imageUsed }, + { icon: , label: "视频", value: usage.videoUsed }, + { icon: , label: "作品", value: shownWorkCount }, + ], + [balance, session?.user.id, shownWorkCount, usage.imageUsed, usage.videoUsed], + ); + + return ( + + + + + + OmniAI 电商智能体 + + + {session ? ( + + onProfileMenuOpenChange(!profileMenuOpen)} + aria-haspopup="dialog" + aria-expanded={profileMenuOpen} + > + + {(Math.max(usage.balanceCents, 0) / 100).toFixed(2)} 积分 + + + {displayName} + + {profileMenuOpen ? ( + <> + onProfileMenuOpenChange(false)} + /> + + + + + {displayName} + {session.user.username} + + + + + {avatarMenuStats.map((item) => ( + + + {item.icon} + {item.label} + + {item.value} + + ))} + + + + + + 个人中心 + + + + Bug 反馈 + + + + 退出 + + + + > + ) : null} + + ) : ( + onOpenAuth("login")} + > + + 登录 / 注册 + + )} + + + ); +}