- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
145 lines
7.6 KiB
TypeScript
145 lines
7.6 KiB
TypeScript
import { useNavigate } from "react-router";
|
||
|
||
import { Button } from "~/components/ui/button";
|
||
import type { OperationsWorkbenchItem, OperationsWorkbenchKpi } from "~/services/operationsApi";
|
||
import { useOperationsData } from "./OperationsDataContext";
|
||
|
||
const FALLBACK_KPIS: OperationsWorkbenchKpi[] = [
|
||
{ key: "models", label: "在管模型", value: 0, detail: "0 家银行 · 0 个大类", target: "/operations/models", tone: "normal" },
|
||
{ key: "grades", label: "B / C 等级", value: 0, detail: "C 0 · B 0", target: "/operations/monitoring?grade=B,C", tone: "normal" },
|
||
{ key: "pending_reviews", label: "待处理结果", value: 0, detail: "到期未处理即默认暂不处理", target: "/operations/monitoring", tone: "normal" },
|
||
{ key: "intervention", label: "需主动干预", value: 0, detail: "KS < 30% 或 PSI > 50%", target: "/operations/monitoring", tone: "normal" },
|
||
{ key: "model_reports", label: "待我阅读报告", value: 0, detail: "超过 5 个工作日进入催办", target: "/operations/reports", tone: "normal" },
|
||
{ key: "stalled_workflows", label: "流程停滞", value: 0, detail: "超过节点期限未更新", target: "/operations/workflows", tone: "normal" },
|
||
];
|
||
|
||
const kpiToneClasses = {
|
||
normal: "border-border bg-card",
|
||
primary: "border-primary/25 bg-card",
|
||
warning: "border-warning/35 bg-[#fffaf5]",
|
||
danger: "border-danger/35 bg-[#fff7f6]",
|
||
} as const;
|
||
|
||
const itemToneClasses = {
|
||
normal: "bg-border",
|
||
primary: "bg-primary",
|
||
warning: "bg-warning",
|
||
danger: "bg-danger",
|
||
} as const;
|
||
|
||
function WorkbenchItem({ item, onNavigate }: { item: OperationsWorkbenchItem; onNavigate: (target: string) => void }) {
|
||
return (
|
||
<div className="flex items-start gap-3 border-b border-dashed border-border-2 px-4 py-3 last:border-b-0">
|
||
<span className={`mt-1.5 h-8 w-1 shrink-0 rounded-full ${itemToneClasses[item.tone]}`} />
|
||
<div className="min-w-0 flex-1">
|
||
<div className="text-sm font-medium text-foreground">{item.title}</div>
|
||
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">{item.detail}</div>
|
||
</div>
|
||
<Button
|
||
className="shrink-0"
|
||
variant={item.tone === "danger" ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => onNavigate(item.target)}
|
||
>
|
||
{item.action_label}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function EmptyList({ text }: { text: string }) {
|
||
return <div className="flex min-h-[72px] items-center justify-center px-4 py-6 text-center text-xs text-muted-foreground">{text}</div>;
|
||
}
|
||
|
||
export default function OperationsWorkbenchPage() {
|
||
const navigate = useNavigate();
|
||
const { workbench } = useOperationsData();
|
||
const kpis = workbench.kpis.length === 6 ? workbench.kpis : FALLBACK_KPIS;
|
||
const todos = workbench.todos;
|
||
const watches = workbench.watches;
|
||
const activities = workbench.activities;
|
||
const serviceOnline = workbench.kpis.length === 6;
|
||
|
||
return (
|
||
<section className="h-full overflow-auto bg-bg px-6 py-5">
|
||
<div className="flex w-full flex-col gap-4 pb-8">
|
||
<section className="dashboard-hero-shell flex flex-col items-start justify-between gap-6 rounded-xl px-[34px] py-[30px] text-white shadow-lg sm:flex-row sm:items-center">
|
||
<div>
|
||
<span className="text-2xs tracking-[0.12em] opacity-80">MODEL OPERATIONS PLATFORM</span>
|
||
<h2 className="my-2 mb-[5px] text-2xl font-medium">运维工作台</h2>
|
||
<p className="m-0 text-xs opacity-90">一屏内可见 KPI、我的待办、我的关注与最近动态</p>
|
||
</div>
|
||
<span className="rounded-[20px] bg-white/16 px-3 py-2 text-sm">
|
||
{serviceOnline ? "服务已连接" : "服务连接中"}
|
||
</span>
|
||
</section>
|
||
|
||
{workbench.alerts.length > 0 && (
|
||
<div className="flex flex-wrap items-center gap-2 rounded-lg border border-danger/25 bg-danger-soft px-4 py-3 text-sm text-danger">
|
||
<span className="size-2 shrink-0 rounded-full bg-danger" />
|
||
<span className="flex-1">{workbench.alerts.join(" ")}</span>
|
||
<Button variant="outline" size="sm" onClick={() => navigate("/operations/monitoring")}>查看监控概览</Button>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-6 gap-3 max-[1180px]:grid-cols-3">
|
||
{kpis.map((kpi) => (
|
||
<button
|
||
className={`min-w-0 rounded-lg border p-4 text-left shadow-sm transition-colors hover:border-primary/45 ${kpiToneClasses[kpi.tone]}`}
|
||
key={kpi.key}
|
||
type="button"
|
||
onClick={() => navigate(kpi.target)}
|
||
title="点击查看明细"
|
||
>
|
||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||
<span className="truncate">{kpi.label}</span>
|
||
</div>
|
||
<div className="mt-1 flex items-end gap-1">
|
||
<strong className="text-2xl font-semibold tabular-nums text-foreground">{kpi.value}</strong>
|
||
<span className="mb-0.5 text-lg leading-none text-muted-foreground">›</span>
|
||
</div>
|
||
<div className="mt-1 truncate text-xs text-muted-foreground">{kpi.detail}</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4 max-[1180px]:grid-cols-1">
|
||
<div className="min-h-[144px] overflow-hidden rounded-lg border border-border bg-card shadow-sm">
|
||
<div className="flex items-center gap-3 border-b border-border-2 px-4 py-3">
|
||
<h2 className="text-sm font-semibold text-foreground">我的待办</h2>
|
||
<span className="text-xs text-muted-foreground">按催办状态 / 截止日期 / 监控结果等级排序</span>
|
||
<span className="ml-auto text-xs text-muted-foreground">共 {todos.length} 项</span>
|
||
</div>
|
||
{todos.length ? todos.map((item) => <WorkbenchItem item={item} key={item.id} onNavigate={navigate} />) : <EmptyList text="暂无待办" />}
|
||
</div>
|
||
|
||
<div className="min-h-[144px] overflow-hidden rounded-lg border border-border bg-card shadow-sm">
|
||
<div className="flex items-center gap-3 border-b border-border-2 px-4 py-3">
|
||
<h2 className="text-sm font-semibold text-foreground">我的关注</h2>
|
||
<span className="text-xs text-muted-foreground">只收纳“查看进度”类事项,不计入待办</span>
|
||
<span className="ml-auto text-xs text-muted-foreground">共 {watches.length} 项</span>
|
||
</div>
|
||
{watches.length ? watches.map((item) => <WorkbenchItem item={item} key={item.id} onNavigate={navigate} />) : <EmptyList text="暂无关注事项" />}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="min-h-[156px] overflow-hidden rounded-lg border border-border bg-card shadow-sm">
|
||
<div className="border-b border-border-2 px-4 py-3">
|
||
<h2 className="text-sm font-semibold text-foreground">最近动态</h2>
|
||
</div>
|
||
{activities.length ? (
|
||
<ul className="divide-y divide-dashed divide-border-2 px-4">
|
||
{activities.map((item, index) => (
|
||
<li className="flex gap-4 py-3 text-sm" key={`${item.occurred_at}-${index}`}>
|
||
<time className="w-36 shrink-0 text-xs tabular-nums text-muted-foreground">{item.occurred_at.replace("T", " ").slice(0, 16)}</time>
|
||
<span className="min-w-0 text-foreground"><b className="font-medium text-primary">{item.actor}</b> {item.text}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : <EmptyList text="暂无最近动态" />}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|