- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
192 lines
8.3 KiB
TypeScript
192 lines
8.3 KiB
TypeScript
import { cn } from '@/lib/utils';
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Filter, Edit, GitBranch, ArrowRight } from 'lucide-react';
|
|
import i18n from 'i18next';
|
|
import { useTranslation } from 'react-i18next';
|
|
import type { JsonData } from '@/types/common';
|
|
|
|
function parseSteps(steps: JsonData) {
|
|
if (Array.isArray(steps)) return steps;
|
|
if (typeof steps === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(steps);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// 审批类型字典 (labels resolved at render via i18n)
|
|
const APPROVAL_TYPE_META: Record<string, { key: string; color: string }> = {
|
|
deploy: { key: 'approval.type.deploy', color: 'var(--dg-blue)' },
|
|
server: { key: 'approval.type.server', color: 'var(--dg-cyan)' },
|
|
permission: { key: 'approval.type.permission', color: 'var(--dg-purple)' },
|
|
};
|
|
|
|
const getApprovalTypeLabel = (value: string): string => {
|
|
const meta = APPROVAL_TYPE_META[value];
|
|
return meta ? i18n.t(meta.key) : (value || i18n.t('approval.type.general'));
|
|
};
|
|
|
|
const getApprovalTypeColor = (value: string): string =>
|
|
APPROVAL_TYPE_META[value]?.color || 'var(--dg-text-secondary)';
|
|
|
|
// 模拟审批流程图
|
|
function renderFlowChart(flow: JsonData) {
|
|
const nodes = parseSteps(flow.steps);
|
|
if (nodes.length === 0) {
|
|
return <div className="text-[var(--dg-text-muted)] text-center py-4">{i18n.t('approval.noFlowNodes')}</div>;
|
|
}
|
|
return (
|
|
<div className="flex items-center gap-2 overflow-x-auto py-4">
|
|
{nodes.map((node: JsonData, index: number) => (
|
|
<div key={node.id || index} className="flex items-center">
|
|
<div
|
|
className={cn(
|
|
'px-4 py-2 rounded-lg border min-w-[100px] text-center',
|
|
node.type === 'start' && 'bg-emerald-500/10 border-emerald-500/30 dg-text-green',
|
|
node.type === 'approval' && 'bg-[var(--dg-blue)]/10 border-[var(--dg-blue)]/30 text-[var(--dg-blue)]',
|
|
node.type === 'condition' && 'bg-amber-500/10 border-amber-500/30 dg-text-yellow',
|
|
node.type === 'end' && 'bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-secondary)]'
|
|
)}
|
|
>
|
|
<p className="text-sm font-medium">{node.name}</p>
|
|
{node.approvers && (
|
|
<p className="text-xs text-[var(--dg-text-muted)] mt-1">
|
|
{Array.isArray(node.approvers) ? node.approvers.join(', ') : node.approvers}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{index < nodes.length - 1 && (
|
|
<ArrowRight className="h-4 w-4 text-[var(--dg-text-muted)] mx-2" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ApprovalTemplateTabProps {
|
|
filteredTemplates: JsonData[];
|
|
templateTypeFilter: string;
|
|
setTemplateTypeFilter: (value: string) => void;
|
|
}
|
|
|
|
export function ApprovalTemplateTab({
|
|
filteredTemplates,
|
|
templateTypeFilter,
|
|
setTemplateTypeFilter,
|
|
}: ApprovalTemplateTabProps) {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<>
|
|
{/* 类型筛选 */}
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<div className="flex items-center gap-1.5 text-sm text-[var(--dg-text-secondary)]">
|
|
<Filter className="h-4 w-4" />
|
|
<span>{t('approval.flow.filterType')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg p-1">
|
|
<button
|
|
onClick={() => setTemplateTypeFilter('all')}
|
|
className={cn(
|
|
'px-3 py-1.5 rounded-md text-sm transition-colors',
|
|
templateTypeFilter === 'all'
|
|
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)] font-medium'
|
|
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
|
|
)}
|
|
>
|
|
{t('approval.flow.allTypes')}
|
|
</button>
|
|
{Object.entries(APPROVAL_TYPE_META).map(([val, meta]) => (
|
|
<button
|
|
key={val}
|
|
onClick={() => setTemplateTypeFilter(val)}
|
|
className={cn(
|
|
'px-3 py-1.5 rounded-md text-sm transition-colors flex items-center gap-1.5',
|
|
templateTypeFilter === val
|
|
? 'bg-[var(--dg-bg-hover)] text-[var(--dg-text-primary)] font-medium'
|
|
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
|
|
)}
|
|
>
|
|
<span style={{ color: meta.color }}>●</span>
|
|
{getApprovalTypeLabel(val)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{filteredTemplates.length === 0 ? (
|
|
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<CardContent className="p-8">
|
|
<div className="text-center">
|
|
<GitBranch 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.noTemplates')}</h3>
|
|
<p className="text-sm text-[var(--dg-text-muted)]">{t('approval.noTemplatesDesc')}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
filteredTemplates.map((flow) => (
|
|
<Card key={flow.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-[var(--dg-blue)]/10 flex items-center justify-center">
|
|
<GitBranch className="h-5 w-5 text-[var(--dg-blue)]" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<CardTitle className="text-base text-[var(--dg-text-primary)]">{flow.name}</CardTitle>
|
|
{flow.type && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs border-[var(--dg-border)]"
|
|
style={{ color: getApprovalTypeColor(flow.type) }}
|
|
>
|
|
{getApprovalTypeLabel(flow.type)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<CardDescription className="text-[var(--dg-text-muted)]">{flow.description}</CardDescription>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
variant="outline"
|
|
className={
|
|
flow.status === 1 || flow.status === 'active' || flow.status === true
|
|
? 'border-emerald-500/30 dg-text-green bg-emerald-500/10'
|
|
: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)]'
|
|
}
|
|
>
|
|
{flow.status === 1 || flow.status === 'active' || flow.status === true ? t('approval.status.enabled') : t('approval.status.disabled')}
|
|
</Badge>
|
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-4 overflow-x-auto">
|
|
{renderFlowChart(flow)}
|
|
</div>
|
|
<div className="flex items-center justify-between mt-4 text-sm text-[var(--dg-text-muted)]">
|
|
<span>{t('approval.flow.createdOn')} {flow.created_at ? new Date(flow.created_at).toLocaleDateString() : '--'}</span>
|
|
<span>{t('approval.flow.nodeCount', { count: parseSteps(flow.steps).length })}</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|