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

93 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { Database, Download, History, RefreshCw, Save, Settings2, Users } from "lucide-react";
import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import { Card, CardAction, 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 { usePersistentState } from "~/hooks/use-persistent-state";
import { FilterSelect, OperationsPageHeader } from "./OperationsUi";
import { BANK_REPORT_CONFIG, TEMPLATE_VERSIONS } from "./governanceData";
function nextGeneration(frequency: string, day: number): string {
const formattedDay = String(day).padStart(2, "0");
if (frequency === "月度") return `2026-09-${formattedDay}`;
if (frequency === "季度") return `2026-10-${formattedDay}`;
return `2027-01-${formattedDay}`;
}
export default function SystemConfigPage() {
const [readDay, setReadDay] = usePersistentState("a-card-config-read-day", "15 日");
const [readPeriod, setReadPeriod] = usePersistentState("a-card-config-read-period", "上一周期(月)");
const [templates, setTemplates] = usePersistentState("a-card-template-versions", TEMPLATE_VERSIONS);
const [selectedBank, setSelectedBank] = useState("江城银行");
const [bankConfigs, setBankConfigs] = usePersistentState("a-card-bank-report-config", BANK_REPORT_CONFIG);
const currentBankConfig = bankConfigs[selectedBank] ?? { frequency: "月度", day: 15 };
const notificationRows = [
["通知通道", "短信 + 平台通知"],
["报告未阅读催办", "模型团队 5 个工作日 · 业务团队 5 个工作日"],
["监控结果未处理催办", "1 个月未选择处理建议"],
["开发评审环节停滞", "1 个月未更新记录"],
["B/C 等级预警", "每月汇总,短信 + 平台通知"],
];
const publishTemplate = () => {
const version = `T${Number(templates[0]?.version.slice(1) ?? 4) + 1}`;
setTemplates((current) => [{ version, author: "管理员 王芳", createdAt: "2026-08-31 11:30", current: true, note: "手动发布(Mock" }, ...current.map((item) => ({ ...item, current: false }))]);
toast.success(`报告模板 ${version} 已模拟发布`);
};
const updateBankConfig = (updates: Partial<{ frequency: string; day: number }>) => {
setBankConfigs((current) => ({ ...current, [selectedBank]: { ...currentBankConfig, ...updates } }));
};
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={() => toast.success("配置已保存至前端 Mock 状态")}><Save />保存配置</Button>} />
<div className="grid grid-cols-2 gap-6">
<Card>
<CardHeader><CardTitle className="flex items-center gap-2"><Database className="size-5 text-primary" />指标读取配置</CardTitle><CardDescription>从共享数据库读取模型平台计算结果</CardDescription></CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-3"><FilterSelect label="读取日(每月)" value={readDay} allLabel="请选择" options={["1 日", "15 日", "20 日"]} onChange={setReadDay} /><FilterSelect label="读取周期" value={readPeriod} allLabel="请选择" options={["上一周期(月)", "上一周期(季)"]} onChange={setReadPeriod} /></div>
<Button className="w-full" variant="outline" onClick={() => toast.success("已模拟触发共享 DB 指标同步")}><RefreshCw />立即重新读取上一周期指标</Button>
<p className="rounded-xl bg-muted/50 p-4 text-xs leading-5 text-muted-foreground">读取排序性、KSPSIIVCSI。本平台不重算指标,只读取模型平台计算结果;共享表名和字段映射待接口设计阶段确定。</p>
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle className="flex items-center gap-2"><History className="size-5 text-primary" />报告模板版本管理</CardTitle><CardDescription>监控报告与诊断报告共用版本体系</CardDescription><CardAction><Button size="sm" onClick={publishTemplate}>发布新版本</Button></CardAction></CardHeader>
<CardContent className="space-y-3">{templates.map((template) => <div className="flex items-center gap-3 rounded-xl border border-border p-4" key={template.version}><b className="text-sm text-foreground">{template.version}</b><span className="min-w-0 flex-1"><small className="block truncate text-xs text-muted-foreground">{template.note}</small><small className="text-3xs text-muted-foreground">{template.author} · {template.createdAt}</small></span>{template.current ? <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong">当前生效</span> : <Button variant="outline" size="xs" onClick={() => { setTemplates((current) => current.map((item) => ({ ...item, current: item.version === template.version }))); toast.success(`报告模板已模拟回滚至 ${template.version}`); }}>一键回滚</Button>}</div>)}</CardContent>
</Card>
</div>
<div className="grid grid-cols-2 gap-6">
<Card>
<CardHeader className="border-b border-border"><CardTitle>通知与催办</CardTitle><CardDescription>邮件通道已取消</CardDescription><CardAction><Button variant="outline" size="xs" onClick={() => void exportRowsToExcel({ fileName: "通知与催办配置", sheetName: "通知配置", headers: ["配置项", "配置值"], rows: notificationRows })}><Download />导出 Excel</Button></CardAction></CardHeader>
<CardContent className="px-0"><Table><TableBody>{notificationRows.map(([item, value]) => <TableRow key={item}><TableCell className="w-52 font-medium text-foreground">{item}</TableCell><TableCell>{value}</TableCell></TableRow>)}</TableBody></Table></CardContent>
</Card>
<Card>
<CardHeader><CardTitle className="flex items-center gap-2"><Users className="size-5 text-primary" />登录角色来源</CardTitle><CardDescription>角色及权限由统一权限模块维护</CardDescription></CardHeader>
<CardContent className="space-y-4"><div className="rounded-xl bg-brand-soft p-4 text-sm leading-6 text-primary">运维前端不提供角色或权限配置功能。登录成功后只读取统一认证返回的 <code className="rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">role_code</code>,用于适配业务团队、模型团队和管理员三种界面。</div><div className="grid grid-cols-3 gap-3">{[["业务团队", "business / business_team"], ["模型团队", "developer / model_team"], ["管理员", "admin"]].map(([label, code]) => <div className="rounded-xl border border-border p-4" key={label}><b className="block text-sm text-foreground">{label}</b><small className="mt-1 block font-mono text-xs text-muted-foreground">{code}</small></div>)}</div><p className="text-xs leading-5 text-muted-foreground">服务端授权和权限管理由独立模块负责;本页不保存、不修改任何权限数据。</p></CardContent>
</Card>
</div>
<Card>
<CardHeader className="border-b border-border"><CardTitle>报告时间维度与输出日期</CardTitle><CardDescription>按银行单独配置,不强制统一为每月 15 </CardDescription><CardAction className="flex items-end gap-2"><FilterSelect className="w-44" label="选择银行" value={selectedBank} allLabel="请选择" options={Object.keys(bankConfigs)} onChange={setSelectedBank} /><Button variant="outline" size="xs" onClick={() => void exportRowsToExcel({ fileName: "报告周期配置", sheetName: "报告周期", headers: ["银行", "报告时间维度", "输出日期", "下次生成"], rows: Object.entries(bankConfigs).map(([bank, config]) => [bank, config.frequency, `每月 ${config.day} 日`, nextGeneration(config.frequency, config.day)]) })}><Download />导出 Excel</Button></CardAction></CardHeader>
<CardContent className="grid grid-cols-3 gap-3"><FilterSelect label="报告时间维度" value={currentBankConfig.frequency} allLabel="请选择" options={["月度", "季度", "半年度", "年度"]} onChange={(frequency) => updateBankConfig({ frequency })} /><label className="flex flex-col gap-1.5"><span className="text-xs font-medium text-ink-caption">报告输出日期(每月第几日)</span><Input type="number" min="1" max="28" value={currentBankConfig.day} onChange={(event) => updateBankConfig({ day: Number(event.target.value) })} /></label><label className="flex flex-col gap-1.5"><span className="text-xs font-medium text-ink-caption">下次生成</span><Input disabled value={nextGeneration(currentBankConfig.frequency, currentBankConfig.day)} /></label></CardContent>
<CardContent className="px-0 pt-0"><Table><TableHeader><TableRow><TableHead>银行</TableHead><TableHead>报告时间维度</TableHead><TableHead>输出日期</TableHead><TableHead>下次生成</TableHead></TableRow></TableHeader><TableBody>{Object.entries(bankConfigs).map(([bank, config]) => <TableRow data-state={bank === selectedBank ? "selected" : undefined} key={bank}><TableCell className="font-medium text-foreground">{bank}</TableCell><TableCell>{config.frequency}</TableCell><TableCell>每月 {config.day} </TableCell><TableCell>{nextGeneration(config.frequency, config.day)}</TableCell></TableRow>)}</TableBody></Table></CardContent>
</Card>
<Card>
<CardHeader><CardTitle className="flex items-center gap-2"><Settings2 className="size-5 text-primary" />模型平台新增页面依赖</CardTitle><CardDescription>非本平台交付范围</CardDescription><CardAction><span className="rounded-full bg-muted px-2.5 py-1 text-xs text-muted-foreground">待模型平台提供</span></CardAction></CardHeader>
<CardContent className="space-y-4"><div className="rounded-xl bg-warning-soft p-4 text-sm leading-6 text-foreground">需求明确由模型平台侧新增模型信息页面,本平台只读消费。该页面是模型大类概览、已上线模型详情和监控明细的数据底座。</div><div className="rounded-xl bg-bg-log p-5 font-mono text-xs leading-6 text-white/80">主键:银行 × 模型ID<br />字段:模型名称 / 模型版本 / 开发日期 / 开发人员 / 上线日期 / 陪跑开始日期 / 陪跑结束日期 / 最近迭代日期 / 迭代原因 / 迭代人员 / 下线日期</div></CardContent>
</Card>
</div>
</section>
);
}