Files
aiops/frontend/src/components/approval/ApprovalProcessTab.tsx
T
Logan e363ded445 feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
2026-07-01 12:45:38 +08:00

237 lines
9.8 KiB
TypeScript

import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import {
Search,
CheckCircle2,
XCircle,
Eye,
Loader2,
FileText,
} from 'lucide-react';
import {
getProgressInfo,
renderStepProgress,
getApprovalTypeLabel,
getApprovalTypeColor,
getApprovalTypeIcon,
getInstanceType,
} from './helpers';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import type { JsonData } from '@/types/common';
// 状态标签页定义
const STATUS_TABS = [
{ key: 'all', labelKey: 'common.all', value: '' },
{ key: 'pending', labelKey: 'approval.status.pending', value: 'pending' },
{ key: 'approved', labelKey: 'approval.status.approved', value: 'approved' },
{ key: 'rejected', labelKey: 'approval.status.rejected', value: 'rejected' },
{ key: 'processing', labelKey: 'approval.status.pending', value: 'processing' },
{ key: 'completed', labelKey: 'common.completed', value: 'completed' },
];
function getStatusBadge(status: string) {
const statusMap: Record<string, { labelKey: string; className: string; dot: string }> = {
pending: {
labelKey: 'approval.status.pending',
className: 'border-amber-500/30 dg-text-yellow bg-amber-500/10',
dot: 'bg-amber-400',
},
approved: {
labelKey: 'approval.status.approved',
className: 'border-emerald-500/30 dg-text-green bg-emerald-500/10',
dot: 'bg-emerald-400',
},
rejected: {
labelKey: 'approval.status.rejected',
className: 'border-red-500/30 dg-text-red bg-red-500/10',
dot: 'bg-red-400',
},
processing: {
labelKey: 'approval.status.pending',
className: 'border-[var(--dg-blue)]/30 text-[var(--dg-blue)] bg-[var(--dg-blue)]/10',
dot: 'bg-[var(--dg-blue)]',
},
completed: {
labelKey: 'common.completed',
className: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]',
dot: 'bg-[var(--dg-text-secondary)]',
},
};
const config = statusMap[status] || statusMap.pending;
return (
<Badge variant="outline" className={config.className}>
<div className={cn('w-1.5 h-1.5 rounded-full mr-1.5', config.dot)} />
{i18n.t(config.labelKey)}
</Badge>
);
}
interface ApprovalProcessTabProps {
processes: JsonData[];
filteredInstances: JsonData[];
loading: boolean;
searchQuery: string;
setSearchQuery: (v: string) => void;
statusFilter: string;
setStatusFilter: (v: string) => void;
onView: (instance: JsonData) => void;
onApprove: (instance: JsonData) => void;
onReject: (instance: JsonData) => void;
}
export function ApprovalProcessTab({
filteredInstances,
loading,
searchQuery,
setSearchQuery,
statusFilter,
setStatusFilter,
onView,
onApprove,
onReject,
}: ApprovalProcessTabProps) {
const { t } = useTranslation();
return (
<>
{/* 搜索 + 状态筛选 */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-muted)]" />
<Input
placeholder={t('approval.flow.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="flex items-center gap-1 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg p-1">
{STATUS_TABS.map((tab) => (
<button
key={tab.key}
onClick={() => setStatusFilter(tab.value)}
className={cn(
'px-3 py-1.5 rounded-md text-sm transition-colors',
statusFilter === tab.value
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)] font-medium'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
)}
>
{t(tab.labelKey)}
</button>
))}
</div>
</div>
{/* 流程实例列表 */}
{loading ? (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-12">
<div className="flex items-center justify-center text-[var(--dg-text-muted)]">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
{t('common.loading')}
</div>
</CardContent>
</Card>
) : filteredInstances.length === 0 ? (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-12">
<div className="text-center">
<FileText className="h-12 w-12 mx-auto text-[var(--dg-text-muted)] mb-4" />
<h3 className="text-lg font-medium text-[var(--dg-text-secondary)] mb-2">{t('approval.noInstances')}</h3>
<p className="text-sm text-[var(--dg-text-muted)]">{t('approval.noInstancesDesc')}</p>
</div>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 gap-4">
{filteredInstances.map((instance) => {
const Icon = getApprovalTypeIcon(getInstanceType(instance));
const { percentage } = getProgressInfo(instance);
return (
<Card key={instance.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] hover:border-[var(--dg-border)] transition-colors">
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-[var(--dg-bg-tertiary)] flex items-center justify-center" style={{ color: getApprovalTypeColor(getInstanceType(instance)) }}>
<Icon className="h-5 w-5" />
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<h3 className="font-medium text-[var(--dg-text-primary)]">{instance.title}</h3>
{getStatusBadge(instance.status)}
</div>
<div className="flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
<span className="flex items-center gap-1">
<Badge variant="outline" className="text-xs border-[var(--dg-border)]" style={{ color: getApprovalTypeColor(getInstanceType(instance)) }}>
{getApprovalTypeLabel(getInstanceType(instance))}
</Badge>
</span>
<span>{t('approval.detail.applicant')}: {instance.applicant_name || instance.applicant || '--'}</span>
<span>{t('approval.detail.submittedAt')} {instance.created_at ? new Date(instance.created_at).toLocaleString() : '--'}</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]" onClick={() => onView(instance)}>
<Eye className="h-4 w-4 mr-1" />
{t('common.detail')}
</Button>
{instance.status === 'pending' && (
<>
<Button
variant="outline"
size="sm"
className="border-emerald-500/30 dg-text-green hover:bg-emerald-500/10"
onClick={() => onApprove(instance)}
>
<CheckCircle2 className="h-4 w-4 mr-1" />
{t('common.approve')}
</Button>
<Button
variant="outline"
size="sm"
className="border-red-500/30 dg-text-red hover:bg-red-500/10"
onClick={() => onReject(instance)}
>
<XCircle className="h-4 w-4 mr-1" />
{t('common.reject')}
</Button>
</>
)}
</div>
</div>
{/* {t('approval.detail.approvalProgress')}条 */}
<div className="mb-3">
<div className="flex items-center justify-between text-xs text-[var(--dg-text-muted)] mb-1.5">
<span>{t('approval.detail.approvalProgress')}</span>
<span>{percentage}%</span>
</div>
<div className="w-full h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all',
instance.status === 'rejected' ? 'bg-red-400' : 'bg-[var(--dg-blue)]'
)}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
{/* 步骤进度 */}
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3">
{renderStepProgress(instance)}
</div>
</CardContent>
</Card>
);
})}
</div>
)}
</>
);
}