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

70 lines
5.4 KiB
TypeScript

import { useMemo, useState } from "react";
import { Download, FileDown, LogIn, Search, Send } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Input } from "~/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { exportRowsToExcel } from "~/lib/exportExcel";
import { FilterSelect, OperationsPageHeader } from "./OperationsUi";
import { USAGE_RECORDS, type UsageRecord } from "./workflowData";
function exportUsage(rows: UsageRecord[]) {
const header = ["姓名", "所属", "登录次数", "提交需求", "阅读报告", "下载报告", "最近登录"];
return exportRowsToExcel({ fileName: "平台使用统计", sheetName: "使用统计", headers: header, rows: rows.map((row) => [row.person, row.team, row.logins, row.requests, row.reportsRead, row.reportsDownloaded, row.lastLoginAt]) });
}
export default function UsageStatsPage() {
const [team, setTeam] = useState("");
const [keyword, setKeyword] = useState("");
const rows = useMemo(() => USAGE_RECORDS.filter((record) => (
record.team !== "管理员"
&& (!team || record.team === team)
&& (!keyword.trim() || record.person.includes(keyword.trim()))
)), [keyword, team]);
const business = USAGE_RECORDS.filter((record) => record.team === "业务团队");
const model = USAGE_RECORDS.filter((record) => record.team === "模型团队");
const sum = (records: UsageRecord[], key: "logins" | "requests" | "reportsRead" | "reportsDownloaded") => records.reduce((total, record) => total + record[key], 0);
const metrics = [
{ label: "业务团队登录", value: sum(business, "logins"), detail: `${business.length} 人 · 人均 ${(sum(business, "logins") / business.length).toFixed(1)} 次`, Icon: LogIn },
{ label: "模型团队登录", value: sum(model, "logins"), detail: `${model.length} 人 · 人均 ${(sum(model, "logins") / model.length).toFixed(1)} 次`, Icon: LogIn },
{ label: "提交需求", value: sum(USAGE_RECORDS, "requests"), detail: "累计业务团队发起", Icon: Send },
{ label: "报告下载", value: sum(USAGE_RECORDS, "reportsDownloaded"), detail: "累计两方合计", Icon: FileDown },
];
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 exportUsage(rows)}><Download />导出 Excel</Button>}
/>
<div className="grid grid-cols-4 gap-4">
{metrics.map(({ label, value, detail, Icon }) => (
<Card key={label} size="sm"><CardContent className="flex items-center gap-3"><span className="grid size-10 place-items-center rounded-xl bg-brand-soft text-primary"><Icon className="size-5" /></span><span><strong className="block text-xl font-bold tabular-nums text-foreground">{value}</strong><b className="block text-sm text-foreground">{label}</b><small className="text-xs text-muted-foreground">{detail}</small></span></CardContent></Card>
))}
</div>
<Card>
<CardHeader className="border-b border-border"><CardTitle>人员使用情况</CardTitle><CardDescription> {rows.length} </CardDescription></CardHeader>
<CardContent className="flex items-end gap-3">
<label className="relative block w-72"><Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /><Input className="pl-9" placeholder="搜索姓名" value={keyword} onChange={(event) => setKeyword(event.target.value)} /></label>
<FilterSelect className="w-48" label="所属团队" value={team} options={["业务团队", "模型团队"]} onChange={setTeam} />
</CardContent>
<CardContent className="px-0 pt-0">
<Table>
<TableHeader><TableRow><TableHead>姓名</TableHead><TableHead>所属</TableHead><TableHead>登录次数</TableHead><TableHead>提交需求</TableHead><TableHead>阅读报告</TableHead><TableHead>下载报告</TableHead><TableHead>最近登录</TableHead><TableHead>活跃度</TableHead></TableRow></TableHeader>
<TableBody>{rows.map((record) => {
const activity = record.logins >= 40 ? ["高", "bg-success-soft text-success-strong"] : record.logins >= 15 ? ["中", "bg-brand-soft text-primary"] : ["低", "bg-warning-soft text-warning"];
return <TableRow key={`${record.team}-${record.person}`}><TableCell className="font-medium text-foreground">{record.person}</TableCell><TableCell>{record.team}</TableCell><TableCell className="tabular-nums">{record.logins}</TableCell><TableCell className="tabular-nums">{record.requests}</TableCell><TableCell className="tabular-nums">{record.reportsRead}</TableCell><TableCell className="tabular-nums">{record.reportsDownloaded}</TableCell><TableCell className="font-mono text-xs">{record.lastLoginAt}</TableCell><TableCell><span className={`rounded-full px-2.5 py-1 text-xs font-medium ${activity[1]}`}>{activity[0]}</span></TableCell></TableRow>;
})}</TableBody>
</Table>
</CardContent>
</Card>
</div>
</section>
);
}