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
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user