- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
237 lines
9.8 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|