feat: 全站 PageShell 设计系统与专业 UI 统一

- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
Logan committed 2026-07-01 12:45:38 +08:00
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); }}>
+47 -113
View File
@@ -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)]'
}
>
+5 -5
View File
@@ -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>