import { useMemo, useState } from "react";
import { Download, FileCheck2, Save, Send, TriangleAlert } 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";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { FilterSelect, MetricLineChart, OperationsPageHeader, SortingComboChart } from "./OperationsUi";
import { FEATURE_METRICS, MONITOR_MONTHS, SORTING_DISTRIBUTION, modelTrend } from "./modelData";
import { useOperationsData } from "./OperationsDataContext";
import { useCanOperationsAction } from "./operationsRole";
import { latestReports, type ReportStatus, type ReportType } from "./reportData";
import { useReportStore } from "./reportStore";
type Filters = { bank: string; name: string; version: string; modelId: string };
function unique(values: string[]): string[] {
return [...new Set(values)].sort((left, right) => left.localeCompare(right, "zh-CN"));
}
function statusClass(status: ReportStatus): string {
if (status === "已发送业务团队") return "bg-success-soft text-success-strong";
if (status === "编辑中") return "bg-brand-soft text-primary";
return "bg-warning-soft text-warning";
}
function ReportTypeBadge({ type }: { type: ReportType }) {
return {type};
}
function ScoreDistributionTable() {
return (
分数区间总账户数账户占比账户累计占比
暂无评分分布数据
);
}
export default function ReportPage() {
const navigate = useNavigate();
const { models } = useOperationsData();
const [searchParams] = useSearchParams();
const requestedId = searchParams.get("report");
const allReports = useReportStore((state) => state.reports);
const updateReport = useReportStore((state) => state.updateReport);
const reports = requestedId && allReports.some((report) => report.reportId === requestedId) ? allReports : latestReports(allReports);
const [filters, setFilters] = useState({ bank: "", name: "", version: "", modelId: "" });
const [selectedId, setSelectedId] = useState(requestedId ?? reports[0]?.reportId ?? "");
const pool = reports.filter((report) => (
(!filters.bank || report.bank === filters.bank)
&& (!filters.name || report.modelName === filters.name)
&& (!filters.version || report.version === filters.version)
&& (!filters.modelId || report.modelId === filters.modelId)
));
const report = pool.find((item) => item.reportId === selectedId) ?? pool[0] ?? null;
const model = report ? models.find((item) => item.modelId === report.modelId) ?? null : null;
const isHistorical = Boolean(report && report.monitorMonth !== "2026-07");
const ksTrend = model ? modelTrend(model, "ks") : [];
const psiTrend = model ? modelTrend(model, "psi") : [];
const canEdit = !isHistorical && useCanOperationsAction("report:edit");
const canExport = useCanOperationsAction("report:export");
const canSend = !isHistorical && useCanOperationsAction("report:send");
const updateFilter = (key: K, value: Filters[K]) => {
setFilters((current) => ({ ...current, [key]: value }));
};
const reportOptions = useMemo(() => pool.map((item) => ({
label: `${item.bank} · ${item.modelName} ${item.version} · ${item.monitorMonth} · ${item.type}`,
value: item.reportId,
})), [pool]);
const printReport = () => {
document.body.classList.add("printing-operations-report");
window.print();
window.setTimeout(() => document.body.classList.remove("printing-operations-report"), 300);
};
return (
{canExport && }{canEdit && }{canSend && report.status !== "已发送业务团队" && }>}
/>
报告筛选{isHistorical ? "历史报告只读查看" : "本页默认仅保留最新月份;历史报告请进入历史报告汇总"}
item.bank))} onChange={(value) => updateFilter("bank", value)} />
item.modelName))} onChange={(value) => updateFilter("name", value)} />
item.version))} onChange={(value) => updateFilter("version", value)} />
item.modelId))} onChange={(value) => updateFilter("modelId", value)} />
{report && model ? (
<>
{report.unreadDays >= 5 && report.status !== "已发送业务团队" && 阅读催办该报告已超过 {report.unreadDays} 个工作日未阅读,系统已发送催办短信。
}
{report.bank} · {report.modelName} · {report.version}信用卡申请评分模型{report.type}{report.status}{report.synced && 已同步}
一、申请评分模型{report.type === "诊断报告" ? "诊断" : "监控"}说明
申请评分模型使用行内存量数据验证区分能力与稳定性。风险区分度采用具有完整表现期的核准账户样本,评分分布和稳定性采用本期申请有评分账户进行验证。
二、{report.modelName}申请评分模型效果验证
{[
["验证样本", "—"], ["坏样本", "—"], ["KS", `${model.ks.toFixed(2)}%`], ["PSI", `${model.psi.toFixed(2)}%`],
].map(([label, value]) =>
{label}{value}
)}
(二)模型评分排序能力
{model.ranking === "相符" ? "随着评分增加,坏账户占比逐渐降低,模型排序能力表现稳定。" : "随着评分增加,坏账户占比未保持单调下降,中段评分区间出现反升,建议结合特征层面进一步排查。"}
(三)评分风险区分度验证KS 分档线 40%,干预线 30%
(四)评分稳定性验证PSI 使用滚动基准期
{report.type === "诊断报告" && (五)入模特征诊断
特征IV(当期)IV 降幅CSI(当期)CSI 升幅诊断结论{FEATURE_METRICS.map((feature) => {feature.name}{feature.key}{feature.iv.toFixed(3)}-{feature.ivDrop}%{feature.csi.toFixed(3)}+{feature.csiRise}pp{feature.csiRise > 2 ? "分布迁移明显,建议核查上游数据口径" : "分布基本稳定"})}
}
三、总结
本次模型{report.type === "诊断报告" ? "诊断" : "监控"}显示:模型 KS 为 {model.ks.toFixed(2)}%,PSI 为 {model.psi.toFixed(2)}%;{model.ranking === "相符" ? "排序性保持稳定。" : "排序性存在反转,建议启动模型微调或重构评估。"}
报告中的指标由平台程序取数计算,大模型仅负责文字表述与归因;正文不展示监控结果等级。
>
) : 当前筛选条件下没有报告}
);
}