2026-06-16 20:15:53 +08:00
|
|
|
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;
|
|
|
|
|
|
2026-06-16 21:09:41 +08:00
|
|
|
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;
|
2026-06-16 20:15:53 +08:00
|
|
|
|
2026-06-16 21:09:41 +08:00
|
|
|
setIsTopbarHidden(true);
|
|
|
|
|
if (restoreTimer) window.clearTimeout(restoreTimer);
|
|
|
|
|
restoreTimer = window.setTimeout(() => {
|
|
|
|
|
setIsTopbarHidden(false);
|
|
|
|
|
}, 240);
|
2026-06-16 20:15:53 +08:00
|
|
|
};
|
|
|
|
|
|
2026-06-16 21:09:41 +08:00
|
|
|
window.addEventListener("scroll", handleScroll, { capture: true, passive: true });
|
2026-06-16 20:15:53 +08:00
|
|
|
|
|
|
|
|
return () => {
|
2026-06-16 21:09:41 +08:00
|
|
|
window.removeEventListener("scroll", handleScroll, { capture: true });
|
2026-06-16 20:15:53 +08:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|