feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -27,9 +27,8 @@ import {
|
||||
modelTrend,
|
||||
monthsBetween,
|
||||
} from "./modelData";
|
||||
import { REPORTS } from "./reportData";
|
||||
import { useOperationsData, useOperationsModelDetail } from "./OperationsDataContext";
|
||||
import { isOperationsActionVisibleForRole, useOperationsRole } from "./operationsRole";
|
||||
import { useCanOperationsAction } from "./operationsRole";
|
||||
|
||||
function recentMonths(count: number): string[] {
|
||||
const end = 2026 * 12 + 6;
|
||||
@@ -43,7 +42,6 @@ export default function MonitoringDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const { models } = useOperationsData();
|
||||
const operationsRole = useOperationsRole();
|
||||
const modelId = params.modelId ?? models[0]?.modelId ?? "";
|
||||
const detail = useOperationsModelDetail(modelId, "2026-07");
|
||||
const baseModel = detail.model ?? models.find((item) => item.modelId === modelId) ?? models[0];
|
||||
@@ -57,6 +55,7 @@ export default function MonitoringDetailPage() {
|
||||
const [reviewDecision, setReviewDecision] = useState("暂不处理");
|
||||
const [reviewNote, setReviewNote] = useState("");
|
||||
const months = range === "custom" ? monthsBetween(fromMonth, toMonth) : recentMonths(Number(range));
|
||||
const monitorMonth = detail.monitoringResult?.monitorMonth ?? "";
|
||||
const ksTrend = modelTrend(model, "ks", months);
|
||||
const psiTrend = modelTrend(model, "psi", months);
|
||||
const compareModel = models.find((item) => item.modelId === compareId && item.modelId !== model.modelId) ?? null;
|
||||
@@ -64,12 +63,11 @@ export default function MonitoringDetailPage() {
|
||||
const comparePsiTrend = compareModel ? modelTrend(compareModel, "psi", months) : null;
|
||||
const grade = gradeOf(model);
|
||||
const abnormal = abnormalLevelOf(model);
|
||||
const linkedReport = REPORTS.find((report) => report.modelId === model.modelId && report.monitorMonth === "2026-07") ?? null;
|
||||
const chosenFeature = FEATURE_METRICS.find((item) => item.key === selectedFeature) ?? null;
|
||||
const ivTop = useMemo(() => [...FEATURE_METRICS].sort((left, right) => right.ivDrop - left.ivDrop), []);
|
||||
const csiTop = useMemo(() => [...FEATURE_METRICS].sort((left, right) => right.csiRise - left.csiRise), []);
|
||||
const canInitialReview = isOperationsActionVisibleForRole(operationsRole, "monitor:initial-review");
|
||||
const canFinalReview = isOperationsActionVisibleForRole(operationsRole, "monitor:final-review");
|
||||
const canInitialReview = useCanOperationsAction("monitor:initial-review");
|
||||
const canFinalReview = useCanOperationsAction("monitor:final-review");
|
||||
const effectiveReviewStage = grade === "A" ? 2 : reviewStage;
|
||||
const canReview = grade !== "A" && ((reviewStage === 0 && canInitialReview) || (reviewStage === 1 && canFinalReview));
|
||||
|
||||
@@ -97,7 +95,7 @@ export default function MonitoringDetailPage() {
|
||||
}, [modelId]);
|
||||
|
||||
if (detail.loading) {
|
||||
return <section className="h-full overflow-auto bg-bg p-6"><div className="mx-auto max-w-screen-2xl space-y-6"><Skeleton className="h-20 w-full rounded-4xl" /><Skeleton className="h-96 w-full rounded-4xl" /><div className="grid grid-cols-2 gap-6"><Skeleton className="h-72 rounded-4xl" /><Skeleton className="h-72 rounded-4xl" /></div></div></section>;
|
||||
return <section className="h-full overflow-auto bg-bg p-6"><div className="w-full space-y-6"><Skeleton className="h-20 w-full rounded-4xl" /><Skeleton className="h-96 w-full rounded-4xl" /><div className="grid grid-cols-2 gap-6"><Skeleton className="h-72 rounded-4xl" /><Skeleton className="h-72 rounded-4xl" /></div></div></section>;
|
||||
}
|
||||
|
||||
if (detail.error) {
|
||||
@@ -106,7 +104,7 @@ export default function MonitoringDetailPage() {
|
||||
|
||||
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={`${model.bank} · ${model.name} ${model.version} · ${model.modelId}`}
|
||||
@@ -155,7 +153,7 @@ export default function MonitoringDetailPage() {
|
||||
<Card>
|
||||
<CardHeader className="border-b border-border">
|
||||
<CardTitle>① 监控结论</CardTitle>
|
||||
<CardDescription>2026-07 监控周期 · 结果优先展示</CardDescription>
|
||||
<CardDescription>{monitorMonth ? `${monitorMonth} 监控周期 · 结果优先展示` : "暂无监控结果"}</CardDescription>
|
||||
<CardAction className="flex items-center gap-2">
|
||||
<StatusBadge status={model.status} />
|
||||
<GradeBadge grade={grade} suffix="等级" />
|
||||
@@ -213,8 +211,8 @@ export default function MonitoringDetailPage() {
|
||||
<FileText className="mt-0.5 size-5 shrink-0 text-primary" />
|
||||
<div className="flex-1">
|
||||
<b className="text-sm text-foreground">关联报告</b>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{grade === "A" ? "模型监控报告" : "模型诊断报告"} · 2026-07 · 输出日期 2026-08-15</p>
|
||||
<Button className="mt-2 px-0" variant="link" size="sm" onClick={() => navigate(linkedReport ? `/operations/reports?report=${linkedReport.reportId}` : "/operations/reports")}>打开报告 <ArrowRight /></Button>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{monitorMonth ? `${grade === "A" ? "模型监控报告" : "模型诊断报告"} · ${monitorMonth}` : "暂无关联报告"}</p>
|
||||
<Button className="mt-2 px-0" variant="link" size="sm" onClick={() => navigate("/operations/reports")}>打开报告 <ArrowRight /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,7 +237,7 @@ export default function MonitoringDetailPage() {
|
||||
</div>
|
||||
|
||||
<Card size="sm">
|
||||
<CardHeader><CardTitle>排序性趋势(当月)</CardTitle><CardDescription>2026-07 · 蓝色柱为客户数,橙色折线为坏客户占比</CardDescription></CardHeader>
|
||||
<CardHeader><CardTitle>排序性趋势(当月)</CardTitle><CardDescription>{monitorMonth || "当期"} · 蓝色柱为客户数,橙色折线为坏客户占比</CardDescription></CardHeader>
|
||||
<CardContent><SortingComboChart {...SORTING_DISTRIBUTION} /></CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -312,22 +310,9 @@ export default function MonitoringDetailPage() {
|
||||
<CardContent className="border-t border-border">
|
||||
<div className="mb-4 flex items-start gap-3 rounded-xl bg-brand-soft p-4 text-sm text-primary">
|
||||
<Info className="mt-0.5 size-5 shrink-0" />
|
||||
<div><b>{chosenFeature.name}({chosenFeature.key})分布变化</b><p className="mt-1 text-primary/80">对比基准期与 2026-07 当期各分箱占比,正式数据由共享指标库读取。</p></div>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
{[38, 27, 18, 11, 6].map((reference, index) => {
|
||||
const current = Math.max(2, reference + [4, -3, 2, -1, -2][index]);
|
||||
return (
|
||||
<div className="rounded-xl border border-border p-4" key={reference}>
|
||||
<span className="text-xs text-muted-foreground">分箱 {index + 1}</span>
|
||||
<div className="mt-3 space-y-2">
|
||||
<div><span className="flex justify-between text-xs"><i>基准期</i><b>{reference}%</b></span><span className="mt-1 block h-2 overflow-hidden rounded-full bg-muted"><i className="block h-full bg-ink-subtle" style={{ width: `${reference * 2}%` }} /></span></div>
|
||||
<div><span className="flex justify-between text-xs"><i>当期</i><b>{current}%</b></span><span className="mt-1 block h-2 overflow-hidden rounded-full bg-muted"><i className="block h-full bg-primary" style={{ width: `${current * 2}%` }} /></span></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div><b>{chosenFeature.name}({chosenFeature.key})分布变化</b><p className="mt-1 text-primary/80">对比基准期与当期各分箱占比,正式数据由后端接口读取。</p></div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-dashed border-border p-8 text-center text-sm text-muted-foreground">暂无特征分布数据</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user