feat: 接入运维真实数据与三角色权限链路

- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。

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

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

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

- 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
郑龙捷
2026-09-02 15:02:47 +08:00
parent 6d044784ec
commit 74dd97428f
51 changed files with 2142 additions and 1062 deletions
@@ -7,7 +7,6 @@ import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle }
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";
@@ -19,48 +18,41 @@ function nextGeneration(frequency: string, day: number): string {
}
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 [readDay, setReadDay] = useState("");
const [readPeriod, setReadPeriod] = useState("");
const [templates] = useState(TEMPLATE_VERSIONS);
const [selectedBank, setSelectedBank] = useState("");
const [bankConfigs] = useState(BANK_REPORT_CONFIG);
const currentBankConfig = selectedBank ? bankConfigs[selectedBank] : undefined;
const notificationRows: string[][] = [];
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} 已模拟发布`);
toast.info("报告模板接口尚未接入");
};
const updateBankConfig = (updates: Partial<{ frequency: string; day: number }>) => {
setBankConfigs((current) => ({ ...current, [selectedBank]: { ...currentBankConfig, ...updates } }));
void updates;
toast.info("报告周期配置接口尚未接入");
};
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="flex w-full flex-col gap-6 pb-8">
<OperationsPageHeader title="运维系统配置" description="维护指标读取、报告模板、通知催办和银行报告周期。" actions={<Button onClick={() => toast.info("系统配置接口尚未接入")}><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>
<Button className="w-full" variant="outline" onClick={() => toast.info("指标同步接口尚未接入")}><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>
<CardContent className="space-y-3">{templates.length ? 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={() => toast.info("报告模板接口尚未接入")}></Button>}</div>) : <div className="py-10 text-center text-sm text-muted-foreground"></div>}</CardContent>
</Card>
</div>
@@ -72,14 +64,14 @@ export default function SystemConfigPage() {
<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>
<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> <code className="rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">permissions</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>
<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={["月度", "季度", "半年度", "年度"]} disabled={!currentBankConfig} onChange={(frequency) => updateBankConfig({ frequency })} /><label className="flex flex-col gap-1.5"><span className="text-xs font-medium text-ink-caption"></span><Input disabled={!currentBankConfig} 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={currentBankConfig ? 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).length ? 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>) : <TableRow><TableCell colSpan={4} className="h-28 text-center text-muted-foreground"></TableCell></TableRow>}</TableBody></Table></CardContent>
</Card>
<Card>