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:
1 parent
be2d6f3894
commit
43d1f3a234
10 files changed
+587
-440
No files matched your search
@@ -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()}
|
||||
|
||||
|
||||
Reference in new issue
Block a user