Files
郑龙捷 74dd97428f feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。

- 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。

- 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。

- 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。

- 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
2026-09-02 15:02:47 +08:00

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>
);
}