import { useMemo, useRef, useState } from "react"; import { ArrowRight, Download, FileSpreadsheet, Upload } from "lucide-react"; import { useNavigate } 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 { usePersistentState } from "~/hooks/use-persistent-state"; import { exportRowsToExcel } from "~/lib/exportExcel"; import { FilterSelect, OperationsPageHeader, StatusBadge } from "./OperationsUi"; import { useOperationsData } from "./OperationsDataContext"; import type { ModelRecord } from "./modelData"; type Filters = { bank: string; name: string; modelId: string; modelType: string; }; function unique(values: string[]): string[] { return [...new Set(values)].sort((left, right) => left.localeCompare(right, "zh-CN")); } function BarList({ values, labels, suffix = "%", tone = "brand", }: { values: number[]; labels: string[]; suffix?: string; tone?: "brand" | "warning" | "success"; }) { const max = Math.max(...values, 1); const colorClass = tone === "warning" ? "bg-warning" : tone === "success" ? "bg-success" : "bg-brand"; return (
{values.length ? values.map((value, index) => (
{labels[index]} {value.toFixed(2)}{suffix}
)) :
暂无指标数据
}
); } export default function DeployedModelsPage() { const navigate = useNavigate(); const { models } = useOperationsData(); const pool = useMemo(() => models.filter((model) => model.status !== "下线"), [models]); const [filters, setFilters] = useState({ bank: "", name: "", modelId: "", modelType: "" }); const [selectedModelId, setSelectedModelId] = useState(pool[0]?.modelId ?? ""); const [scoreFiles, setScoreFiles] = usePersistentState>("a-card-score-files", {}); const scoreInputRef = useRef(null); const filtered = pool.filter((model) => ( (!filters.bank || model.bank === filters.bank) && (!filters.name || model.name === filters.name) && (!filters.modelId || model.modelId === filters.modelId) && (!filters.modelType || (model.commonModel ? "通用模型" : "个性化模型") === filters.modelType) )); const model = filtered.find((item) => item.modelId === selectedModelId) ?? filtered[0] ?? null; const scoreLabels = ["低分段", "中低分", "中分段", "中高分", "高分段", "最高分"]; const rankingRates: number[] = []; const liftValues: number[] = []; const psiValues: number[] = []; const scoreFile = model ? scoreFiles[model.modelId] : undefined; const update = (key: K, value: Filters[K]) => { setFilters((current) => ({ ...current, [key]: value })); setSelectedModelId(""); }; const uploadScoreFile = (file: File | undefined) => { if (!model || !file) return; if (!/\.(xlsx|xls)$/i.test(file.name)) { toast.error("请选择 .xlsx 或 .xls 文件"); return; } if (file.size > 20 * 1024 * 1024) { toast.error("文件不能超过 20 MB"); return; } setScoreFiles((current) => ({ ...current, [model.modelId]: { name: file.name, size: file.size, updatedAt: new Date().toLocaleString("zh-CN", { hour12: false }) } })); toast.success("评分逻辑文件已加入本地上传队列"); }; const downloadScoreFile = () => { if (!model || !scoreFile) return; toast.info("评分逻辑文件下载接口尚未接入"); }; return (
navigate(`/operations/monitoring/${model.modelId}`)}>查看监控详情 } /> 模型筛选 筛出 {filtered.length} 个模型
item.bank))} onChange={(value) => update("bank", value)} /> item.name))} onChange={(value) => update("name", value)} /> item.modelId)} onChange={(value) => update("modelId", value)} /> update("modelType", value)} />
({ label: `${item.bank} · ${item.name} ${item.version}(${item.modelId})`, value: item.modelId }))} onChange={setSelectedModelId} />
{model ? ( <> {model.bank} · {model.name} {model.version} {model.modelId} {model.commonModel ? `通用 · ${model.commonModel}` : "个性化模型"} {[ ["银行 × 模型ID", `${model.bank} × ${model.modelId}`], ["版本", model.version], ["开发人员", "—"], ["上线日期", "—"], ["最近迭代", model.iteratedAt], ["陪跑开始", "—"], ["陪跑结束", "—"], ["下线日期", "—"], ["状态", model.status], ].map(([label, value]) => (
{label}{value}
))}
开发时点 · 排序性各评分区间坏客户占比

开发时点排序性相符:评分越高,坏客户占比越低。

开发时点 · KS 与 LIFT暂无开发时点指标 开发时点 · PSI暂无开发时点指标

上线后的滚动 PSI 请进入模型监控详情查看。

评分逻辑以 Excel 文件维护,不在页面逐行展开
{scoreFile?.name ?? "暂无评分逻辑文件"}{scoreFile ? `本地待上传 · ${(scoreFile.size / 1024).toFixed(1)} KB · ${scoreFile.updatedAt}` : "未关联评分逻辑文件"}
{ uploadScoreFile(event.target.files?.[0]); event.target.value = ""; }} />

增量模型由开发评审材料自动带入;存量模型由模型团队手工上传。

模型开发材料材料存放于开发评审域,本页仅提供链接 材料环节操作暂无模型开发材料
) : ( 当前筛选条件下没有匹配的已上线模型 )}
); }