提取 4 个独立文件到 approval-workflow/ 子目录: - types.ts (93L): 接口+常量+工具函数 - UserSelectInline.tsx (96L): 用户多选下拉组件 - WorkflowEditorModal.tsx (272L): 工作流编辑器弹窗 - WorkflowCard.tsx (134L): 工作流卡片组件 验证: tsc -b=0, npm build✓ (16.99s), ESLint errors=0, 17/17 测试通过
135 lines
6.6 KiB
TypeScript
135 lines
6.6 KiB
TypeScript
// 工作流卡片组件 — 从 ApprovalWorkflowPage.tsx 提取 (Q10 组件拆分)
|
|
// 渲染单个审批流程卡片:标题/徽章/步骤链/操作按钮/展开详情
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Star, Play, Edit2, Trash2, ChevronUp, ChevronDown,
|
|
} from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { STEP_TYPE_COLORS, parseApproverIds } from './types';
|
|
import type { WorkflowItem } from './types';
|
|
|
|
export interface WorkflowCardProps {
|
|
wf: WorkflowItem;
|
|
isExpanded: boolean;
|
|
getUserName: (uid: number) => string;
|
|
onToggleExpand: () => void;
|
|
onSetDefault: () => void;
|
|
onToggleActive: () => void;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
}
|
|
|
|
export function WorkflowCard({
|
|
wf, isExpanded, getUserName,
|
|
onToggleExpand, onSetDefault, onToggleActive, onEdit, onDelete,
|
|
}: WorkflowCardProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const categoryLabels: Record<string, string> = {
|
|
resource: t('approval.category.resource'),
|
|
app: t('approval.category.app'),
|
|
permission: t('approval.category.permission'),
|
|
};
|
|
|
|
const stepTypeLabels: Record<string, string> = {
|
|
submit: t('approval.step.submit'),
|
|
audit: t('approval.step.audit'),
|
|
execute: t('approval.step.execute'),
|
|
};
|
|
|
|
return (
|
|
<Card className={`dg-card overflow-hidden ${!wf.is_active ? 'opacity-60' : ''}`}>
|
|
<CardContent className="p-4 sm:p-5">
|
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
|
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{wf.name}</h3>
|
|
{wf.is_default && <span className="text-[10px] px-1.5 py-0.5 rounded bg-yellow-400/15 dg-text-yellow flex items-center gap-1"><Star className="w-3 h-3" />{t('approval.status.default')}</span>}
|
|
<span className={`text-[10px] px-1.5 py-0.5 rounded ${wf.is_active ? 'bg-green-500/15 dg-text-green' : 'bg-red-500/15 dg-text-red'}`}>
|
|
{wf.is_active ? t('approval.status.enabled') : t('approval.status.disabled')}
|
|
</span>
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-500/15 dg-text-blue">
|
|
{categoryLabels[wf.category] || wf.category}
|
|
</span>
|
|
</div>
|
|
{wf.description && <p className="text-sm text-[var(--dg-text-secondary)] mb-1">{wf.description}</p>}
|
|
<div className="flex items-center gap-1.5 mt-2 flex-wrap">
|
|
{wf.steps?.map((s, i) => (
|
|
<span key={i} className="flex items-center gap-1">
|
|
<span className={`text-[10px] px-1.5 py-0.5 rounded ${STEP_TYPE_COLORS[s.step_type] || 'dg-badge-muted'}`}>
|
|
{stepTypeLabels[s.step_type] || s.step_type}
|
|
</span>
|
|
{i < (wf.steps?.length || 0) - 1 && (
|
|
<span className="text-[var(--dg-text-muted)] text-[10px]">→</span>
|
|
)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 flex-shrink-0 sm:ml-4 self-end sm:self-start">
|
|
<button onClick={onToggleExpand}
|
|
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)] transition-colors">
|
|
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
|
</button>
|
|
{!wf.is_default && (
|
|
<button onClick={onSetDefault}
|
|
className="p-1.5 rounded hover:bg-yellow-500/10 text-[var(--dg-text-muted)] hover:dg-text-yellow transition-colors" title={t('approval.msg.setDefaultSuccess')}>
|
|
<Star className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
<button onClick={onToggleActive}
|
|
className={`p-1.5 rounded transition-colors ${wf.is_active ? 'hover:bg-red-500/10 dg-text-green hover:dg-text-red' : 'hover:bg-green-500/10 dg-text-red hover:dg-text-green'}`}
|
|
title={wf.is_active ? t('approval.status.disabled') : t('approval.status.enabled')}>
|
|
<Play className={`w-4 h-4 ${!wf.is_active ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
<button onClick={onEdit}
|
|
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-muted)] hover:dg-text-blue transition-colors" title={t('common.edit')}>
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button onClick={onDelete}
|
|
className="p-1.5 rounded hover:bg-red-500/10 text-[var(--dg-text-muted)] hover:dg-text-red transition-colors" title={t('common.delete')}>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && (
|
|
<div className="mt-4 pt-4 border-t border-[var(--dg-border)] space-y-3">
|
|
{wf.steps?.map((s, i) => {
|
|
const ids = parseApproverIds(s.approver_ids);
|
|
return (
|
|
<div key={i} className="flex items-start gap-3 p-3 bg-[var(--dg-bg-primary)] rounded-lg">
|
|
<div className="w-6 h-6 rounded-full bg-[var(--dg-bg-tertiary)] flex items-center justify-center text-xs font-medium text-[var(--dg-text-secondary)] flex-shrink-0 mt-0.5">
|
|
{s.step_number}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{s.name}</span>
|
|
<span className={`text-[10px] px-1.5 py-0.5 rounded ${STEP_TYPE_COLORS[s.step_type] || 'dg-badge-muted'}`}>
|
|
{stepTypeLabels[s.step_type] || s.step_type}
|
|
</span>
|
|
</div>
|
|
{ids.length > 0 && (
|
|
<div className="flex flex-wrap gap-1">
|
|
{ids.map((uid) => (
|
|
<span key={uid} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] bg-blue-500/15 dg-text-blue">
|
|
{getUserName(uid)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
{ids.length === 0 && s.step_type !== 'submit' && (
|
|
<span className="text-[11px] dg-text-red">{t('approval.detail.noApproverLinked')}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|