feat: 工单权限过滤+仪表盘优化+LDAP登录+审批流程+待处理工单

后端:
- ticket_statistics/list_tickets 按用户身份过滤(admin全部, dba/auditor审批相关, developer自己的)
- 新增 /dashboard/my-tickets 接口(按角色返回待处理工单)
- auth.py 本地优先+LDAP fallback 登录认证
- ldap_config.py LDAPS 证书验证修复
- instance_detail.py developer/auditor 权限修复

前端:
- Dashboard 仪表盘: 3核心大数字指标+公告+我的待处理工单
- 删除 ECharts 趋势图(无真实数据)
- TicketDetailPage 审批流程时间线展示
- TicketsPage 工单统计按用户过滤+关联列(我提交的/待我审批)
- types 新增 TicketApproval 等类型定义
This commit is contained in:
logan committed 2026-05-25 20:47:15 +08:00
1 parent be2d6f3894
commit 43d1f3a234
10 files changed
+587 -440

No files matched your search

+107 -5
View File
@@ -88,6 +88,7 @@ export default function TicketDetailPage() {
// -- Data --
const [ticket, setTicket] = useState<TicketData | null>(null);
const [timeline, setTimeline] = useState<TimelineEntry[]>([]);
const [approvalSteps, setApprovalSteps] = useState<any[]>([]);
const [relatedTickets, setRelatedTickets] = useState<RelatedTicket[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@@ -129,6 +130,15 @@ export default function TicketDetailPage() {
}
}, [ticketId]);
const fetchApprovals = useCallback(async () => {
try {
const res = await ticketApi.approvals(ticketId);
setApprovalSteps(Array.isArray(res) ? res : []);
} catch {
setApprovalSteps([]);
}
}, [ticketId]);
// Fetch related tickets from API
const fetchRelated = useCallback(async () => {
try {
@@ -154,6 +164,7 @@ export default function TicketDetailPage() {
setLoading(true);
fetchTicket();
fetchTimeline();
fetchApprovals();
fetchRelated();
}
}, [ticketId, fetchTicket, fetchTimeline, fetchRelated]);
@@ -196,6 +207,7 @@ export default function TicketDetailPage() {
}
fetchTicket();
fetchTimeline();
fetchApprovals();
} catch (e: unknown) {
const err = e as { response?: { data?: { detail?: string } } };
setError(err?.response?.data?.detail || `操作失败: ${action}`);
@@ -379,6 +391,98 @@ export default function TicketDetailPage() {
);
};
const renderApprovalFlow = () => {
if (approvalSteps.length === 0) return null;
const actionLabel: Record<string, string> = {
approved: '已通过',
rejected: '已拒绝',
pending: '待处理',
skipped: '已跳过',
};
const actionStyle: Record<string, { color: string; bg: string }> = {
approved: { color: 'text-green-400', bg: 'bg-green-500/15' },
rejected: { color: 'text-red-400', bg: 'bg-red-500/15' },
pending: { color: 'text-[var(--dg-text-muted)]', bg: 'bg-[var(--dg-bg-tertiary)]' },
};
const stepAccent: Record<string, string> = {
'提交': 'text-blue-400',
'审核': 'text-amber-400',
'执行': 'text-purple-400',
};
const getAccent = (name: string) => {
for (const key of Object.keys(stepAccent)) {
if (name.includes(key)) return stepAccent[key];
}
return 'text-[var(--dg-text-secondary)]';
};
return (
<div className="bg-[var(--dg-bg-secondary)] rounded-lg p-5">
<h3 className="text-[15px] font-semibold flex items-center gap-2 border-b border-[var(--dg-border)] pb-3 mb-4">
<Shield className="w-4 h-4 text-blue-400" /> 审批流程
</h3>
<div className="space-y-2">
{approvalSteps.map((step, idx) => {
const actCfg = actionStyle[step.action] || actionStyle.pending;
const isCurrent = step.action === 'pending';
const isDone = step.action === 'approved' || step.action === 'rejected';
return (
<div
key={step.id || idx}
className={`flex items-start gap-3 p-3 rounded-lg ${
isCurrent
? 'bg-blue-500/5 border border-blue-500/20'
: 'bg-[var(--dg-bg-primary)]'
}`}
>
<div className={`w-6 h-6 rounded-full ${actCfg.bg} flex items-center justify-center shrink-0 mt-0.5`}>
{isDone && step.action === 'approved' ? (
<CheckCircle className={`w-3.5 h-3.5 ${actCfg.color}`} />
) : isDone ? (
<XCircle className={`w-3.5 h-3.5 ${actCfg.color}`} />
) : (
<div className={`w-1.5 h-1.5 rounded-full ${isCurrent ? 'bg-blue-400 animate-pulse' : 'bg-[var(--dg-text-muted)]'}`} />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className={`text-sm font-medium ${getAccent(step.step_name)}`}>
{step.step_name}
</span>
{step.approver_name && (
<span className="text-xs text-[var(--dg-text-muted)]">
{step.approver_name}
</span>
)}
</div>
<span className={`text-xs px-1.5 py-0.5 rounded ${actCfg.bg} ${actCfg.color} shrink-0`}>
{actionLabel[step.action] || step.action}
</span>
</div>
{step.comment && step.comment !== '提交工单' && (
<p className="text-xs text-[var(--dg-text-secondary)] mt-1">{step.comment}</p>
)}
{step.updated_at && isDone && (
<p className="text-xs text-[var(--dg-text-muted)] mt-0.5">
{new Date(step.updated_at).toLocaleString()}
</p>
)}
</div>
</div>
);
})}
</div>
</div>
);
};
const renderTimeline = () => {
if (timeline.length === 0) return null;
@@ -512,9 +616,6 @@ export default function TicketDetailPage() {
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${statusColorMap[ticket.status] || ''}`}>
{statusMap[ticket.status] || ticket.status}
</span>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${priorityInfo.color}`}>
{priorityInfo.label}优先级
</span>
</h1>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">
TK-{ticket.id} · 由 {ticket.applicant_name || '-'} 创建于 {new Date(ticket.create_time).toLocaleString()}
@@ -537,8 +638,6 @@ export default function TicketDetailPage() {
{ label: '数据库类型', value: ticket.db_type.toUpperCase() },
{ label: '目标数据库', value: ticket.target_database, icon: Database },
{ label: '申请人', value: ticket.applicant_name || '-' },
{ label: '审批人', value: ticket.approver_name || '-' },
{ label: '优先级', value: priorityInfo.label },
{
label: '创建时间',
value: new Date(ticket.create_time).toLocaleString(),
@@ -798,6 +897,9 @@ export default function TicketDetailPage() {
);
})()}
{/* Approval Flow */}
{renderApprovalFlow()}
{/* Timeline */}
{renderTimeline()}