Merge origin/develop into feature/a-card-operations

This commit is contained in:
郑龙捷
2026-09-02 10:33:00 +08:00
73 changed files with 7177 additions and 3280 deletions
+81 -26
View File
@@ -1,8 +1,10 @@
import { ChevronRight, LayoutGrid } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { ChevronRight, LayoutGrid, LogOut, Settings } from "lucide-react";
import type { AuthUser, AuthWorkspace } from "../../context/AuthContext";
import { AccountSettingsDialog } from "./AccountSettingsDialog";
type TopbarProps = {
activePage: string;
pageTitle: string;
apiOnline: boolean;
user: AuthUser | null;
currentWorkspace: AuthWorkspace;
@@ -14,7 +16,7 @@ type TopbarProps = {
};
export function Topbar({
activePage,
pageTitle,
apiOnline,
user,
currentWorkspace,
@@ -24,18 +26,30 @@ export function Topbar({
onSetCurrentWorkspace,
onLogout,
}: TopbarProps) {
const [userMenuOpen, setUserMenuOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!userMenuOpen) return;
const onPointerDown = (event: MouseEvent) => {
if (!userMenuRef.current?.contains(event.target as Node)) {
setUserMenuOpen(false);
}
};
document.addEventListener("mousedown", onPointerDown);
return () => document.removeEventListener("mousedown", onPointerDown);
}, [userMenuOpen]);
const openUserMenu = () => {
onSetWorkspaceMenuOpen(false);
setUserMenuOpen((open) => !open);
};
const roleLabel = user?.is_system_admin || user?.role_code === "admin"
? "管理员"
: ["business", "business_team", "biz"].includes(user?.role_code ?? "")
? "业务团队"
: "模型团队";
const pageTitles: Record<string, string> = {
home: "工作台",
scripts: "构建脚本",
schedules: "调度配置",
operations: "A卡模型运维",
system: "系统管理",
};
return (
<header className="relative z-[2] flex h-[70px] min-h-[70px] items-center justify-between border-b border-[#dfe6ee] bg-white px-[22px]">
@@ -44,14 +58,14 @@ export function Topbar({
className="grid h-[31px] w-[31px] rotate-180 cursor-pointer place-items-center rounded-[7px] border border-transparent bg-white text-[#7b8999] hover:border-[#b9c9da] hover:bg-[#f7faff]"
type="button"
>
<ChevronRight size={19} />
<ChevronRight size={19} className="text-[#748497]"/>
</button>
<div>
<span className="text-[10px] font-[650] uppercase tracking-[0.08em] text-[#94a2b3]">
</span>
<h1 className="m-0 mt-px text-[18px] leading-[1.1] text-[#14243a]">
{pageTitles[activePage] || "工作台"}
{pageTitle}
</h1>
</div>
</div>
@@ -70,7 +84,10 @@ export function Topbar({
<button
className="flex h-[46px] cursor-pointer items-center gap-[9px] rounded-lg border border-line bg-white px-[11px] hover:border-[#b9cce0]"
type="button"
onClick={() => onSetWorkspaceMenuOpen(!workspaceMenuOpen)}
onClick={() => {
setUserMenuOpen(false);
onSetWorkspaceMenuOpen(!workspaceMenuOpen);
}}
>
<span className="grid size-[30px] place-items-center rounded-[7px] bg-gradient-to-br from-[#23aa73] to-[#158d5d] text-white">
<LayoutGrid size={18} />
@@ -81,10 +98,10 @@ export function Topbar({
{currentWorkspace.workspace_name}
</strong>
</span>
<ChevronRight size={15} />
<ChevronRight size={15} className="text-[#748497]"/>
</button>
{workspaceMenuOpen && (
<div className="absolute top-[calc(100%+7px)] right-0 z-50 w-[260px] rounded-lg border border-line bg-white p-1.5 shadow-lg">
<div className="absolute top-[calc(100%+7px)] right-0 z-50 max-h-[min(300px,calc(100vh-90px))] w-[260px] overflow-y-auto rounded-lg border border-line bg-white p-1.5 shadow-lg">
{workspaces.map((workspace) => {
const isSelected =
workspace.workspace_id === currentWorkspace.workspace_id;
@@ -117,10 +134,13 @@ export function Topbar({
</div>
)}
</div>
<div className="relative flex items-center gap-[9px]">
<div className="relative" ref={userMenuRef}>
<button
className="flex h-[46px] cursor-pointer items-center gap-[9px] rounded-lg border border-transparent bg-white px-[11px] hover:border-[#b9cce0]"
className="flex h-[46px] cursor-pointer items-center gap-[9px] rounded-lg border border-line bg-white px-[11px] hover:border-[#b9cce0]"
type="button"
aria-expanded={userMenuOpen}
aria-haspopup="menu"
onClick={openUserMenu}
>
<span className="grid size-[34px] place-items-center rounded-full bg-gradient-to-br from-[#3b92ed] to-[#1869c9] text-sm font-bold text-white">
{user?.display_name?.slice(0, 1) ?? "?"}
@@ -133,18 +153,53 @@ export function Topbar({
{roleLabel}
</small>
</span>
{/* <ChevronRight size={15} className="text-[#97a3b1]" /> */}
<ChevronRight size={15} className="text-[#748497]" />
</button>
<button
className="inline-flex cursor-pointer items-center gap-0.5 border-0 bg-transparent px-px py-[5px] text-xs font-semibold text-[#1474d4] hover:text-[#0d58a2]"
type="button"
onClick={() => {
void onLogout();
}}
>
</button>
{userMenuOpen && (
<div
className="absolute top-[calc(100%+7px)] right-0 z-50 w-[200px] rounded-lg border border-line bg-white p-1.5 shadow-lg"
role="menu"
>
<div className="border-b border-line px-2.5 py-2.5">
<strong className="block truncate text-[12px] text-[#243a50]">
{user?.display_name ?? "未知用户"}
</strong>
<small className="mt-0.5 block truncate text-[10px] text-[#8797a7]">
{user?.username}
{user?.role_code === "admin" ? " · 管理员" : " · 开发人员"}
</small>
</div>
<button
className="mt-1 flex w-full cursor-pointer items-center gap-2 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-[12px] text-[#40576d] hover:bg-brand-soft"
type="button"
role="menuitem"
onClick={() => {
setUserMenuOpen(false);
setSettingsOpen(true);
}}
>
<Settings size={14} />
</button>
<button
className="flex w-full cursor-pointer items-center gap-2 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-[12px] text-[#c14a4a] hover:bg-[#fdf2f2]"
type="button"
role="menuitem"
onClick={() => {
setUserMenuOpen(false);
void onLogout();
}}
>
<LogOut size={14} />
</button>
</div>
)}
</div>
</div>
<AccountSettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
</header>
);
}