feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -8,11 +8,10 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
||||
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 { MODEL_CATEGORIES, categoryName, type ModelCategoryId } from "./modelData";
|
||||
import { WORKFLOWS, WORKFLOW_STAGES, type WorkflowFile, type WorkflowInstance } from "./workflowData";
|
||||
import { isOperationsActionVisibleForRole, useOperationsRole } from "./operationsRole";
|
||||
import { useCanOperationsAction, useOperationsRole } from "./operationsRole";
|
||||
|
||||
type Filters = { bank: string; category: string; year: string; status: string; reuse: string };
|
||||
const EMPTY_FILTERS: Filters = { bank: "", category: "", year: "", status: "", reuse: "" };
|
||||
@@ -27,18 +26,21 @@ function statusOf(workflow: WorkflowInstance): "进行中" | "已完结" {
|
||||
|
||||
export default function WorkflowPage() {
|
||||
const operationsRole = useOperationsRole();
|
||||
const canCreate = isOperationsActionVisibleForRole(operationsRole, "workflow:create");
|
||||
const canAdvance = isOperationsActionVisibleForRole(operationsRole, "workflow:advance");
|
||||
const canBusinessConfirm = isOperationsActionVisibleForRole(operationsRole, "workflow:business-confirm");
|
||||
const canCreate = useCanOperationsAction("workflow:create");
|
||||
const canFeedback = useCanOperationsAction("workflow:feedback");
|
||||
const canSubmitMaterial = useCanOperationsAction("workflow:submit-material");
|
||||
const canAdvance = useCanOperationsAction("workflow:advance");
|
||||
const canOnline = useCanOperationsAction("workflow:online");
|
||||
const canBusinessConfirm = useCanOperationsAction("workflow:business-confirm");
|
||||
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
|
||||
const [selectedId, setSelectedId] = useState(WORKFLOWS[0]?.workflowId ?? "");
|
||||
const [statisticsYear, setStatisticsYear] = useState("2026");
|
||||
const [openStages, setOpenStages] = useState<Set<number>>(new Set([WORKFLOWS[0]?.currentStage ?? 1]));
|
||||
const [newRequestOpen, setNewRequestOpen] = useState(false);
|
||||
const [newBank, setNewBank] = useState("江城银行");
|
||||
const [newBank, setNewBank] = useState("");
|
||||
const [newCategory, setNewCategory] = useState("std");
|
||||
const [newReuse, setNewReuse] = useState("独立开发");
|
||||
const [localFiles, setLocalFiles] = usePersistentState<Record<string, WorkflowFile[]>>("a-card-workflow-local-files", {});
|
||||
const [localFiles, setLocalFiles] = useState<Record<string, WorkflowFile[]>>({});
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const uploadTargetRef = useRef<{ workflowId: string; stage: number } | null>(null);
|
||||
const filtered = useMemo(() => WORKFLOWS.filter((workflow) => (
|
||||
@@ -85,12 +87,12 @@ export default function WorkflowPage() {
|
||||
confirmed: target.stage === 3,
|
||||
};
|
||||
setLocalFiles((current) => ({ ...current, [key]: [...(current[key] ?? []), uploaded] }));
|
||||
toast.success("材料已加入本地上传队列");
|
||||
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">
|
||||
<div className="flex w-full flex-col gap-6 pb-8">
|
||||
<input ref={fileInputRef} className="hidden" type="file" accept=".doc,.docx,.xls,.xlsx,.pdf,.ppt,.pptx,.zip" onChange={(event) => { addWorkflowFile(event.target.files?.[0]); event.target.value = ""; }} />
|
||||
<OperationsPageHeader
|
||||
title="全流程进度"
|
||||
@@ -146,7 +148,7 @@ export default function WorkflowPage() {
|
||||
const current = workflow.currentStage === stage.stage;
|
||||
const files = [...(workflow.files[stage.stage] ?? []), ...(localFiles[`${workflow.workflowId}:${stage.stage}`] ?? [])];
|
||||
const open = openStages.has(stage.stage);
|
||||
const canUpload = canAdvance || (canBusinessConfirm && stage.stage === 4);
|
||||
const canUpload = canSubmitMaterial || (canBusinessConfirm && stage.stage === 4);
|
||||
return (
|
||||
<Card key={stage.stage} size="sm">
|
||||
<button className="flex w-full cursor-pointer items-center gap-3 px-4 text-left" type="button" onClick={() => toggleStage(stage.stage)}>
|
||||
@@ -155,7 +157,7 @@ export default function WorkflowPage() {
|
||||
{current && <span className="rounded-full bg-brand-soft px-2.5 py-1 text-xs font-medium text-primary">进行中</span>}{done && <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs font-medium text-success-strong">已完成</span>}{!done && !current && <span className="rounded-full bg-muted px-2.5 py-1 text-xs text-muted-foreground">未开始</span>}
|
||||
<ChevronDown className={`size-4 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
{open && <CardContent className="space-y-4 border-t border-border pt-4"><div className="grid grid-cols-2 gap-4"><div className="rounded-xl bg-muted/50 p-4"><b className="text-sm text-foreground">业务团队</b><p className="mt-1 text-xs leading-5 text-muted-foreground">{stage.businessDuty}</p></div><div className="rounded-xl bg-muted/50 p-4"><b className="text-sm text-foreground">模型团队</b><p className="mt-1 text-xs leading-5 text-muted-foreground">{stage.modelDuty}</p></div></div>{files.length ? <Table><TableHeader><TableRow><TableHead>材料名称</TableHead><TableHead>上传人</TableHead><TableHead>上传时间</TableHead><TableHead>确认状态</TableHead><TableHead className="text-right">操作</TableHead></TableRow></TableHeader><TableBody>{files.map((file) => <TableRow key={`${file.name}-${file.uploadedAt}`}><TableCell className="font-medium text-foreground">{file.name}</TableCell><TableCell>{file.uploadedBy}</TableCell><TableCell>{file.uploadedAt}</TableCell><TableCell>{stage.stage === 3 ? <span className="rounded-full bg-muted px-2.5 py-1 text-xs">无需业务确认</span> : file.confirmed ? <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong">已确认</span> : canBusinessConfirm ? <Button variant="outline" size="xs" onClick={() => toast.success("已模拟确认材料")}>确认</Button> : <span className="rounded-full bg-warning-soft px-2.5 py-1 text-xs text-warning">待确认</span>}</TableCell><TableCell className="text-right"><Button variant="link" size="sm" onClick={() => toast.info("原始文件内容将在文件服务接入后下载")}>下载</Button></TableCell></TableRow>)}</TableBody></Table> : <div className="rounded-xl border border-dashed border-border p-6 text-center text-xs text-muted-foreground">本环节暂无材料</div>}{current && <div className="flex flex-wrap gap-2">{canUpload && <Button variant="outline" size="sm" onClick={() => chooseWorkflowFile(workflow.workflowId, stage.stage)}><FileUp />上传材料</Button>}<Button variant="outline" size="sm" onClick={() => toast.success("已模拟发送催办通知")}><Bell />发起催办</Button>{canAdvance && stage.stage < 7 && <Button size="sm" onClick={() => toast.success("已模拟推进到下一环节")}>推进下一环节 <ArrowRight /></Button>}</div>}</CardContent>}
|
||||
{open && <CardContent className="space-y-4 border-t border-border pt-4"><div className="grid grid-cols-2 gap-4"><div className="rounded-xl bg-muted/50 p-4"><b className="text-sm text-foreground">业务团队</b><p className="mt-1 text-xs leading-5 text-muted-foreground">{stage.businessDuty}</p></div><div className="rounded-xl bg-muted/50 p-4"><b className="text-sm text-foreground">模型团队</b><p className="mt-1 text-xs leading-5 text-muted-foreground">{stage.modelDuty}</p></div></div>{files.length ? <Table><TableHeader><TableRow><TableHead>材料名称</TableHead><TableHead>上传人</TableHead><TableHead>上传时间</TableHead><TableHead>确认状态</TableHead><TableHead className="text-right">操作</TableHead></TableRow></TableHeader><TableBody>{files.map((file) => <TableRow key={`${file.name}-${file.uploadedAt}`}><TableCell className="font-medium text-foreground">{file.name}</TableCell><TableCell>{file.uploadedBy}</TableCell><TableCell>{file.uploadedAt}</TableCell><TableCell>{stage.stage === 3 ? <span className="rounded-full bg-muted px-2.5 py-1 text-xs">无需业务确认</span> : file.confirmed ? <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong">已确认</span> : canBusinessConfirm ? <Button variant="outline" size="xs" onClick={() => toast.info("材料确认接口尚未接入")}>确认</Button> : <span className="rounded-full bg-warning-soft px-2.5 py-1 text-xs text-warning">待确认</span>}</TableCell><TableCell className="text-right"><Button variant="link" size="sm" onClick={() => toast.info("原始文件内容将在文件服务接入后下载")}>下载</Button></TableCell></TableRow>)}</TableBody></Table> : <div className="rounded-xl border border-dashed border-border p-6 text-center text-xs text-muted-foreground">本环节暂无材料</div>}{current && <div className="flex flex-wrap gap-2">{canUpload && <Button variant="outline" size="sm" onClick={() => chooseWorkflowFile(workflow.workflowId, stage.stage)}><FileUp />上传材料</Button>}{canFeedback && stage.stage === 2 && <Button variant="outline" size="sm" onClick={() => toast.info("流程反馈接口尚未接入")}>填写反馈</Button>}{canBusinessConfirm && stage.stage === 5 && <Button variant="outline" size="sm" onClick={() => toast.info("流程确认接口尚未接入")}>确认评审材料</Button>}<Button variant="outline" size="sm" onClick={() => toast.info("催办通知接口尚未接入")}><Bell />发起催办</Button>{canAdvance && stage.stage < 7 && <Button size="sm" onClick={() => toast.info("流程推进接口尚未接入")}>推进下一环节 <ArrowRight /></Button>}{canOnline && stage.stage === 7 && <Button size="sm" onClick={() => toast.info("模型上线接口尚未接入")}>确认上线 <ArrowRight /></Button>}</div>}</CardContent>}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
@@ -168,7 +170,7 @@ export default function WorkflowPage() {
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle>发起模型需求</DialogTitle><DialogDescription>支持单银行需求,也可输入多个银行并选择复用通用模型。</DialogDescription></DialogHeader>
|
||||
<div className="space-y-4"><label className="flex flex-col gap-1.5"><span className="text-xs font-medium text-ink-caption">银行</span><Input value={newBank} onChange={(event) => setNewBank(event.target.value)} placeholder="多个银行使用顿号分隔" /></label><FilterSelect label="模型大类" value={newCategory} allLabel="请选择" options={MODEL_CATEGORIES.map((item) => ({ label: item.name, value: item.id }))} onChange={setNewCategory} /><FilterSelect label="模型来源" value={newReuse} allLabel="请选择" options={["独立开发", "复用通用模型"]} onChange={setNewReuse} /></div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setNewRequestOpen(false)}>取消</Button><Button onClick={() => { if (!newBank.trim()) { toast.error("请输入银行"); return; } setNewRequestOpen(false); toast.success(`已模拟发起需求:${newBank} · ${categoryName(newCategory as ModelCategoryId)} · ${newReuse}`); }}>提交需求</Button></DialogFooter>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setNewRequestOpen(false)}>取消</Button><Button onClick={() => { if (!newBank.trim()) { toast.error("请输入银行"); return; } setNewRequestOpen(false); toast.info("需求提交接口尚未接入"); }}>提交需求</Button></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user