feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -152,7 +152,7 @@ export function ApprovalCreateDialog({
|
||||
<div key={field.key} className={cn('space-y-1.5', isTextarea && 'col-span-2')}>
|
||||
<Label className="text-sm font-medium text-[var(--dg-text-secondary)]">
|
||||
{field.label}
|
||||
{field.required && <span className="text-red-400 ml-0.5">*</span>}
|
||||
{field.required && <span className="dg-text-red ml-0.5">*</span>}
|
||||
</Label>
|
||||
{field.type === 'select' ? (
|
||||
<Select
|
||||
|
||||
@@ -97,9 +97,9 @@ export function ApprovalDetailDialog({
|
||||
{instance.actions.map((action: JsonData, index: number) => (
|
||||
<div key={index} className="flex items-center gap-3 text-sm">
|
||||
{action.action === 'approve' ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
||||
<CheckCircle2 className="h-4 w-4 dg-text-green" />
|
||||
) : action.action === 'reject' ? (
|
||||
<XCircle className="h-4 w-4 text-red-400" />
|
||||
<XCircle className="h-4 w-4 dg-text-red" />
|
||||
) : (
|
||||
<Clock className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
)}
|
||||
@@ -127,7 +127,7 @@ export function ApprovalDetailDialog({
|
||||
<DialogFooter>
|
||||
{instance.status === 'pending' && (
|
||||
<>
|
||||
<Button variant="outline" className="border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={() => { onOpenChange(false); onReject(instance); }}>
|
||||
<Button variant="outline" className="border-red-500/30 dg-text-red hover:bg-red-500/10" onClick={() => { onOpenChange(false); onReject(instance); }}>
|
||||
<XCircle className="h-4 w-4 mr-2" />{t('common.reject')}
|
||||
</Button>
|
||||
<Button className="bg-emerald-500/90 hover:bg-emerald-500 text-white" onClick={() => { onOpenChange(false); onApprove(instance); }}>
|
||||
|
||||
@@ -4,6 +4,9 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -17,7 +20,6 @@ import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Clock,
|
||||
Plus,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -138,105 +140,40 @@ export function ApprovalFlow() {
|
||||
const rejectedCount = processes.filter((p) => p.status === 'rejected').length;
|
||||
const myPendingCount = myApprovals.filter((m) => m.status === 'pending').length;
|
||||
|
||||
// 页面级 Tab 配置 // Logan.Li
|
||||
const pageTabs = [
|
||||
{ id: 'instances', label: t('approval.flow.tabInstances') },
|
||||
{ id: 'flows', label: t('approval.flow.tabTemplates') },
|
||||
{ id: 'my-approvals', label: t('approval.flow.tabMyApprovals'), badge: myPendingCount > 0 ? myPendingCount : undefined },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">{t('approval.flow.title')}</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">{t('approval.flow.subtitle')}</p>
|
||||
</div>
|
||||
<Button
|
||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t('approval.newRequest')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('approval.flow.pendingApproval')}</p>
|
||||
<p className="text-2xl font-bold text-amber-400">{pendingCount}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400">
|
||||
<Clock className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('approval.status.approved')}</p>
|
||||
<p className="text-2xl font-bold text-emerald-400">{approvedCount}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('approval.status.rejected')}</p>
|
||||
<p className="text-2xl font-bold text-red-400">{rejectedCount}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-red-500/10 text-red-400">
|
||||
<XCircle className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('approval.flow.myTodo')}</p>
|
||||
<p className="text-2xl font-bold text-[var(--dg-blue)]">{myPendingCount}</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-[var(--dg-blue)]/10 text-[var(--dg-blue)]">
|
||||
<FileText className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tab Bar */}
|
||||
<div className="flex items-center gap-1 border-b border-[var(--dg-border)]">
|
||||
{([
|
||||
{ key: 'instances', label: t('approval.flow.tabInstances') },
|
||||
{ key: 'flows', label: t('approval.flow.tabTemplates') },
|
||||
{ key: 'my-approvals', label: t('approval.flow.tabMyApprovals') },
|
||||
] as const).map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={cn(
|
||||
'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors',
|
||||
activeTab === tab.key
|
||||
? 'border-[var(--dg-blue)] text-[var(--dg-blue)]'
|
||||
: 'border-transparent text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.key === 'my-approvals' && myPendingCount > 0 && (
|
||||
<Badge className="ml-2 bg-red-500/20 text-red-400 border-0 text-xs">{myPendingCount}</Badge>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<>
|
||||
<PageShell
|
||||
variant="list"
|
||||
title={t('approval.flow.title')}
|
||||
subtitle={t('approval.flow.subtitle')}
|
||||
icon={<FileText className="h-5 w-5" />}
|
||||
metrics={(
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: t('approval.flow.pendingApproval'), value: pendingCount, color: 'amber' },
|
||||
{ label: t('approval.status.approved'), value: approvedCount, color: 'emerald' },
|
||||
{ label: t('approval.status.rejected'), value: rejectedCount, color: 'red' },
|
||||
{ label: t('approval.flow.myTodo'), value: myPendingCount, color: 'blue' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
tabs={pageTabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={(id) => setActiveTab(id as typeof activeTab)}
|
||||
actions={(
|
||||
<Button className="dg-btn-primary" onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t('approval.newRequest')}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{activeTab === 'instances' && (
|
||||
<ApprovalProcessTab
|
||||
processes={processes}
|
||||
@@ -263,15 +200,11 @@ export function ApprovalFlow() {
|
||||
{activeTab === 'my-approvals' && (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{myApprovals.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.noTodo')}</h3>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">{t('approval.noTodoDesc')}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<PageEmpty
|
||||
icon={<FileText className="h-12 w-12" />}
|
||||
title={t('approval.noTodo')}
|
||||
description={t('approval.noTodoDesc')}
|
||||
/>
|
||||
) : (
|
||||
myApprovals.map((instance) => {
|
||||
const { percentage } = getProgressInfo(instance);
|
||||
@@ -305,10 +238,10 @@ export function ApprovalFlow() {
|
||||
</Button>
|
||||
{instance.status === 'pending' && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10" onClick={() => openActionDialog('approve', instance)}>
|
||||
<Button variant="outline" size="sm" className="border-emerald-500/30 dg-text-green hover:bg-emerald-500/10" onClick={() => openActionDialog('approve', instance)}>
|
||||
<CheckCircle2 className="h-4 w-4 mr-1" />{t('common.approve')}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={() => openActionDialog('reject', instance)}>
|
||||
<Button variant="outline" size="sm" className="border-red-500/30 dg-text-red hover:bg-red-500/10" onClick={() => openActionDialog('reject', instance)}>
|
||||
<XCircle className="h-4 w-4 mr-1" />{t('common.reject')}
|
||||
</Button>
|
||||
</>
|
||||
@@ -334,6 +267,7 @@ export function ApprovalFlow() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageShell>
|
||||
|
||||
{/* 详情弹窗 — 提取到 ApprovalDetailDialog */}
|
||||
<ApprovalDetailDialog
|
||||
@@ -352,12 +286,12 @@ export function ApprovalFlow() {
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
{actionType === 'approve' ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
|
||||
<CheckCircle2 className="h-5 w-5 dg-text-green" />
|
||||
{t('approval.flow.approveConfirm')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="h-5 w-5 text-red-400" />
|
||||
<XCircle className="h-5 w-5 dg-text-red" />
|
||||
{t('approval.flow.rejectConfirm')}
|
||||
</>
|
||||
)}
|
||||
@@ -407,7 +341,7 @@ export function ApprovalFlow() {
|
||||
templates={templates}
|
||||
onSuccess={loadData}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,17 +37,17 @@ function getStatusBadge(status: string) {
|
||||
const statusMap: Record<string, { labelKey: string; className: string; dot: string }> = {
|
||||
pending: {
|
||||
labelKey: 'approval.status.pending',
|
||||
className: 'border-amber-500/30 text-amber-400 bg-amber-500/10',
|
||||
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 text-emerald-400 bg-emerald-500/10',
|
||||
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 text-red-400 bg-red-500/10',
|
||||
className: 'border-red-500/30 dg-text-red bg-red-500/10',
|
||||
dot: 'bg-red-400',
|
||||
},
|
||||
processing: {
|
||||
@@ -186,7 +186,7 @@ export function ApprovalProcessTab({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
|
||||
className="border-emerald-500/30 dg-text-green hover:bg-emerald-500/10"
|
||||
onClick={() => onApprove(instance)}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4 mr-1" />
|
||||
@@ -195,7 +195,7 @@ export function ApprovalProcessTab({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
|
||||
className="border-red-500/30 dg-text-red hover:bg-red-500/10"
|
||||
onClick={() => onReject(instance)}
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-1" />
|
||||
|
||||
@@ -48,9 +48,9 @@ function renderFlowChart(flow: JsonData) {
|
||||
<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 text-emerald-400',
|
||||
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 text-amber-400',
|
||||
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)]'
|
||||
)}
|
||||
>
|
||||
@@ -161,7 +161,7 @@ export function ApprovalTemplateTab({
|
||||
variant="outline"
|
||||
className={
|
||||
flow.status === 1 || flow.status === 'active' || flow.status === true
|
||||
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
|
||||
? '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)]'
|
||||
}
|
||||
>
|
||||
|
||||
@@ -108,9 +108,9 @@ export function renderStepProgress(instance: JsonData) {
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs whitespace-nowrap',
|
||||
isCompleted && 'bg-emerald-500/10 text-emerald-400',
|
||||
isCurrent && !isRejected && 'bg-amber-500/10 text-amber-400 border border-amber-500/30',
|
||||
isRejected && 'bg-red-500/10 text-red-400',
|
||||
isCompleted && 'bg-emerald-500/10 dg-text-green',
|
||||
isCurrent && !isRejected && 'bg-amber-500/10 dg-text-yellow border border-amber-500/30',
|
||||
isRejected && 'bg-red-500/10 dg-text-red',
|
||||
!isCompleted && !isCurrent && !isRejected && 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]'
|
||||
)}
|
||||
>
|
||||
@@ -122,8 +122,8 @@ export function renderStepProgress(instance: JsonData) {
|
||||
<span className={cn(
|
||||
'text-[10px] px-1 py-0 rounded',
|
||||
step.step_type === 'execute'
|
||||
? 'bg-green-500/15 text-green-400'
|
||||
: 'bg-purple-500/15 text-purple-400'
|
||||
? 'bg-green-500/15 dg-text-green'
|
||||
: 'bg-purple-500/15 dg-text-purple'
|
||||
)}>
|
||||
{step.step_type === 'execute' ? i18n.t('approval.step.executeShort') : i18n.t('approval.step.auditShort')}
|
||||
</span>
|
||||
|
||||
Reference in new issue
Block a user