diff --git a/ROADMAP.md b/ROADMAP.md
index 639d71c..49366d8 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -504,6 +504,7 @@ services/api.ts (1324L)
| Q30 | 前端巨型组件拆分(CloudBilling 534L→295L (-45%),提取 cloud-billing/ 3 个文件 (OverviewTab 110L 概览统计:同步信息+成本告警横幅+趋势图+厂商分布 + RecordsTab 146L 成本记录表格+RecordsFilterToolbar筛选工具栏 + AlertRulesTab 91L 预警规则卡片网格)) | P0 | **done** |
| Q31 | 前端巨型组件拆分(PromDsFormDialog 528L→294L (-44%),提取 6 个文件 (CollapsibleSection 42L 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection 107L 认证折叠 + HeadersSection 64L 自定义Headers + AlertingSection 58L 告警折叠 + MiscSection 47L 杂项折叠 + TestResultBanner 32L 测试结果横幅),3 个折叠区块全部使用共享 CollapsibleSection 消除重复 JSX)) | P0 | **done** |
| Q32 | 前端巨型组件拆分(AuditOperationsTab 527L→275L (-48%),提取 audit-operations/ 3 个文件 (types 43L normalizeLog 纯函数+AuditStatistics 接口 + LogDetailDialog 85L 详情弹窗+DetailRow + StatisticsDialog 108L 统计弹窗+StatCard),清除死代码 useConfirm/_confirm/_handleShowStats/statsOpen/statistics/statsLoading,ESLint errors=0/any=0,tsc -b=0,npm build✓,go build=0,17/17 测试通过) | P0 | **done** |
+| Q33 | 前端巨型组件拆分(DashboardRecent 518L→25L (-95%),提取 dashboard-recent/ 7 个文件 (types 48L + shared-components(StatusDot/SeverityBadge) 35L + AlertMessagesCard 72L + CertificateExpiryCard 99L + ReleaseHistoryCard 99L + OpsOverviewCard 77L + KubernetesClusterCard 97L),主文件改为 barrel re-export 保持向后兼容,tsc -b=0,npm build✓,ESLint=0/0/0,go build=0,17/17 测试通过) | P0 | **done** |
---
diff --git a/frontend/src/components/dashboard/DashboardRecent.tsx b/frontend/src/components/dashboard/DashboardRecent.tsx
index 34d8bab..2a9f3aa 100644
--- a/frontend/src/components/dashboard/DashboardRecent.tsx
+++ b/frontend/src/components/dashboard/DashboardRecent.tsx
@@ -1,518 +1,25 @@
-import { useNavigate } from 'react-router-dom';
-import { useTranslation } from 'react-i18next';
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { ScrollArea } from '@/components/ui/scroll-area';
-import {
- Rocket,
- AlertTriangle,
- ArrowRight,
- Cloud,
- Shield,
- Clock3,
- Globe,
- Database,
- Server,
- Monitor,
- Activity,
-} from 'lucide-react';
-import { cn } from '@/lib/utils';
-import type { AlertEventRecord, CertExpiryRecord } from '@/types';
-
-/** 仪表盘卡片数据类型 — 与 Dashboard API 响应对齐 */
-export interface DashboardRelease {
- id: number | string;
- status: string;
- app_name: string;
- version: string;
- environment?: string;
- commit_id?: string;
- [key: string]: unknown;
-}
-
-export interface DashboardCluster {
- id: number | string;
- code?: string;
- name: string;
- status: string;
- cpu_usage?: number;
- memory_usage?: number;
- node_count?: number;
- pod_count?: number;
- ingress_count?: number;
- deployment_count?: number;
- service_count?: number;
- [key: string]: unknown;
-}
-
-export interface DashboardAutomationStats {
- cron_job_count?: number;
- script_count?: number;
- today_executions?: number;
- success_rate?: number;
-}
-
-export interface DashboardOpsStats {
- domain_count?: number;
- site_task_count?: number;
- middleware_count?: number;
- snmp_device_count?: number;
- monitor_target_count?: number;
-}
-
-export interface DashboardPipelineStats {
- today_count?: number;
- success?: number;
- failed?: number;
- running?: number;
-}
-
/**
- * DashboardRecent - 优化后的仪表盘底部卡片组件
- *
- * 优化点:
- * 1. 使用 dg-card 工具类替代硬编码的 slate 颜色
- * 2. 统一的状态指示器样式
- * 3. 更好的 hover 效果和过渡动画
- * 4. ScrollArea 使用 scrollbar-thin 类
- * 5. 空状态显示更友好
+ * DashboardRecent — barrel re-export
+ *
+ * 原始 518L 单文件拆分为 dashboard-recent/ 目录下独立文件:
+ * - types.ts: DashboardRelease / DashboardCluster / DashboardAutomationStats / DashboardOpsStats / DashboardPipelineStats
+ * - shared-components.tsx: StatusDot / SeverityBadge
+ * - AlertMessagesCard.tsx: 告警消息卡片
+ * - CertificateExpiryCard.tsx: 证书到期卡片
+ * - ReleaseHistoryCard.tsx: 发布历史卡片
+ * - OpsOverviewCard.tsx: 运维概览卡片
+ * - KubernetesClusterCard.tsx: Kubernetes 集群卡片
*/
+export { AlertMessagesCard } from './dashboard-recent/AlertMessagesCard';
+export { CertificateExpiryCard } from './dashboard-recent/CertificateExpiryCard';
+export { ReleaseHistoryCard } from './dashboard-recent/ReleaseHistoryCard';
+export { OpsOverviewCard } from './dashboard-recent/OpsOverviewCard';
+export { KubernetesClusterCard } from './dashboard-recent/KubernetesClusterCard';
-// Status dot component
-function StatusDot({ severity }: { severity: string }) {
- return (
-
- );
-}
-
-// Severity badge
-function SeverityBadge({ severity }: { severity: string }) {
- const { t } = useTranslation();
- return (
-
- {severity === 'critical' ? t('common.error') : severity === 'warning' ? t('common.warning') : t('common.info')}
-
- );
-}
-
-export function AlertMessagesCard({ alerts }: { alerts: AlertEventRecord[] }) {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const recentAlerts = alerts.filter((a) => a.status === 'firing').slice(0, 8);
-
- return (
-
-
-
-
- {t('dashboard.alerts.title')}
- {t('dashboard.alerts.description')}
-
-
-
-
-
-
- {recentAlerts.length === 0 ? (
-
-
-
{t('dashboard.alerts.empty')}
-
{t('dashboard.alerts.emptyHint')}
-
- ) : (
-
- {recentAlerts.map((alert) => (
-
navigate('/alert-center')}
- >
-
-
-
-
- {alert.rule_name}
-
-
-
-
{alert.target}
-
{alert.message}
-
-
- {alert.created_at ? new Date(alert.created_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : ''}
-
-
- ))}
-
- )}
-
-
-
- );
-}
-
-export function CertificateExpiryCard({ expCerts }: { expCerts: CertExpiryRecord[] }) {
- const { t } = useTranslation();
- const expiringDomains = (expCerts ?? [])
- .map((d) => ({
- ...d,
- // eslint-disable-next-line react-hooks/purity
- daysUntilExpiry: d.days_until_expiry ?? Math.ceil((new Date(d.expire_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)),
- }))
- .sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry);
-
- const criticalCount = expiringDomains.filter((d) => d.daysUntilExpiry < 30).length;
- const warningCount = expiringDomains.filter((d) => d.daysUntilExpiry >= 30 && d.daysUntilExpiry < 60).length;
-
- return (
-
-
-
-
- {t('dashboard.certificates.title')}
- {t('dashboard.certificates.description')}
-
-
- {criticalCount > 0 && (
-
-
- {criticalCount} {t('dashboard.certificates.expiringSoon')}
-
- )}
- {warningCount > 0 && (
-
- {warningCount} {t('dashboard.certificates.expiringIn30Days')}
-
- )}
-
-
-
-
-
- {expiringDomains.length === 0 ? (
-
-
-
{t('dashboard.certificates.empty')}
-
- ) : (
-
- {expiringDomains.map((domain) => (
-
-
-
-
-
-
-
{domain.name}
-
{t('dashboard.certificates.expireDate')}: {domain.expire_date?.substring(0, 10)}
-
-
-
- {domain.daysUntilExpiry < 0 ? t('dashboard.certificates.expired') : `${domain.daysUntilExpiry} ${t('dashboard.certificates.days')}`}
-
-
- ))}
-
- )}
-
-
-
- );
-}
-
-export function ReleaseHistoryCard({ releases }: { releases: DashboardRelease[] }) {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const releaseList = releases?.slice(0, 5) ?? [];
-
- return (
-
-
-
-
- {t('dashboard.releases.title')}
- {t('dashboard.releases.description')}
-
-
-
-
-
- {releaseList.length === 0 ? (
-
-
-
{t('dashboard.releases.empty')}
-
- ) : (
-
- {releaseList.map((release) => (
-
-
-
-
-
-
- {release.app_name}
-
- {release.version}
-
-
-
- {release.environment}
- ·
- {release.commit_id}
-
-
-
- {release.status === 'success' ? t('dashboard.releases.success') :
- release.status === 'failed' ? t('dashboard.releases.failed') :
- release.status === 'deploying' ? t('dashboard.releases.deploying') :
- release.status === 'rolled_back' ? t('dashboard.releases.rolledBack') : t('dashboard.releases.building')}
-
-
- ))}
-
- )}
-
-
- );
-}
-
-/** 运维概览 — 资产 / 监控 / 任务快捷入口 // Logan.Li */
-export function OpsOverviewCard({
- automation,
- ops,
- serverCount,
- hostMonitorCount,
- firingAlerts,
-}: {
- automation?: DashboardAutomationStats;
- ops?: DashboardOpsStats;
- serverCount?: number;
- hostMonitorCount?: number;
- firingAlerts?: number;
-}) {
- const { t } = useTranslation();
- const navigate = useNavigate();
-
- const tiles = [
- { key: 'servers', label: t('dashboard.ops.servers'), value: serverCount ?? 0, icon: Server, path: '/servers', color: 'text-[var(--dg-blue)]' },
- { key: 'domains', label: t('dashboard.ops.domains'), value: ops?.domain_count ?? 0, icon: Globe, path: '/domains', color: 'text-[var(--dg-cyan)]' },
- { key: 'host', label: t('dashboard.ops.hostMonitor'), value: hostMonitorCount ?? serverCount ?? 0, icon: Monitor, path: '/host-monitor', color: 'text-[var(--dg-green)]' },
- { key: 'mw', label: t('dashboard.ops.middleware'), value: ops?.middleware_count ?? 0, icon: Database, path: '/monitor-center?tab=middleware', color: 'text-[var(--dg-red)]' },
- { key: 'site', label: t('dashboard.ops.siteProbes'), value: ops?.site_task_count ?? 0, icon: Activity, path: '/monitor-center?tab=site', color: 'text-[var(--dg-yellow)]' },
- { key: 'cron', label: t('dashboard.ops.cronJobs'), value: automation?.cron_job_count ?? 0, icon: Clock3, path: '/cronjobs', color: 'text-[var(--dg-purple)]' },
- ];
-
- return (
-
-
-
-
- {t('dashboard.ops.title')}
- {t('dashboard.ops.description')}
-
-
-
-
-
-
- {tiles.map((tile) => (
-
- ))}
-
-
-
- {t('dashboard.ops.todayExecutions')}: {automation?.today_executions ?? 0}
-
-
-
-
-
- );
-}
-
-export function KubernetesClusterCard({ clusters }: { clusters: DashboardCluster[] }) {
- const { t } = useTranslation();
- const navigate = useNavigate();
-
- return (
-
-
-
-
- {t('dashboard.clusters.title')}
- {t('dashboard.clusters.description')}
-
-
-
-
-
- {clusters.length === 0 ? (
-
-
-
{t('dashboard.clusters.empty')}
-
- ) : (
-
- {clusters.map((cluster) => {
- const cpuUsage = Number(cluster.cpu_usage ?? 0);
- const memoryUsage = Number(cluster.memory_usage ?? 0);
- const hasUsage = cpuUsage > 0 || memoryUsage > 0;
- const metrics = hasUsage
- ? [
- { label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 },
- { label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 },
- { label: t('dashboard.clusters.cpu'), value: `${cpuUsage.toFixed(1)}%` },
- { label: t('dashboard.clusters.memory'), value: `${memoryUsage.toFixed(1)}%` },
- ]
- : [
- { label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 },
- { label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 },
- { label: t('dashboard.clusters.ingress'), value: cluster.ingress_count ?? 0 },
- { label: t('dashboard.clusters.deployments'), value: cluster.deployment_count ?? 0 },
- ];
- return (
-
navigate(`/kubernetes/${cluster.code || cluster.id}`)}
- >
-
-
-
- {cluster.name}
-
-
- {cluster.status === 'healthy' ? t('dashboard.clusters.healthy') : cluster.status === 'warning' ? t('dashboard.clusters.warning') : t('dashboard.clusters.error')}
-
-
-
- {metrics.map((item) => (
-
-
{item.value}
-
{item.label}
-
- ))}
-
-
- );
- })}
-
- )}
-
-
- );
-}
+export type {
+ DashboardRelease,
+ DashboardCluster,
+ DashboardAutomationStats,
+ DashboardOpsStats,
+ DashboardPipelineStats,
+} from './dashboard-recent/types';
diff --git a/frontend/src/components/dashboard/dashboard-recent/AlertMessagesCard.tsx b/frontend/src/components/dashboard/dashboard-recent/AlertMessagesCard.tsx
new file mode 100644
index 0000000..15e62c7
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-recent/AlertMessagesCard.tsx
@@ -0,0 +1,72 @@
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { AlertTriangle, ArrowRight } from 'lucide-react';
+import type { AlertEventRecord } from '@/types';
+import { StatusDot, SeverityBadge } from './shared-components';
+
+export function AlertMessagesCard({ alerts }: { alerts: AlertEventRecord[] }) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const recentAlerts = alerts.filter((a) => a.status === 'firing').slice(0, 8);
+
+ return (
+
+
+
+
+ {t('dashboard.alerts.title')}
+ {t('dashboard.alerts.description')}
+
+
+
+
+
+
+ {recentAlerts.length === 0 ? (
+
+
+
{t('dashboard.alerts.empty')}
+
{t('dashboard.alerts.emptyHint')}
+
+ ) : (
+
+ {recentAlerts.map((alert) => (
+
navigate('/alert-center')}
+ >
+
+
+
+
+ {alert.rule_name}
+
+
+
+
{alert.target}
+
{alert.message}
+
+
+ {alert.created_at ? new Date(alert.created_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : ''}
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/dashboard/dashboard-recent/CertificateExpiryCard.tsx b/frontend/src/components/dashboard/dashboard-recent/CertificateExpiryCard.tsx
new file mode 100644
index 0000000..f921768
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-recent/CertificateExpiryCard.tsx
@@ -0,0 +1,99 @@
+import { useTranslation } from 'react-i18next';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { AlertTriangle, Shield } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import type { CertExpiryRecord } from '@/types';
+
+export function CertificateExpiryCard({ expCerts }: { expCerts: CertExpiryRecord[] }) {
+ const { t } = useTranslation();
+ const expiringDomains = (expCerts ?? [])
+ .map((d) => ({
+ ...d,
+ // eslint-disable-next-line react-hooks/purity
+ daysUntilExpiry: d.days_until_expiry ?? Math.ceil((new Date(d.expire_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)),
+ }))
+ .sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry);
+
+ const criticalCount = expiringDomains.filter((d) => d.daysUntilExpiry < 30).length;
+ const warningCount = expiringDomains.filter((d) => d.daysUntilExpiry >= 30 && d.daysUntilExpiry < 60).length;
+
+ return (
+
+
+
+
+ {t('dashboard.certificates.title')}
+ {t('dashboard.certificates.description')}
+
+
+ {criticalCount > 0 && (
+
+
+ {criticalCount} {t('dashboard.certificates.expiringSoon')}
+
+ )}
+ {warningCount > 0 && (
+
+ {warningCount} {t('dashboard.certificates.expiringIn30Days')}
+
+ )}
+
+
+
+
+
+ {expiringDomains.length === 0 ? (
+
+
+
{t('dashboard.certificates.empty')}
+
+ ) : (
+
+ {expiringDomains.map((domain) => (
+
+
+
+
+
+
+
{domain.name}
+
{t('dashboard.certificates.expireDate')}: {domain.expire_date?.substring(0, 10)}
+
+
+
+ {domain.daysUntilExpiry < 0 ? t('dashboard.certificates.expired') : `${domain.daysUntilExpiry} ${t('dashboard.certificates.days')}`}
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/dashboard/dashboard-recent/KubernetesClusterCard.tsx b/frontend/src/components/dashboard/dashboard-recent/KubernetesClusterCard.tsx
new file mode 100644
index 0000000..eb7b8a1
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-recent/KubernetesClusterCard.tsx
@@ -0,0 +1,97 @@
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Cloud, ArrowRight } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import type { DashboardCluster } from './types';
+
+export function KubernetesClusterCard({ clusters }: { clusters: DashboardCluster[] }) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+ {t('dashboard.clusters.title')}
+ {t('dashboard.clusters.description')}
+
+
+
+
+
+ {clusters.length === 0 ? (
+
+
+
{t('dashboard.clusters.empty')}
+
+ ) : (
+
+ {clusters.map((cluster) => {
+ const cpuUsage = Number(cluster.cpu_usage ?? 0);
+ const memoryUsage = Number(cluster.memory_usage ?? 0);
+ const hasUsage = cpuUsage > 0 || memoryUsage > 0;
+ const metrics = hasUsage
+ ? [
+ { label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 },
+ { label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 },
+ { label: t('dashboard.clusters.cpu'), value: `${cpuUsage.toFixed(1)}%` },
+ { label: t('dashboard.clusters.memory'), value: `${memoryUsage.toFixed(1)}%` },
+ ]
+ : [
+ { label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 },
+ { label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 },
+ { label: t('dashboard.clusters.ingress'), value: cluster.ingress_count ?? 0 },
+ { label: t('dashboard.clusters.deployments'), value: cluster.deployment_count ?? 0 },
+ ];
+ return (
+
navigate(`/kubernetes/${cluster.code || cluster.id}`)}
+ >
+
+
+
+ {cluster.name}
+
+
+ {cluster.status === 'healthy' ? t('dashboard.clusters.healthy') : cluster.status === 'warning' ? t('dashboard.clusters.warning') : t('dashboard.clusters.error')}
+
+
+
+ {metrics.map((item) => (
+
+
{item.value}
+
{item.label}
+
+ ))}
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/dashboard/dashboard-recent/OpsOverviewCard.tsx b/frontend/src/components/dashboard/dashboard-recent/OpsOverviewCard.tsx
new file mode 100644
index 0000000..8466f9f
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-recent/OpsOverviewCard.tsx
@@ -0,0 +1,77 @@
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { ArrowRight, Clock3, Globe, Database, Server, Monitor, Activity } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import type { DashboardAutomationStats, DashboardOpsStats } from './types';
+
+/** 运维概览 — 资产 / 监控 / 任务快捷入口 */
+export function OpsOverviewCard({
+ automation,
+ ops,
+ serverCount,
+ hostMonitorCount,
+ firingAlerts,
+}: {
+ automation?: DashboardAutomationStats;
+ ops?: DashboardOpsStats;
+ serverCount?: number;
+ hostMonitorCount?: number;
+ firingAlerts?: number;
+}) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ const tiles = [
+ { key: 'servers', label: t('dashboard.ops.servers'), value: serverCount ?? 0, icon: Server, path: '/servers', color: 'text-[var(--dg-blue)]' },
+ { key: 'domains', label: t('dashboard.ops.domains'), value: ops?.domain_count ?? 0, icon: Globe, path: '/domains', color: 'text-[var(--dg-cyan)]' },
+ { key: 'host', label: t('dashboard.ops.hostMonitor'), value: hostMonitorCount ?? serverCount ?? 0, icon: Monitor, path: '/host-monitor', color: 'text-[var(--dg-green)]' },
+ { key: 'mw', label: t('dashboard.ops.middleware'), value: ops?.middleware_count ?? 0, icon: Database, path: '/monitor-center?tab=middleware', color: 'text-[var(--dg-red)]' },
+ { key: 'site', label: t('dashboard.ops.siteProbes'), value: ops?.site_task_count ?? 0, icon: Activity, path: '/monitor-center?tab=site', color: 'text-[var(--dg-yellow)]' },
+ { key: 'cron', label: t('dashboard.ops.cronJobs'), value: automation?.cron_job_count ?? 0, icon: Clock3, path: '/cronjobs', color: 'text-[var(--dg-purple)]' },
+ ];
+
+ return (
+
+
+
+
+ {t('dashboard.ops.title')}
+ {t('dashboard.ops.description')}
+
+
+
+
+
+
+ {tiles.map((tile) => (
+
+ ))}
+
+
+
+ {t('dashboard.ops.todayExecutions')}: {automation?.today_executions ?? 0}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/dashboard/dashboard-recent/ReleaseHistoryCard.tsx b/frontend/src/components/dashboard/dashboard-recent/ReleaseHistoryCard.tsx
new file mode 100644
index 0000000..85549e8
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-recent/ReleaseHistoryCard.tsx
@@ -0,0 +1,99 @@
+import { useNavigate } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Rocket, ArrowRight } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import type { DashboardRelease } from './types';
+
+export function ReleaseHistoryCard({ releases }: { releases: DashboardRelease[] }) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const releaseList = releases?.slice(0, 5) ?? [];
+
+ return (
+
+
+
+
+ {t('dashboard.releases.title')}
+ {t('dashboard.releases.description')}
+
+
+
+
+
+ {releaseList.length === 0 ? (
+
+
+
{t('dashboard.releases.empty')}
+
+ ) : (
+
+ {releaseList.map((release) => (
+
+
+
+
+
+
+ {release.app_name}
+
+ {release.version}
+
+
+
+ {release.environment}
+ ·
+ {release.commit_id}
+
+
+
+ {release.status === 'success' ? t('dashboard.releases.success') :
+ release.status === 'failed' ? t('dashboard.releases.failed') :
+ release.status === 'deploying' ? t('dashboard.releases.deploying') :
+ release.status === 'rolled_back' ? t('dashboard.releases.rolledBack') : t('dashboard.releases.building')}
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/dashboard/dashboard-recent/shared-components.tsx b/frontend/src/components/dashboard/dashboard-recent/shared-components.tsx
new file mode 100644
index 0000000..e1e48ec
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-recent/shared-components.tsx
@@ -0,0 +1,35 @@
+import { useTranslation } from 'react-i18next';
+import { Badge } from '@/components/ui/badge';
+import { cn } from '@/lib/utils';
+
+/** Status dot component */
+export function StatusDot({ severity }: { severity: string }) {
+ return (
+
+ );
+}
+
+/** Severity badge */
+export function SeverityBadge({ severity }: { severity: string }) {
+ const { t } = useTranslation();
+ return (
+
+ {severity === 'critical' ? t('common.error') : severity === 'warning' ? t('common.warning') : t('common.info')}
+
+ );
+}
diff --git a/frontend/src/components/dashboard/dashboard-recent/types.ts b/frontend/src/components/dashboard/dashboard-recent/types.ts
new file mode 100644
index 0000000..1c40bbc
--- /dev/null
+++ b/frontend/src/components/dashboard/dashboard-recent/types.ts
@@ -0,0 +1,48 @@
+/** Dashboard 卡片数据类型 — 与 Dashboard API 响应对齐 */
+
+export interface DashboardRelease {
+ id: number | string;
+ status: string;
+ app_name: string;
+ version: string;
+ environment?: string;
+ commit_id?: string;
+ [key: string]: unknown;
+}
+
+export interface DashboardCluster {
+ id: number | string;
+ code?: string;
+ name: string;
+ status: string;
+ cpu_usage?: number;
+ memory_usage?: number;
+ node_count?: number;
+ pod_count?: number;
+ ingress_count?: number;
+ deployment_count?: number;
+ service_count?: number;
+ [key: string]: unknown;
+}
+
+export interface DashboardAutomationStats {
+ cron_job_count?: number;
+ script_count?: number;
+ today_executions?: number;
+ success_rate?: number;
+}
+
+export interface DashboardOpsStats {
+ domain_count?: number;
+ site_task_count?: number;
+ middleware_count?: number;
+ snmp_device_count?: number;
+ monitor_target_count?: number;
+}
+
+export interface DashboardPipelineStats {
+ today_count?: number;
+ success?: number;
+ failed?: number;
+ running?: number;
+}