226 lines
12 KiB
TypeScript
226 lines
12 KiB
TypeScript
import { Activity, ArrowRight, Building2, FileChartColumn, Layers3, Radar } from "lucide-react";
|
|
import { useNavigate } from "react-router";
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
|
import { GradeBadge, OperationsPageHeader } from "./OperationsUi";
|
|
import { useOperationsData } from "./OperationsDataContext";
|
|
import { MODEL_CATEGORIES, categoryName, gradeOf, type ModelGrade } from "./modelData";
|
|
|
|
export default function OperationsWorkbenchPage() {
|
|
const navigate = useNavigate();
|
|
const { models, source } = useOperationsData();
|
|
const liveModels = models.filter((model) => model.status !== "下线");
|
|
const bankCount = new Set(liveModels.map((model) => model.bank)).size;
|
|
const gradeCounts = liveModels.reduce<Record<ModelGrade, number>>(
|
|
(counts, model) => ({ ...counts, [gradeOf(model)]: counts[gradeOf(model)] + 1 }),
|
|
{ A: 0, B: 0, C: 0 },
|
|
);
|
|
const pending = liveModels
|
|
.filter((model) => gradeOf(model) !== "A")
|
|
.sort((left, right) => gradeOf(right).localeCompare(gradeOf(left)) || left.ks - right.ks)
|
|
.slice(0, 5);
|
|
const watchItems = [
|
|
{ title: "华东银行 · 标准A卡重构", detail: "当前阶段:方案设计 · 预计 2026-09-18 完成", path: "/operations/workflows" },
|
|
{ title: "滨海银行 · 大额A卡陪跑上线", detail: "当前阶段:模型上线 · 预计 2026-09-25 完成", path: "/operations/workflows" },
|
|
{ title: "南岭银行 · 白户A卡模型微调", detail: "当前阶段:开发评审 · 预计 2026-10-08 完成", path: "/operations/workflows" },
|
|
];
|
|
|
|
const metricCards = [
|
|
{ label: "在管模型", value: liveModels.length, detail: "覆盖 4 个模型大类", Icon: Layers3 },
|
|
{ label: "接入银行", value: bankCount, detail: "本月均已完成监控", Icon: Building2 },
|
|
{ label: "B / C 等级", value: gradeCounts.B + gradeCounts.C, detail: `${gradeCounts.C} 个需重点处理`, Icon: Radar },
|
|
{ label: "最新报告", value: 6, detail: "2026-07 监控周期", Icon: FileChartColumn },
|
|
];
|
|
|
|
return (
|
|
<section className="h-full overflow-auto bg-bg p-6">
|
|
<div className="mx-auto flex max-w-screen-2xl flex-col gap-6 pb-8">
|
|
<OperationsPageHeader
|
|
title="我的工作台"
|
|
description="独立展示模型上线后的运行状态、监控结果和待处理事项;模型开发侧功能保持不变。"
|
|
actions={(
|
|
<Button onClick={() => navigate("/operations/monitoring")}>
|
|
<Activity />查看监控明细
|
|
</Button>
|
|
)}
|
|
/>
|
|
|
|
<Card className="bg-[linear-gradient(125deg,var(--sidebar-background),var(--color-brand))] text-white ring-0">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl font-bold text-white">2026 年 7 月监控周期已完成</CardTitle>
|
|
<CardDescription className="max-w-3xl text-white/80">
|
|
共完成 {liveModels.length} 个模型的月度监控,当前 {gradeCounts.C} 个 C 等级模型需要优先处理,
|
|
模型团队初审后流转至业务团队终审。
|
|
</CardDescription>
|
|
<CardAction>
|
|
<span className="inline-flex rounded-full bg-white/15 px-3 py-1.5 text-xs font-medium text-white">
|
|
{source === "api" ? "真实接口" : "Mock 数据"} · 更新于 2026-08-15 06:12
|
|
</span>
|
|
</CardAction>
|
|
</CardHeader>
|
|
<CardContent className="flex gap-2">
|
|
<Button className="bg-white text-brand hover:bg-white/90" onClick={() => navigate("/operations/models")}>
|
|
模型大类概览 <ArrowRight />
|
|
</Button>
|
|
<Button className="border-white/30 bg-transparent text-white hover:bg-white/10" variant="outline" onClick={() => navigate("/operations/monitoring")}>
|
|
进入监控明细
|
|
</Button>
|
|
<Button className="border-white/30 bg-transparent text-white hover:bg-white/10" variant="outline" onClick={() => navigate("/operations/banks")}>
|
|
细分银行概览
|
|
</Button>
|
|
<Button className="border-white/30 bg-transparent text-white hover:bg-white/10" variant="outline" onClick={() => navigate("/operations/report-summary")}>
|
|
历史报告汇总
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="grid grid-cols-4 gap-4">
|
|
{metricCards.map(({ label, value, detail, Icon }) => (
|
|
<Card key={label} size="sm">
|
|
<CardContent className="flex items-center gap-3">
|
|
<span className="grid size-10 shrink-0 place-items-center rounded-xl bg-brand-soft text-primary">
|
|
<Icon className="size-5" />
|
|
</span>
|
|
<span className="min-w-0">
|
|
<strong className="block text-xl font-bold tabular-nums text-foreground">{value}</strong>
|
|
<span className="block text-sm font-medium text-foreground">{label}</span>
|
|
<small className="block truncate text-xs text-muted-foreground">{detail}</small>
|
|
</span>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-[minmax(0,0.8fr)_minmax(0,1.4fr)] gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>监控结果概览</CardTitle>
|
|
<CardDescription>点击等级进入对应模型明细</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{(["A", "B", "C"] as ModelGrade[]).map((grade) => {
|
|
const total = liveModels.length || 1;
|
|
const count = gradeCounts[grade];
|
|
return (
|
|
<button
|
|
className="group flex w-full cursor-pointer items-center gap-3 rounded-xl border border-border bg-background p-3 text-left transition-colors hover:bg-muted/50"
|
|
key={grade}
|
|
type="button"
|
|
onClick={() => navigate(`/operations/monitoring?grade=${grade}`)}
|
|
>
|
|
<GradeBadge grade={grade} />
|
|
<span className="min-w-0 flex-1">
|
|
<span className="flex items-center justify-between text-sm">
|
|
<b>{grade === "A" ? "运行正常" : grade === "B" ? "需要关注" : "需要处理"}</b>
|
|
<strong className="tabular-nums">{count} 个</strong>
|
|
</span>
|
|
<span className="mt-2 block h-2 overflow-hidden rounded-full bg-muted">
|
|
<i className="block h-full rounded-full bg-primary transition-all" style={{ width: `${count / total * 100}%` }} />
|
|
</span>
|
|
</span>
|
|
<ArrowRight className="size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
|
</button>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>我的待办</CardTitle>
|
|
<CardDescription>除“查看进度”外,需要当前角色处理的事项</CardDescription>
|
|
<CardAction>
|
|
<Button variant="ghost" size="sm" onClick={() => navigate("/operations/monitoring")}>查看全部</Button>
|
|
</CardAction>
|
|
</CardHeader>
|
|
<CardContent className="px-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>银行 / 模型</TableHead>
|
|
<TableHead>模型ID</TableHead>
|
|
<TableHead>KS</TableHead>
|
|
<TableHead>PSI</TableHead>
|
|
<TableHead>等级</TableHead>
|
|
<TableHead className="text-right">操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{pending.map((model) => (
|
|
<TableRow key={model.modelId}>
|
|
<TableCell>
|
|
<strong className="block text-foreground">{model.bank}</strong>
|
|
<small className="text-muted-foreground">{categoryName(model.category)} · {model.version}</small>
|
|
</TableCell>
|
|
<TableCell className="font-mono text-xs">{model.modelId}</TableCell>
|
|
<TableCell className="tabular-nums">{model.ks.toFixed(2)}%</TableCell>
|
|
<TableCell className="tabular-nums">{model.psi.toFixed(2)}%</TableCell>
|
|
<TableCell><GradeBadge grade={gradeOf(model)} /></TableCell>
|
|
<TableCell className="text-right">
|
|
<Button variant="link" size="sm" onClick={() => navigate(`/operations/monitoring/${model.modelId}`)}>
|
|
去处理
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>我的关注</CardTitle>
|
|
<CardDescription>仅收录以“查看进度”为动作的流程事项</CardDescription>
|
|
<CardAction><Button variant="ghost" size="sm" onClick={() => navigate("/operations/workflows")}>查看全部</Button></CardAction>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-3 gap-4">
|
|
{watchItems.map((item) => (
|
|
<div className="flex min-w-0 items-start gap-3 rounded-xl border border-border bg-background p-4" key={item.title}>
|
|
<span className="mt-1 size-2 shrink-0 rounded-full bg-primary" />
|
|
<span className="min-w-0 flex-1"><b className="block truncate text-sm text-foreground">{item.title}</b><small className="mt-1 block text-xs leading-5 text-muted-foreground">{item.detail}</small></span>
|
|
<Button className="shrink-0" variant="outline" size="xs" onClick={() => navigate(item.path)}>查看进度</Button>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>模型资产分布</CardTitle>
|
|
<CardDescription>按模型大类查看在管模型与版本情况</CardDescription>
|
|
<CardAction>
|
|
<Button variant="outline" size="sm" onClick={() => navigate("/operations/models")}>查看大类概览</Button>
|
|
</CardAction>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-4 gap-4">
|
|
{MODEL_CATEGORIES.map((category) => {
|
|
const models = liveModels.filter((model) => model.category === category.id);
|
|
return (
|
|
<button
|
|
className="group rounded-xl border border-border bg-background p-4 text-left transition-colors hover:border-primary/40 hover:bg-brand-soft/50"
|
|
key={category.id}
|
|
type="button"
|
|
onClick={() => navigate(`/operations/models?category=${category.id}`)}
|
|
>
|
|
<span className="flex items-center justify-between">
|
|
<b className="text-sm text-foreground">{category.name}</b>
|
|
<ArrowRight className="size-4 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
|
</span>
|
|
<strong className="mt-3 block text-xl font-bold tabular-nums text-foreground">{models.length}</strong>
|
|
<small className="text-xs text-muted-foreground">
|
|
{new Set(models.map((model) => model.bank)).size} 家银行 · {new Set(models.map((model) => model.version)).size} 个版本
|
|
</small>
|
|
</button>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|