feat(ui): 改造控制台页面
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 199h23m59s
CI/CD Pipeline / Frontend Lint (push) Failing after 199h24m30s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 199h24m41s

- KPI 卡片网格:渐变背景 + 大号数字 + 趋势指标
- 快速入口卡片网格:4列布局,图标渐变
- 最近任务卡片列表:替代原 Table 布局
- 使用统计图表:纯 CSS 柱状图展示本周趋势
- 系统公告区域 + 存储用量进度条
- 响应式适配:1200px/768px 断点
- 全部使用 CSS 变量,支持深色/浅色主题
- 使用 mock 数据
This commit is contained in:
灵应
2026-07-01 05:29:48 +08:00
parent dc2555bae4
commit d8c8fcf858
2 changed files with 975 additions and 206 deletions
+455 -206
View File
@@ -1,225 +1,474 @@
/**
* 仪表盘页面
* 展示用户用量总览和最近生成记录
* 控制台页面 — V21 设计系统
* KPI 卡片网格 + 快速入口 + 最近任务卡片列表 + 使用统计图表 + 公告
* 使用 mock 数据,CSS 变量,V21 组件
*/
import React from "react";
import { useQuery } from "@tanstack/react-query";
import {
Card,
Col,
Row,
Statistic,
Table,
Tag,
Spin,
Button,
Progress,
Space,
Alert,
} from "antd";
import {
FileOutlined,
VideoCameraOutlined,
AudioOutlined,
FileTextOutlined,
CloudServerOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import { getDashboardOverview } from "@/api/dashboard";
import type { ColumnsType } from "antd/es/table";
import PageHead from "@/components/layout/PageHead";
import { Button, Tag } from "@/components/ui";
import "./dashboard.css";
/** 格式化文件大小 */
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
/* ============================================================
* Mock 数据
* ============================================================ */
interface KpiItem {
key: string;
icon: string;
iconGradient: string;
value: string;
label: string;
trend: string;
trendDirection: "up" | "down" | "neutral";
accent: string;
}
const kpiData: KpiItem[] = [
{
key: "projects",
icon: "🎬",
iconGradient: "linear-gradient(135deg, #6366f1, #4f46e5)",
value: "12",
label: "项目总数",
trend: "↑ 2 本月新增",
trendDirection: "up",
accent: "#6366f1",
},
{
key: "assets",
icon: "📦",
iconGradient: "linear-gradient(135deg, #0ea5e9, #0284c7)",
value: "486",
label: "素材总数",
trend: "↑ 38 本月上传",
trendDirection: "up",
accent: "#0ea5e9",
},
{
key: "generations",
icon: "✨",
iconGradient: "linear-gradient(135deg, #10b981, #059669)",
value: "156",
label: "本月生成数",
trend: "↑ 23% 较上月",
trendDirection: "up",
accent: "#10b981",
},
{
key: "storage",
icon: "💾",
iconGradient: "linear-gradient(135deg, #f59e0b, #d97706)",
value: "2.4GB",
label: "存储空间",
trend: "已用 24%",
trendDirection: "neutral",
accent: "#f59e0b",
},
];
interface QuickEntry {
id: string;
icon: string;
iconGradient: string;
title: string;
description: string;
path: string;
}
const quickEntries: QuickEntry[] = [
{
id: "titles",
icon: "📝",
iconGradient: "linear-gradient(135deg, #6366f1, #4f46e5)",
title: "标题库",
description: "24条标题 · 5个分类",
path: "/titles",
},
{
id: "assets",
icon: "📦",
iconGradient: "linear-gradient(135deg, #0ea5e9, #0284c7)",
title: "素材库",
description: "486个素材 · 3个素材库",
path: "/assets",
},
{
id: "generate",
icon: "✨",
iconGradient: "linear-gradient(135deg, #10b981, #059669)",
title: "一键生成",
description: "开始创作新视频",
path: "/generate",
},
{
id: "products",
icon: "🎬",
iconGradient: "linear-gradient(135deg, #f59e0b, #d97706)",
title: "成片库",
description: "89个成片 · 3个待复核",
path: "/products",
},
];
type TaskStatus = "completed" | "processing" | "pending" | "failed";
interface RecentTask {
id: string;
name: string;
type: string;
template: string;
status: TaskStatus;
date: string;
duration?: string;
}
const statusLabel: Record<TaskStatus, string> = {
completed: "已完成",
processing: "进行中",
pending: "排队中",
failed: "失败",
};
/** 任务状态标签 */
const StatusTag: React.FC<{ status: string }> = ({ status }) => {
const config: Record<string, { color: string; icon: React.ReactNode }> = {
completed: { color: "success", icon: <CheckCircleOutlined /> },
processing: { color: "processing", icon: <ClockCircleOutlined /> },
pending: { color: "default", icon: <ClockCircleOutlined /> },
failed: { color: "error", icon: <CloseCircleOutlined /> },
};
const c = config[status] || config.pending;
return (
<Tag color={c.color} icon={c.icon}>
{status}
</Tag>
);
const recentTasks: RecentTask[] = [
{
id: "t-1",
name: "产品介绍视频_春季促销",
type: "视频生成",
template: "商品展示模板",
status: "completed",
date: "2026-07-01 09:30",
duration: "2分18秒",
},
{
id: "t-2",
name: "品牌宣传片_终版",
type: "视频生成",
template: "品牌宣传模板",
status: "processing",
date: "2026-07-01 10:15",
},
{
id: "t-3",
name: "用户评价合集",
type: "视频生成",
template: "评价展示模板",
status: "completed",
date: "2026-06-30 16:42",
duration: "1分45秒",
},
{
id: "t-4",
name: "新品发布预告",
type: "视频生成",
template: "新品预告模板",
status: "pending",
date: "2026-06-30 14:20",
},
{
id: "t-5",
name: "活动回顾_618大促",
type: "视频生成",
template: "活动回顾模板",
status: "failed",
date: "2026-06-29 11:05",
},
];
interface ChartItem {
label: string;
value: number;
}
const weeklyData: ChartItem[] = [
{ label: "周一", value: 18 },
{ label: "周二", value: 25 },
{ label: "周三", value: 32 },
{ label: "周四", value: 28 },
{ label: "周五", value: 42 },
{ label: "周六", value: 15 },
{ label: "周日", value: 8 },
];
interface Announcement {
id: string;
tag: "update" | "notice" | "activity";
tagLabel: string;
title: string;
date: string;
}
const announcements: Announcement[] = [
{
id: "a-1",
tag: "update",
tagLabel: "更新",
title: "系统已升级至 v2.0,新增批量生成功能",
date: "2026-07-01",
},
{
id: "a-2",
tag: "activity",
tagLabel: "活动",
title: "7月创作挑战赛已开启,参与赢积分奖励",
date: "2026-06-28",
},
{
id: "a-3",
tag: "notice",
tagLabel: "公告",
title: "7月3日凌晨 2:00-4:00 系统维护通知",
date: "2026-06-25",
},
];
/* ============================================================
* 工具函数
* ============================================================ */
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 6) return "夜深了";
if (hour < 12) return "早上好";
if (hour < 14) return "中午好";
if (hour < 18) return "下午好";
return "晚上好";
};
const formatDate = () => {
const d = new Date();
const weekDays = ["日", "一", "二", "三", "四", "五", "六"];
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}日 星期${weekDays[d.getDay()]}`;
};
/* ============================================================
* 组件
* ============================================================ */
const Dashboard: React.FC = () => {
const navigate = useNavigate();
const { data, isLoading, isError } = useQuery({
queryKey: ["dashboard-overview"],
queryFn: getDashboardOverview,
});
const taskColumns: ColumnsType<
NonNullable<typeof data>["recent_tasks"][number]
> = [
{
title: "任务类型",
dataIndex: "task_type",
key: "task_type",
render: (type: string) => (type === "generation" ? "视频生成" : type),
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (status: string) => <StatusTag status={status} />,
},
{
title: "进度",
dataIndex: "progress",
key: "progress",
render: (progress: number) => (
<Progress percent={Math.round(progress * 100)} size="small" />
),
},
{
title: "信息",
dataIndex: "user_message",
key: "user_message",
ellipsis: true,
},
{
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
render: (t: string) => (t ? new Date(t).toLocaleString("zh-CN") : "-"),
},
];
if (isLoading) {
return (
<div style={{ textAlign: "center", padding: 80 }}>
<Spin size="large" />
</div>
);
}
if (isError) {
return (
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
<Alert
type="error"
message="加载数据失败"
description="仪表盘数据获取失败,请刷新页面重试。"
showIcon
/>
</div>
);
}
const maxChart = Math.max(...weeklyData.map((d) => d.value));
return (
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
<PageHead title="概览" />
<div className="xx-dashboard-page">
{/* ── 欢迎头部 ─────────────────────────────────────────── */}
<div className="xx-dashboard-welcome">
<h2>{getGreeting()} 👋</h2>
<p>{formatDate()} </p>
</div>
{/* 用量统计卡片 */}
<Row gutter={[16, 16]}>
<Col xs={12} sm={8} md={4}>
<Card hoverable onClick={() => navigate("/assets")}>
<Statistic
title="素材"
value={data?.total_assets ?? 0}
prefix={<FileOutlined />}
suffix="个"
/>
</Card>
</Col>
<Col xs={12} sm={8} md={4}>
<Card hoverable onClick={() => navigate("/assets")}>
<Statistic
title="存储空间"
value={formatFileSize(data?.used_storage_bytes ?? 0)}
prefix={<CloudServerOutlined />}
/>
</Card>
</Col>
<Col xs={12} sm={8} md={4}>
<Card hoverable onClick={() => navigate("/titles")}>
<Statistic
title="标题"
value={data?.total_titles ?? 0}
prefix={<FileTextOutlined />}
suffix="条"
/>
</Card>
</Col>
<Col xs={12} sm={8} md={4}>
<Card hoverable onClick={() => navigate("/voices")}>
<Statistic
title="配音"
value={data?.total_voices ?? 0}
prefix={<AudioOutlined />}
suffix="条"
/>
</Card>
</Col>
<Col xs={12} sm={8} md={4}>
<Card hoverable onClick={() => navigate("/history")}>
<Statistic
title="生成任务"
value={data?.total_tasks ?? 0}
prefix={<VideoCameraOutlined />}
suffix="次"
/>
</Card>
</Col>
<Col xs={12} sm={8} md={4}>
<Card hoverable onClick={() => navigate("/products")}>
<Statistic
title="成品"
value={data?.total_products ?? 0}
prefix={<VideoCameraOutlined />}
suffix="个"
/>
</Card>
</Col>
</Row>
{/* 快捷操作 */}
<Card style={{ marginTop: 24 }}>
<Space wrap>
<Button type="primary" onClick={() => navigate("/generate")}>
</Button>
<Button onClick={() => navigate("/assets")}></Button>
<Button onClick={() => navigate("/templates")}></Button>
<Button onClick={() => navigate("/subscription")}></Button>
</Space>
</Card>
{/* 最近生成任务 */}
<Card title="最近生成" style={{ marginTop: 24 }}>
<Table
columns={taskColumns}
dataSource={data?.recent_tasks || []}
rowKey="id"
pagination={false}
size="small"
locale={{ emptyText: "暂无生成记录" }}
scroll={{ x: 600 }}
/>
{(data?.recent_tasks?.length ?? 0) > 0 && (
<div style={{ textAlign: "center", marginTop: 12 }}>
<Button type="link" onClick={() => navigate("/history")}>
</Button>
{/* ── KPI 卡片网格 ─────────────────────────────────────── */}
<div className="xx-kpi-grid">
{kpiData.map((item) => (
<div
key={item.key}
className="xx-kpi-card"
style={{ "--kpi-accent": item.accent } as React.CSSProperties}
>
<div
className="xx-kpi-icon"
style={{ background: item.iconGradient }}
>
{item.icon}
</div>
<div className="xx-kpi-value">{item.value}</div>
<div className="xx-kpi-label">{item.label}</div>
<span
className={`xx-kpi-trend xx-kpi-trend--${item.trendDirection}`}
>
{item.trend}
</span>
</div>
)}
</Card>
))}
</div>
{/* ── 主内容区:左侧任务+图表 / 右侧公告 ──────────────── */}
<div className="xx-dashboard-main">
{/* 左列 */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--space-md)",
}}
>
{/* 最近任务 */}
<div className="xx-dashboard-section">
<div className="xx-dashboard-section-header">
<h3></h3>
<button onClick={() => navigate("/history")}></button>
</div>
<div className="xx-task-list">
{recentTasks.map((task) => (
<div key={task.id} className="xx-task-item">
<div className="xx-task-info">
<h4>{task.name}</h4>
<span>
{task.type} · {task.template}
</span>
</div>
<Tag
variant={
task.status === "completed"
? "success"
: task.status === "processing"
? "info"
: task.status === "failed"
? "error"
: "warning"
}
>
{statusLabel[task.status]}
</Tag>
<div className="xx-task-time">
<span>{task.date}</span>
{task.status === "completed"
? `耗时 ${task.duration}`
: task.status === "processing"
? "生成中..."
: task.status === "failed"
? "请重试"
: "等待中"}
</div>
<div className="xx-task-action">
<Button
buttonType="ghost"
buttonSize="sm"
onClick={() => navigate("/history")}
>
</Button>
</div>
</div>
))}
</div>
</div>
{/* 使用统计图表 */}
<div className="xx-dashboard-section">
<div className="xx-dashboard-section-header">
<h3></h3>
<span
style={{
fontSize: "12px",
color: "var(--text-secondary)",
}}
>
{weeklyData.reduce((s, d) => s + d.value, 0)}
</span>
</div>
<div className="xx-chart-container">
<div className="xx-chart-bars">
{weeklyData.map((d, i) => (
<div key={i} className="xx-chart-bar-wrapper">
<div
className="xx-chart-bar"
style={{
height: `${(d.value / maxChart) * 100}%`,
}}
>
<span className="xx-chart-bar-value">{d.value}</span>
</div>
</div>
))}
</div>
<div className="xx-chart-labels">
{weeklyData.map((d, i) => (
<div key={i} className="xx-chart-label">
{d.label}
</div>
))}
</div>
</div>
</div>
</div>
{/* 右列 — 公告 + 存储用量 */}
<div className="xx-dashboard-section" style={{ alignSelf: "start" }}>
<div className="xx-dashboard-section-header">
<h3></h3>
</div>
<div className="xx-announcement-list">
{announcements.map((a) => (
<div key={a.id} className="xx-announcement-item">
<span
className={`xx-announcement-tag xx-announcement-tag--${a.tag}`}
>
{a.tagLabel}
</span>
<div className="xx-announcement-content">
<h4>{a.title}</h4>
<time>{a.date}</time>
</div>
</div>
))}
</div>
{/* 存储用量 */}
<div style={{ marginTop: "var(--space-md)" }}>
<div
style={{
fontSize: "13px",
fontWeight: 500,
color: "var(--text-primary)",
marginBottom: "4px",
}}
>
</div>
<div className="xx-storage-bar">
<div className="xx-storage-bar-track">
<div
className="xx-storage-bar-fill"
style={{ width: "24%" }}
/>
</div>
<div className="xx-storage-bar-label">
<span>2.4 GB </span>
<span>10 GB </span>
</div>
</div>
</div>
</div>
</div>
{/* ── 快速入口 ─────────────────────────────────────────── */}
<div style={{ marginBottom: "var(--space-md)" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "var(--space-md)",
}}
>
<h3
style={{
margin: 0,
fontSize: "var(--font-size-base)",
fontWeight: "var(--font-weight-semibold)",
color: "var(--text-primary)",
}}
>
</h3>
</div>
<div className="xx-quick-grid">
{quickEntries.map((entry) => (
<div
key={entry.id}
className="xx-quick-card"
onClick={() => navigate(entry.path)}
>
<div
className="xx-quick-card-icon"
style={{ background: entry.iconGradient }}
>
{entry.icon}
</div>
<h3>{entry.title}</h3>
<p>{entry.description}</p>
</div>
))}
</div>
</div>
</div>
);
};
+520
View File
@@ -0,0 +1,520 @@
/**
* 控制台页面 - V21 设计系统样式
* KPI 卡片网格 + 快速入口 + 最近任务卡片列表 + 使用统计图表 + 公告
* 统一使用 CSS 变量,支持深色/浅色主题
*/
@import "../../styles/global.css";
/* ============================================================
页面容器
============================================================ */
.xx-dashboard-page {
min-height: 100%;
padding: var(--space-xl);
}
/* ============================================================
欢迎头部
============================================================ */
.xx-dashboard-welcome {
margin-bottom: var(--space-lg);
}
.xx-dashboard-welcome h2 {
margin: 0 0 4px;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
}
.xx-dashboard-welcome p {
margin: 0;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
/* ============================================================
KPI 卡片网格
============================================================ */
.xx-kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.xx-kpi-card {
background: linear-gradient(180deg, var(--bg-primary), var(--bg-secondary, #f8fafc));
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
transition: var(--transition-all);
position: relative;
overflow: hidden;
}
.xx-kpi-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--kpi-accent, var(--primary-color));
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.xx-kpi-card:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.xx-kpi-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-bottom: 12px;
}
.xx-kpi-value {
font-size: 30px;
font-weight: 800;
color: var(--text-primary);
line-height: 1.2;
margin-bottom: 4px;
}
.xx-kpi-label {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.xx-kpi-trend {
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--radius-sm);
}
.xx-kpi-trend--up {
color: var(--success-color);
background: var(--success-soft);
}
.xx-kpi-trend--down {
color: var(--error-color);
background: var(--error-soft);
}
.xx-kpi-trend--neutral {
color: var(--text-secondary);
background: var(--bg-secondary, #f1f5f9);
}
/* ============================================================
主内容区两栏布局
============================================================ */
.xx-dashboard-main {
display: grid;
grid-template-columns: 1fr 320px;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
/* ============================================================
区块卡片
============================================================ */
.xx-dashboard-section {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--space-md);
}
.xx-dashboard-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-md);
}
.xx-dashboard-section-header h3 {
margin: 0;
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.xx-dashboard-section-header a,
.xx-dashboard-section-header button {
font-size: var(--font-size-sm);
color: var(--primary-color);
cursor: pointer;
background: none;
border: none;
padding: 0;
text-decoration: none;
transition: var(--transition-color);
}
.xx-dashboard-section-header a:hover,
.xx-dashboard-section-header button:hover {
color: var(--primary-hover);
text-decoration: underline;
}
/* ============================================================
最近任务卡片列表
============================================================ */
.xx-task-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.xx-task-item {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 14px 16px;
display: grid;
grid-template-columns: 1fr auto auto auto;
gap: 16px;
align-items: center;
transition: var(--transition-all);
}
.xx-task-item:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-sm);
}
.xx-task-info h4 {
margin: 0 0 4px;
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.xx-task-info span {
font-size: 12px;
color: var(--text-secondary);
}
.xx-task-status {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.xx-task-status--completed {
color: var(--success-color);
background: var(--success-soft);
border: 1px solid var(--success-border);
}
.xx-task-status--processing {
color: var(--info-color);
background: var(--primary-soft);
border: 1px solid var(--color-primary-200);
}
.xx-task-status--pending {
color: var(--text-secondary);
background: var(--bg-secondary, #f1f5f9);
border: 1px solid var(--border-color);
}
.xx-task-status--failed {
color: var(--error-color);
background: var(--error-soft);
border: 1px solid var(--error-border);
}
.xx-task-time {
text-align: right;
font-size: 12px;
color: var(--text-secondary);
min-width: 100px;
}
.xx-task-time span {
display: block;
margin-bottom: 2px;
}
.xx-task-action {
font-size: 12px;
}
/* ============================================================
使用统计图表(纯 CSS 柱状图)
============================================================ */
.xx-chart-container {
padding: var(--space-sm) 0;
}
.xx-chart-bars {
display: flex;
align-items: flex-end;
gap: 8px;
height: 160px;
padding-top: var(--space-sm);
}
.xx-chart-bar-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
}
.xx-chart-bar {
width: 100%;
max-width: 36px;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
background: var(--gradient-primary);
transition: height 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
min-height: 4px;
cursor: pointer;
}
.xx-chart-bar:hover {
opacity: 0.85;
}
.xx-chart-bar-value {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
opacity: 0;
transition: var(--transition-opacity);
}
.xx-chart-bar:hover .xx-chart-bar-value {
opacity: 1;
}
.xx-chart-labels {
display: flex;
gap: 8px;
margin-top: 8px;
}
.xx-chart-label {
flex: 1;
text-align: center;
font-size: 11px;
color: var(--text-secondary);
}
/* ============================================================
快速入口卡片网格
============================================================ */
.xx-quick-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
.xx-quick-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
cursor: pointer;
transition: var(--transition-all);
text-align: center;
}
.xx-quick-card:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.xx-quick-card-icon {
width: 52px;
height: 52px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin: 0 auto 14px;
color: #fff;
}
.xx-quick-card h3 {
margin: 0 0 8px;
font-size: 16px;
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.xx-quick-card p {
margin: 0;
color: var(--text-secondary);
font-size: 13px;
}
/* ============================================================
公告区域
============================================================ */
.xx-announcement-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.xx-announcement-item {
display: flex;
gap: 12px;
padding: 12px;
border-radius: var(--radius-md);
background: var(--bg-secondary, #f8fafc);
border: 1px solid var(--border-color);
transition: var(--transition-all);
}
.xx-announcement-item:hover {
border-color: var(--primary-color);
background: var(--primary-soft);
}
.xx-announcement-tag {
flex-shrink: 0;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
height: fit-content;
}
.xx-announcement-tag--update {
color: var(--primary-color);
background: var(--primary-soft);
}
.xx-announcement-tag--notice {
color: var(--warning-color);
background: var(--warning-soft);
}
.xx-announcement-tag--activity {
color: var(--success-color);
background: var(--success-soft);
}
.xx-announcement-content {
flex: 1;
min-width: 0;
}
.xx-announcement-content h4 {
margin: 0 0 4px;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
line-height: 1.4;
}
.xx-announcement-content time {
font-size: 11px;
color: var(--text-secondary);
}
/* ============================================================
存储用量条
============================================================ */
.xx-storage-bar {
margin-top: 12px;
}
.xx-storage-bar-track {
height: 8px;
background: var(--bg-secondary, #e2e8f0);
border-radius: 4px;
overflow: hidden;
}
.xx-storage-bar-fill {
height: 100%;
border-radius: 4px;
background: var(--gradient-primary);
transition: width 0.6s ease;
}
.xx-storage-bar-label {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--text-secondary);
margin-top: 4px;
}
/* ============================================================
响应式
============================================================ */
@media (max-width: 1200px) {
.xx-dashboard-main {
grid-template-columns: 1fr;
}
.xx-kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
.xx-quick-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.xx-dashboard-page {
padding: var(--space-md);
}
.xx-kpi-grid {
grid-template-columns: 1fr;
}
.xx-quick-grid {
grid-template-columns: 1fr;
}
.xx-task-item {
grid-template-columns: 1fr;
gap: 8px;
}
.xx-task-time {
text-align: left;
}
.xx-chart-bars {
height: 120px;
}
}