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 请进入模型监控详情查看。
>
) : (
当前筛选条件下没有匹配的已上线模型
)}
);
}