Files
model-platform/frontend/app/features/operations/KnowledgeBasePage.tsx
T

76 lines
6.1 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { Download, FolderArchive, RotateCcw } from "lucide-react";
import { 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 { exportRowsToExcel } from "~/lib/exportExcel";
import { FilterSelect, OperationsPageHeader } from "./OperationsUi";
import { workflowDocuments, type KnowledgeDocument } from "./workflowData";
type Filters = { bank: string; modelName: string; modelId: string; stage: string };
const EMPTY_FILTERS: Filters = { bank: "", modelName: "", modelId: "", stage: "" };
function unique(values: string[]): string[] {
return [...new Set(values)].sort((left, right) => left.localeCompare(right, "zh-CN"));
}
function exportDocuments(rows: KnowledgeDocument[]) {
const header = ["流程", "银行", "模型名称", "模型版本", "模型ID", "环节", "材料名称", "上传人", "上传时间", "确认状态"];
return exportRowsToExcel({ fileName: "文档知识库", sheetName: "文档知识库", headers: header, rows: rows.map((row) => [row.workflowTitle, row.bank, row.modelName, row.modelVersion, row.modelId, row.stage, row.name, row.uploadedBy, row.uploadedAt, row.confirmed ? "已确认" : "待确认"]) });
}
export default function KnowledgeBasePage() {
const documents = useMemo(() => workflowDocuments(), []);
const [searchParams, setSearchParams] = useSearchParams();
const [filters, setFilters] = useState<Filters>(() => ({
...EMPTY_FILTERS,
bank: searchParams.get("bank") ?? "",
modelName: searchParams.get("modelName") ?? "",
modelId: searchParams.get("modelId") ?? "",
stage: searchParams.get("stage") ?? "",
}));
const rows = useMemo(() => documents.filter((document) => (
(!filters.bank || document.bank === filters.bank)
&& (!filters.modelName || document.modelName === filters.modelName)
&& (!filters.modelId || document.modelId === filters.modelId)
&& (!filters.stage || document.stage === filters.stage)
)), [documents, filters]);
const update = <K extends keyof Filters>(key: K, value: Filters[K]) => setFilters((current) => ({ ...current, [key]: value }));
useEffect(() => {
const params = new URLSearchParams();
(Object.keys(filters) as Array<keyof Filters>).forEach((key) => { if (filters[key]) params.set(key, filters[key]); });
setSearchParams(params, { replace: true });
}, [filters, setSearchParams]);
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">
<OperationsPageHeader
title="文档知识库"
description="汇总开发、迭代、评审、测试和部署各环节材料,按模型、名称和环节检索。"
actions={<Button onClick={() => void exportDocuments(rows)}><Download />导出 Excel</Button>}
/>
<Card>
<CardHeader className="border-b border-border"><CardTitle className="flex items-center gap-2"><FolderArchive className="size-5 text-primary" />流程材料</CardTitle><CardDescription> {rows.length} 份材料</CardDescription><CardAction><Button variant="outline" size="sm" onClick={() => setFilters(EMPTY_FILTERS)}><RotateCcw />重置筛选</Button></CardAction></CardHeader>
<CardContent className="grid grid-cols-4 gap-3">
<FilterSelect label="银行" value={filters.bank} options={unique(documents.map((item) => item.bank))} onChange={(value) => update("bank", value)} />
<FilterSelect label="模型名称" value={filters.modelName} options={unique(documents.map((item) => item.modelName))} onChange={(value) => update("modelName", value)} />
<FilterSelect label="模型 ID" value={filters.modelId} options={unique(documents.map((item) => item.modelId))} onChange={(value) => update("modelId", value)} />
<FilterSelect label="环节" value={filters.stage} options={unique(documents.map((item) => item.stage))} onChange={(value) => update("stage", value)} />
</CardContent>
<CardContent className="px-0 pt-0">
<Table>
<TableHeader><TableRow><TableHead>流程</TableHead><TableHead>银行</TableHead><TableHead>模型名称</TableHead><TableHead>模型版本</TableHead><TableHead>模型ID</TableHead><TableHead>环节</TableHead><TableHead>材料名称</TableHead><TableHead>上传人</TableHead><TableHead>上传时间</TableHead><TableHead>确认状态</TableHead><TableHead className="text-right">操作</TableHead></TableRow></TableHeader>
<TableBody>{rows.length ? rows.map((document) => <TableRow key={`${document.workflowId}-${document.stage}-${document.name}`}><TableCell className="min-w-48 whitespace-normal font-medium text-foreground">{document.workflowTitle}</TableCell><TableCell>{document.bank}</TableCell><TableCell>{document.modelName}</TableCell><TableCell>{document.modelVersion}{document.commonModel && <span className="ml-2 rounded-full bg-brand-soft px-2 py-0.5 text-2xs text-primary">通用</span>}</TableCell><TableCell className="font-mono text-xs">{document.modelId}</TableCell><TableCell>{document.stage}</TableCell><TableCell className="min-w-56 whitespace-normal font-mono text-xs">{document.name}</TableCell><TableCell>{document.uploadedBy}</TableCell><TableCell>{document.uploadedAt}</TableCell><TableCell>{document.confirmed ? <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong">已确认</span> : <span className="rounded-full bg-warning-soft px-2.5 py-1 text-xs text-warning">待确认</span>}</TableCell><TableCell className="text-right"><Button variant="link" size="sm" onClick={() => toast.info("文件下载待文件服务接入")}>下载</Button></TableCell></TableRow>) : <TableRow><TableCell colSpan={11} className="h-32 text-center text-muted-foreground">当前筛选条件下暂无材料</TableCell></TableRow>}</TableBody>
</Table>
</CardContent>
</Card>
</div>
</section>
);
}