feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -9,7 +9,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~
|
||||
import { FilterSelect, MetricLineChart, OperationsPageHeader, SortingComboChart } from "./OperationsUi";
|
||||
import { FEATURE_METRICS, MONITOR_MONTHS, SORTING_DISTRIBUTION, modelTrend } from "./modelData";
|
||||
import { useOperationsData } from "./OperationsDataContext";
|
||||
import { isOperationsActionVisibleForRole, useOperationsRole } from "./operationsRole";
|
||||
import { useCanOperationsAction } from "./operationsRole";
|
||||
import { latestReports, type ReportStatus, type ReportType } from "./reportData";
|
||||
import { useReportStore } from "./reportStore";
|
||||
|
||||
@@ -29,28 +29,11 @@ function ReportTypeBadge({ type }: { type: ReportType }) {
|
||||
return <span className={type === "监控报告" ? "inline-flex rounded-full bg-success-soft px-2.5 py-1 text-xs font-medium text-success-strong" : "inline-flex rounded-full bg-warning-soft px-2.5 py-1 text-xs font-medium text-warning"}>{type}</span>;
|
||||
}
|
||||
|
||||
function ScoreDistributionTable({ modelId }: { modelId: string }) {
|
||||
const offset = [...modelId].reduce((sum, character) => sum + character.charCodeAt(0), 0) % 5;
|
||||
const rows = [
|
||||
{ bin: "(0,580]", count: 310 + offset * 7 },
|
||||
{ bin: "[580,620)", count: 742 + offset * 11 },
|
||||
{ bin: "[620,660)", count: 2645 + offset * 19 },
|
||||
{ bin: "[660,700)", count: 4812 + offset * 23 },
|
||||
{ bin: "[700,+)", count: 3657 + offset * 17 },
|
||||
];
|
||||
const total = rows.reduce((sum, row) => sum + row.count, 0);
|
||||
let cumulative = 0;
|
||||
function ScoreDistributionTable() {
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader><TableRow><TableHead>分数区间</TableHead><TableHead>总账户数</TableHead><TableHead>账户占比</TableHead><TableHead>账户累计占比</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row) => {
|
||||
const share = row.count / total;
|
||||
cumulative += share;
|
||||
return <TableRow key={row.bin}><TableCell className="font-mono text-xs">{row.bin}</TableCell><TableCell className="tabular-nums">{row.count.toLocaleString("zh-CN")}</TableCell><TableCell className="tabular-nums">{(share * 100).toFixed(2)}%</TableCell><TableCell className="tabular-nums">{(cumulative * 100).toFixed(2)}%</TableCell></TableRow>;
|
||||
})}
|
||||
<TableRow><TableCell className="font-semibold">合计</TableCell><TableCell className="font-semibold tabular-nums">{total.toLocaleString("zh-CN")}</TableCell><TableCell className="font-semibold">100.00%</TableCell><TableCell>—</TableCell></TableRow>
|
||||
</TableBody>
|
||||
<TableBody><TableRow><TableCell className="h-28 text-center text-muted-foreground" colSpan={4}>暂无评分分布数据</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -58,7 +41,6 @@ function ScoreDistributionTable({ modelId }: { modelId: string }) {
|
||||
export default function ReportPage() {
|
||||
const navigate = useNavigate();
|
||||
const { models } = useOperationsData();
|
||||
const operationsRole = useOperationsRole();
|
||||
const [searchParams] = useSearchParams();
|
||||
const requestedId = searchParams.get("report");
|
||||
const allReports = useReportStore((state) => state.reports);
|
||||
@@ -77,8 +59,9 @@ export default function ReportPage() {
|
||||
const isHistorical = Boolean(report && report.monitorMonth !== "2026-07");
|
||||
const ksTrend = model ? modelTrend(model, "ks") : [];
|
||||
const psiTrend = model ? modelTrend(model, "psi") : [];
|
||||
const canEdit = !isHistorical && isOperationsActionVisibleForRole(operationsRole, "report:edit");
|
||||
const canSend = !isHistorical && isOperationsActionVisibleForRole(operationsRole, "report:send");
|
||||
const canEdit = !isHistorical && useCanOperationsAction("report:edit");
|
||||
const canExport = useCanOperationsAction("report:export");
|
||||
const canSend = !isHistorical && useCanOperationsAction("report:send");
|
||||
|
||||
const updateFilter = <K extends keyof Filters>(key: K, value: Filters[K]) => {
|
||||
setFilters((current) => ({ ...current, [key]: value }));
|
||||
@@ -97,11 +80,11 @@ export default function ReportPage() {
|
||||
|
||||
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={report ? `${report.type} · ${report.bank} ${report.modelName}` : "监控诊断报告"}
|
||||
description={report ? `${report.monitorMonth} 周期 · 自动生成于 ${report.generatedAt} · 报告输出日期 ${report.outputDate}` : "当前筛选条件下没有报告"}
|
||||
actions={report && <><Button variant="outline" onClick={printReport}><Download />导出 PDF</Button>{canEdit && <Button variant="outline" onClick={() => { updateReport(report.reportId, { status: "编辑中", synced: true }); toast.success("草稿已保存,并同步至历史报告汇总(Mock)"); }}><Save />保存草稿</Button>}{canSend && report.status !== "已发送业务团队" && <Button onClick={() => { updateReport(report.reportId, { status: "已发送业务团队", unreadDays: 0, synced: true }); toast.success("已模拟发送至业务团队"); }}><Send />发送业务团队</Button>}</>}
|
||||
actions={report && <>{canExport && <Button variant="outline" onClick={printReport}><Download />导出 PDF</Button>}{canEdit && <Button variant="outline" onClick={() => { updateReport(report.reportId, { status: "编辑中", synced: true }); toast.info("报告保存接口尚未接入"); }}><Save />保存草稿</Button>}{canSend && report.status !== "已发送业务团队" && <Button onClick={() => toast.info("报告发送接口尚未接入")}><Send />发送业务团队</Button>}</>}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
@@ -135,14 +118,14 @@ export default function ReportPage() {
|
||||
<h3 className="text-xl font-bold text-foreground">二、{report.modelName}申请评分模型效果验证</h3>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[
|
||||
["验证样本", "12,186 户"], ["坏样本", "842 户"], ["KS", `${model.ks.toFixed(2)}%`], ["PSI", `${model.psi.toFixed(2)}%`],
|
||||
["验证样本", "—"], ["坏样本", "—"], ["KS", `${model.ks.toFixed(2)}%`], ["PSI", `${model.psi.toFixed(2)}%`],
|
||||
].map(([label, value]) => <div className="rounded-xl bg-muted/50 p-4" key={label}><span className="text-xs text-muted-foreground">{label}</span><strong className="mt-2 block text-xl tabular-nums text-foreground">{value}</strong></div>)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h4 className="text-base font-semibold text-foreground">(一)本期申请评分分布</h4>
|
||||
<ScoreDistributionTable modelId={model.modelId} />
|
||||
<ScoreDistributionTable />
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
@@ -165,7 +148,7 @@ export default function ReportPage() {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="rounded-xl border border-border p-4 text-xs leading-5 text-muted-foreground">报告中的指标由平台程序取数计算,大模型仅负责文字表述与归因;正文不展示监控结果等级。当前页面为前端 Mock,保存和发送刷新后会重置。</p>
|
||||
<p className="rounded-xl border border-border p-4 text-xs leading-5 text-muted-foreground">报告中的指标由平台程序取数计算,大模型仅负责文字表述与归因;正文不展示监控结果等级。</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user