Files
omniai-ds-code-package/src/components/Topbar.tsx
T

196 lines
6.7 KiB
TypeScript

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>
);
}