- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
70 lines
5.6 KiB
TypeScript
70 lines
5.6 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 ? `${business.length} 人 · 人均 ${(sum(business, "logins") / business.length).toFixed(1)} 次` : "暂无数据", Icon: LogIn },
|
|
{ label: "模型团队登录", value: sum(model, "logins"), detail: model.length ? `${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="flex w-full 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.length ? 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>;
|
|
}) : <TableRow><TableCell colSpan={8} className="h-32 text-center text-muted-foreground">暂无使用统计数据</TableCell></TableRow>}</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|