- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
210 lines
8.5 KiB
TypeScript
210 lines
8.5 KiB
TypeScript
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 = {
|
|
pageTitle: string;
|
|
apiOnline: boolean;
|
|
user: AuthUser | null;
|
|
currentWorkspace: AuthWorkspace;
|
|
workspaces: AuthWorkspace[];
|
|
workspaceMenuOpen: boolean;
|
|
onSetWorkspaceMenuOpen: (open: boolean) => void;
|
|
onSetCurrentWorkspace: (workspaceId: string) => void;
|
|
onLogout: () => void;
|
|
onBack: () => void;
|
|
};
|
|
|
|
export function Topbar({
|
|
pageTitle,
|
|
apiOnline,
|
|
user,
|
|
currentWorkspace,
|
|
workspaces,
|
|
workspaceMenuOpen,
|
|
onSetWorkspaceMenuOpen,
|
|
onSetCurrentWorkspace,
|
|
onLogout,
|
|
onBack,
|
|
}: 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 ?? "")
|
|
? "业务团队"
|
|
: "模型团队";
|
|
|
|
return (
|
|
<header className="relative z-[2] flex h-[70px] min-h-[70px] items-center justify-between border-b border-[#dfe6ee] bg-white px-[22px]">
|
|
<div className="flex items-center gap-[9px]">
|
|
<button
|
|
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"
|
|
aria-label="返回上一页"
|
|
onClick={onBack}
|
|
>
|
|
<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]">
|
|
{pageTitle}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-[14px]">
|
|
<div className="flex items-center gap-[7px] text-xs text-[#768597] max-xl:hidden">
|
|
<span
|
|
className={`size-[7px] rounded-full ${
|
|
apiOnline
|
|
? "bg-[#19b878] ring-3 ring-success/10"
|
|
: "bg-[#d05151] ring-3 ring-danger/10"
|
|
}`}
|
|
/>
|
|
{apiOnline ? "服务已连接" : "服务未连接"}
|
|
</div>
|
|
<div className="relative flex items-center gap-[9px]">
|
|
<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={() => {
|
|
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} />
|
|
</span>
|
|
<span className="flex min-w-[155px] max-xl:min-w-[130px] flex-col items-start">
|
|
<small className="text-[10px] text-[#97a3b1]">当前 Workspace</small>
|
|
<strong className="max-w-[190px] truncate text-[13px] text-[#26384d]">
|
|
{currentWorkspace.workspace_name}
|
|
</strong>
|
|
</span>
|
|
<ChevronRight size={15} className="text-[#748497]"/>
|
|
</button>
|
|
{workspaceMenuOpen && (
|
|
<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;
|
|
return (
|
|
<button
|
|
className={`flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 px-2.5 py-2 text-left text-[#40576d] ${
|
|
isSelected
|
|
? "bg-brand-soft"
|
|
: "bg-transparent hover:bg-brand-soft"
|
|
}`}
|
|
type="button"
|
|
key={workspace.workspace_id}
|
|
onClick={() => {
|
|
onSetCurrentWorkspace(workspace.workspace_id);
|
|
onSetWorkspaceMenuOpen(false);
|
|
}}
|
|
>
|
|
<LayoutGrid size={15} />
|
|
<span className="flex min-w-0 flex-col gap-0.5">
|
|
<strong className="truncate text-[11px] text-[#243a50]">
|
|
{workspace.workspace_name}
|
|
</strong>
|
|
<small className="text-[9px] text-[#8797a7]">
|
|
{isSelected ? "当前使用" : "点击切换"}
|
|
</small>
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="relative" ref={userMenuRef}>
|
|
<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"
|
|
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) ?? "?"}
|
|
</span>
|
|
<span className="flex min-w-[50px] flex-col items-start">
|
|
<strong className="max-w-[190px] truncate text-[13px] text-[#26384d]">
|
|
{user?.display_name ?? "未知用户"}
|
|
</strong>
|
|
<small className="text-[10px] text-[#97a3b1]">
|
|
{roleLabel}
|
|
</small>
|
|
</span>
|
|
{/* <ChevronRight size={15} className="text-[#97a3b1]" /> */}
|
|
<ChevronRight size={15} className="text-[#748497]" />
|
|
</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>
|
|
);
|
|
}
|