feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { ArrowRight, Download, TrendingDown, TrendingUp } from "lucide-react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
@@ -37,13 +36,12 @@ type BankCategoryCard = {
|
||||
modelCount: number;
|
||||
versions: number;
|
||||
latestIteration: string;
|
||||
averageCycle: number;
|
||||
averageCycle: number | null;
|
||||
ownKs: number;
|
||||
ownPsi: number;
|
||||
peerKs: number;
|
||||
peerPsi: number;
|
||||
grades: Record<ModelGrade, number>;
|
||||
demo?: boolean;
|
||||
};
|
||||
|
||||
function isCategoryId(value: string | null): value is ModelCategoryId {
|
||||
@@ -114,14 +112,13 @@ export default function BankOverviewPage() {
|
||||
const grades = bankModels.reduce<Record<ModelGrade, number>>((result, model) => ({ ...result, [gradeOf(model)]: result[gradeOf(model)] + 1 }), { A: 0, B: 0, C: 0 });
|
||||
return { ...item, models: bankModels, modelCount: bankModels.length, versions: new Set(bankModels.map((model) => model.version)).size, latestIteration: latestIterationDate(bankModels), averageCycle: averageIterationCycle(bankModels), ownKs: average(bankModels.map((model) => model.ks)), ownPsi: average(bankModels.map((model) => model.psi)), peerKs: average(allPeers.map((model) => model.ks)), peerPsi: average(allPeers.map((model) => model.psi)), grades };
|
||||
});
|
||||
return [...realCards, { id: "consumer-demo", name: "消费贷评分(示意)", models: [], modelCount: 1, versions: 1, latestIteration: "2026-07-18", averageCycle: 11, ownKs: 37.9, ownPsi: 16.6, peerKs: 39.6, peerPsi: 14.2, grades: { A: 0, B: 1, C: 0 }, demo: true }];
|
||||
return realCards;
|
||||
}, [bank, models]);
|
||||
|
||||
const syncParams = (nextBank: string, nextCategory: ModelCategoryId) => setSearchParams({ bank: nextBank, category: nextCategory });
|
||||
const selectBank = (value: string) => { setBank(value); syncParams(value, category); };
|
||||
const selectCategory = (value: ModelCategoryId, scroll = false) => { setCategory(value); syncParams(bank, value); if (scroll) requestAnimationFrame(() => trendRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })); };
|
||||
const openGrade = (item: BankCategoryCard, grade: ModelGrade) => {
|
||||
if (item.demo) return toast.info("该卡片仅用于展示细分银行第 5 个大类的纵向滚动效果");
|
||||
const gradeModels = item.models.filter((model) => gradeOf(model) === grade);
|
||||
if (gradeModels.length === 1) return navigate(`/operations/monitoring/${gradeModels[0]?.modelId}`);
|
||||
setDrilldown({ category: item.id as ModelCategoryId, grade, models: gradeModels });
|
||||
@@ -129,7 +126,7 @@ export default function BankOverviewPage() {
|
||||
|
||||
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">
|
||||
<div className="flex w-full flex-col gap-6 pb-8">
|
||||
<OperationsPageHeader title="细分银行概览" description="按银行和模型大类查看本行表现,并与同业均值对照。" actions={<Button variant="outline" onClick={() => navigate("/operations/deployed-models")}>查看已上线模型 <ArrowRight /></Button>} />
|
||||
|
||||
<Card size="sm"><CardContent className="flex items-end gap-4"><FilterSelect className="w-56" label="筛选银行" value={bank} allLabel="请选择银行" options={banks} onChange={selectBank} /><p className="pb-2 text-sm text-muted-foreground">选择银行后,卡片展示本行指标及同业对比</p></CardContent></Card>
|
||||
@@ -137,12 +134,12 @@ export default function BankOverviewPage() {
|
||||
<CategoryCardRail>
|
||||
{cards.map((item) => {
|
||||
const total = item.grades.A + item.grades.B + item.grades.C || 1;
|
||||
const selected = !item.demo && item.id === category;
|
||||
const selected = item.id === category;
|
||||
const hasModels = item.modelCount > 0;
|
||||
const activate = () => item.demo ? toast.info("扩展示意卡:正式接入第 5 个大类后沿用相同结构") : selectCategory(item.id as ModelCategoryId, true);
|
||||
return <Card className={`cursor-pointer transition-all hover:-translate-y-0.5 hover:ring-1 hover:ring-primary/40 ${selected ? "ring-2 ring-primary" : ""}`} key={item.id} role="button" size="sm" tabIndex={0} onClick={activate} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") activate(); }}><CardHeader><CardTitle className="flex items-center gap-2">{item.name}<span className="rounded-lg bg-muted px-2 py-1 text-2xs font-medium text-muted-foreground">{item.demo ? "扩展示意" : `银行-${bank}`}</span></CardTitle></CardHeader><CardContent className="space-y-3">
|
||||
const activate = () => selectCategory(item.id as ModelCategoryId, true);
|
||||
return <Card className={`cursor-pointer transition-all hover:-translate-y-0.5 hover:ring-1 hover:ring-primary/40 ${selected ? "ring-2 ring-primary" : ""}`} key={item.id} role="button" size="sm" tabIndex={0} onClick={activate} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") activate(); }}><CardHeader><CardTitle className="flex items-center gap-2">{item.name}<span className="rounded-lg bg-muted px-2 py-1 text-2xs font-medium text-muted-foreground">银行-{bank || "全部"}</span></CardTitle></CardHeader><CardContent className="space-y-3">
|
||||
<dl className="grid grid-cols-3 gap-3">{[["银行数", hasModels ? 1 : 0], ["模型数", item.modelCount], ["版本数", item.versions]].map(([label, value]) => <div key={label}><dt className="whitespace-nowrap text-2xs text-ink-caption">{label}</dt><dd className="mt-1 text-lg font-bold tabular-nums">{value}</dd></div>)}</dl>
|
||||
<dl className="grid grid-cols-2 gap-3"><div><dt className="text-2xs text-ink-caption">最近迭代日期</dt><dd className="mt-1 whitespace-nowrap text-sm font-bold tabular-nums">{item.latestIteration}</dd></div><div><dt className="whitespace-nowrap text-2xs text-ink-caption">平均迭代周期</dt><dd className="mt-1 whitespace-nowrap text-lg font-bold tabular-nums">{hasModels ? `${item.averageCycle.toFixed(1)}月` : "—"}</dd></div></dl>
|
||||
<dl className="grid grid-cols-2 gap-3"><div><dt className="text-2xs text-ink-caption">最近迭代日期</dt><dd className="mt-1 whitespace-nowrap text-sm font-bold tabular-nums">{item.latestIteration}</dd></div><div><dt className="whitespace-nowrap text-2xs text-ink-caption">平均迭代周期</dt><dd className="mt-1 whitespace-nowrap text-lg font-bold tabular-nums">{hasModels && item.averageCycle !== null ? `${item.averageCycle.toFixed(1)}月` : "—"}</dd></div></dl>
|
||||
<dl className="grid grid-cols-2 gap-3"><div><dt className="text-2xs text-ink-caption">本行平均 KS</dt><dd className="mt-1 text-xl font-bold tabular-nums">{hasModels ? `${item.ownKs.toFixed(1)}%` : "—"}</dd></div><div><dt className="text-2xs text-ink-caption">本行平均 PSI</dt><dd className="mt-1 text-xl font-bold tabular-nums">{hasModels ? `${item.ownPsi.toFixed(1)}%` : "—"}</dd></div><div><dt className="text-2xs text-warning">同业平均 KS</dt><dd className="mt-1 text-xl font-bold tabular-nums text-warning">{item.peerKs.toFixed(1)}%</dd></div><div><dt className="text-2xs text-warning">同业平均 PSI</dt><dd className="mt-1 text-xl font-bold tabular-nums text-warning">{item.peerPsi.toFixed(1)}%</dd></div></dl>
|
||||
<div className="flex h-2 overflow-hidden rounded-full bg-muted"><i className="bg-success" style={{ width: `${item.grades.A / total * 100}%` }} /><i className="bg-warning" style={{ width: `${item.grades.B / total * 100}%` }} /><i className="bg-danger" style={{ width: `${item.grades.C / total * 100}%` }} /></div>
|
||||
<div className="flex flex-wrap gap-2" onClick={(event) => event.stopPropagation()}>{(["A", "B", "C"] as ModelGrade[]).map((grade) => <GradeBadge grade={grade} key={grade} suffix={`${item.grades[grade]} 个模型`} onClick={() => openGrade(item, grade)} />)}</div>
|
||||
|
||||
Reference in New Issue
Block a user