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

@@ -26,6 +26,10 @@ import {
} from 'lucide-react';
import { cloudApi } from '@/api';
import { CloudBrandIcon, getProviderColor } from '@/components/icons/CloudBrandIcon';
import { PageShell } from '@/components/layout/PageShell';
import { PageLoading } from '@/components/layout/PageLoading';
import { PageEmpty } from '@/components/layout/PageEmpty';
import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common';
const providerLabels: Record<string, string> = {
@@ -146,70 +150,84 @@ export function CloudAccountManager() {
return matchesSearch && matchesProvider;
});
const _stats = {
const stats = {
total: accounts.length,
active: accounts.filter(a => a.status === 'active').length,
inactive: accounts.filter(a => a.status === 'inactive').length,
error: accounts.filter(a => a.status === 'error').length,
};
const toolbar = (
<div className="flex items-center gap-4 flex-wrap">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
<Input
placeholder="搜索账号名称或AccessKey..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 h-9 dg-input"
/>
</div>
<select
value={selectedProvider}
onChange={(e) => setSelectedProvider(e.target.value)}
className="px-3 py-2 h-9 rounded-md text-sm dg-input"
>
<option value="all">全部平台</option>
<option value="aws">AWS</option>
<option value="aliyun">阿里云</option>
<option value="tencent">腾讯云</option>
<option value="huawei">华为云</option>
<option value="azure">Azure</option>
</select>
</div>
);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">云账号管理</h1>
<p className="text-slate-400 mt-1">管理多云平台的访问凭证和账号信息</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={loadData} className="border-slate-700 text-slate-300">
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
<PageShell
variant="list"
title="云账号管理"
subtitle="管理多云平台的访问凭证和账号信息"
icon={<Key className="h-5 w-5" />}
metrics={
!loading ? (
<StatStrip
items={[
{ label: '总计', value: stats.total, color: 'blue' },
{ label: '正常', value: stats.active, color: 'green' },
{ label: '停用', value: stats.inactive, color: 'muted' },
{ label: '异常', value: stats.error, color: stats.error > 0 ? 'red' : 'muted' },
]}
/>
) : undefined
}
toolbar={toolbar}
actions={(
<>
<Button variant="outline" size="sm" onClick={loadData} className="dg-btn-secondary">
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
刷新
</Button>
<Button className="bg-blue-600 hover:bg-blue-700" onClick={handleOpenCreate}>
<Plus className="h-4 w-4 mr-2" />
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
<Plus className="h-4 w-4 mr-1" />
添加账号
</Button>
</div>
</div>
{/* Filters */}
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
<Input
placeholder="搜索账号名称或AccessKey..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-slate-800 border-slate-700 text-slate-100"
/>
</div>
<select
value={selectedProvider}
onChange={(e) => setSelectedProvider(e.target.value)}
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-md text-slate-100 text-sm"
>
<option value="all">全部平台</option>
<option value="aws">AWS</option>
<option value="aliyun">阿里云</option>
<option value="tencent">腾讯云</option>
<option value="huawei">华为云</option>
<option value="azure">Azure</option>
</select>
</div>
{/* Account List */}
</>
)}
>
{loading ? (
<Card className="bg-slate-800 border-slate-700">
<CardContent className="p-12 text-center text-slate-500">
<RefreshCw className="h-6 w-6 animate-spin mx-auto mb-3" />
加载中...
</CardContent>
</Card>
<PageLoading />
) : filteredAccounts.length === 0 ? (
<Card className="bg-slate-800 border-slate-700">
<CardContent className="p-12 text-center text-slate-500">暂无云账号数据</CardContent>
</Card>
<PageEmpty
icon={<Key className="h-12 w-12" />}
title="暂无云账号数据"
action={(
<Button size="sm" className="dg-btn-primary" onClick={handleOpenCreate}>
<Plus className="h-4 w-4 mr-1" />
添加账号
</Button>
)}
/>
) : (
<div className="grid grid-cols-2 gap-4">
{filteredAccounts.map((account) => {
@@ -218,37 +236,37 @@ export function CloudAccountManager() {
const lastSync = account.last_sync || account.lastSync || account.created_at || '-';
const monthlyCost = account.monthly_cost ?? account.monthlyCost ?? 0;
return (
<Card key={account.id} className="bg-slate-800 border-slate-700">
<Card key={account.id} className="dg-card dg-card-hover">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center" style={{ borderBottom: `2px solid ${getProviderColor(account.provider)}` }}>
<div className="w-10 h-10 rounded-lg bg-[var(--dg-bg-tertiary)] flex items-center justify-center" style={{ borderBottom: `2px solid ${getProviderColor(account.provider)}` }}>
<CloudBrandIcon provider={account.provider} className="h-6 w-6" />
</div>
<div>
<h3 className="font-medium text-slate-100">{account.name}</h3>
<p className="text-sm text-slate-400 mt-0.5">{providerLabels[account.provider] || account.provider}</p>
<h3 className="font-medium text-[var(--dg-text-primary)]">{account.name}</h3>
<p className="text-sm dg-text-secondary mt-0.5">{providerLabels[account.provider] || account.provider}</p>
<div className="flex items-center gap-3 mt-2">
{accessKey && (
<Badge variant="secondary" className="bg-slate-700 text-slate-300">
<Badge variant="secondary" className="dg-badge-muted">
<Key className="h-3 w-3 mr-1" />
{accessKey.slice(0, 8)}...
</Badge>
)}
{account.status === 'active' && (
<Badge className="bg-green-500/20 text-green-400 border-0">
<Badge className="dg-status-running border-0">
<CheckCircle2 className="h-3 w-3 mr-1" />
正常
</Badge>
)}
{account.status === 'inactive' && (
<Badge className="bg-slate-500/20 text-slate-400 border-0">
<Badge className="dg-status-stopped border-0">
<XCircle className="h-3 w-3 mr-1" />
已停用
</Badge>
)}
{account.status === 'error' && (
<Badge className="bg-red-500/20 text-red-400 border-0">
<Badge className="dg-status-error border-0">
<Key className="h-3 w-3 mr-1" />
异常
</Badge>
@@ -260,7 +278,7 @@ export function CloudAccountManager() {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-blue-400"
className="h-8 w-8 dg-text-muted hover:text-[var(--dg-blue)]"
onClick={() => handleOpenEdit(account)}
>
<Pencil className="h-4 w-4" />
@@ -268,25 +286,25 @@ export function CloudAccountManager() {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-400"
className="h-8 w-8 dg-text-muted hover:text-[var(--dg-red)]"
onClick={() => handleDelete(account.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-slate-700">
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-[var(--dg-border)]">
<div>
<p className="text-xs text-slate-400">月费用</p>
<p className="text-lg font-medium text-emerald-400">¥{Number(monthlyCost).toLocaleString()}</p>
<p className="text-xs dg-text-secondary">月费用</p>
<p className="text-lg font-medium text-[var(--dg-green)]">¥{Number(monthlyCost).toLocaleString()}</p>
</div>
<div>
<p className="text-xs text-slate-400">服务器数</p>
<p className="text-lg font-medium text-slate-100">{serverCount}</p>
<p className="text-xs dg-text-secondary">服务器数</p>
<p className="text-lg font-medium text-[var(--dg-text-primary)]">{serverCount}</p>
</div>
<div>
<p className="text-xs text-slate-400">最后同步</p>
<p className="text-sm text-slate-300">
<p className="text-xs dg-text-secondary">最后同步</p>
<p className="text-sm text-[var(--dg-text-secondary)]">
{lastSync !== '-' ? new Date(lastSync).toLocaleString() : '-'}
</p>
</div>
@@ -300,13 +318,13 @@ export function CloudAccountManager() {
{/* 创建账号 Dialog */}
<Dialog open={formOpen} onOpenChange={setFormOpen}>
<DialogContent className="bg-slate-800 border-slate-700 text-slate-100 max-w-lg">
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<Cloud className="h-5 w-5 text-blue-400" />
<DialogTitle className="flex items-center gap-2">
<Cloud className="h-5 w-5 text-[var(--dg-blue)]" />
{editingAccount ? '编辑云账号' : '添加云账号'}
</DialogTitle>
<DialogDescription className="text-slate-400">
<DialogDescription>
{editingAccount ? '修改账号信息和访问凭证,留空则保持原凭证不变' : '录入多云平台的访问凭证,用于资源同步与成本统计'}
</DialogDescription>
</DialogHeader>
@@ -314,24 +332,24 @@ export function CloudAccountManager() {
<div className="space-y-4 py-2">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-slate-300">
账号名称 <span className="text-red-400">*</span>
<Label>
账号名称 <span className="text-[var(--dg-red)]">*</span>
</Label>
<Input
placeholder="阿里云生产账号"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="bg-slate-900 border-slate-700 text-slate-100 placeholder:text-slate-500"
className="dg-input"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">
云平台 <span className="text-red-400">*</span>
<Label>
云平台 <span className="text-[var(--dg-red)]">*</span>
</Label>
<select
value={formData.provider}
onChange={(e) => setFormData({ ...formData, provider: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-slate-100 text-sm"
className="w-full px-3 py-2 rounded-md text-sm dg-input"
>
<option value="aliyun">阿里云</option>
<option value="tencent">腾讯云</option>
@@ -343,50 +361,50 @@ export function CloudAccountManager() {
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">
<Label>
AccessKey
{editingAccount && <span className="text-xs text-slate-500 ml-2">留空保持不变</span>}
{editingAccount && <span className="text-xs dg-text-muted ml-2">留空保持不变</span>}
</Label>
<Input
placeholder="LTAI..."
value={formData.access_key}
onChange={(e) => setFormData({ ...formData, access_key: e.target.value })}
className="bg-slate-900 border-slate-700 text-slate-100 placeholder:text-slate-500 font-mono text-sm"
className="dg-input font-mono text-sm"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">
<Label>
SecretKey
{editingAccount && <span className="text-xs text-slate-500 ml-2">留空保持不变</span>}
{editingAccount && <span className="text-xs dg-text-muted ml-2">留空保持不变</span>}
</Label>
<Input
type="password"
placeholder="••••••••"
value={formData.secret_key}
onChange={(e) => setFormData({ ...formData, secret_key: e.target.value })}
className="bg-slate-900 border-slate-700 text-slate-100 placeholder:text-slate-500 font-mono text-sm"
className="dg-input font-mono text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-slate-300">月费用 (元)</Label>
<Label>月费用 (元)</Label>
<Input
type="number"
min="0"
placeholder="0"
value={formData.monthly_cost}
onChange={(e) => setFormData({ ...formData, monthly_cost: e.target.value })}
className="bg-slate-900 border-slate-700 text-slate-100 placeholder:text-slate-500"
className="dg-input"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">状态</Label>
<Label>状态</Label>
<select
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-slate-100 text-sm"
className="w-full px-3 py-2 rounded-md text-sm dg-input"
>
<option value="active">正常</option>
<option value="inactive">已停用</option>
@@ -397,11 +415,11 @@ export function CloudAccountManager() {
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setFormOpen(false)} className="border-slate-700 text-slate-300 hover:bg-slate-700">
<Button variant="outline" onClick={() => setFormOpen(false)} className="dg-btn-secondary">
取消
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700"
className="dg-btn-primary"
disabled={!formData.name.trim() || submitting}
onClick={handleSubmit}
>
@@ -411,6 +429,6 @@ export function CloudAccountManager() {
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</PageShell>
);
}