26 KiB
26 KiB
Personal Blog - Admin Dashboard UI 设计与实现文档
设计风格: Modern Admin Dashboard
版本: v2.0
更新日期: 2026-04-19
1. 设计概述
1.1 设计理念
Admin Dashboard 采用现代化的 SaaS 管理后台设计,强调:
- 专业感: 深色侧边栏 + 浅色内容区的高对比度设计
- 效率优先: 信息密度适中,关键数据一目了然
- 视觉层次: 清晰的视觉层级引导用户操作
- 一致性: 所有组件遵循统一的设计语言
1.2 与访客端的区别
| 维度 | Admin Dashboard | 访客端 (Neobrutalism) |
|---|---|---|
| 用户 | 管理员 | 普通访客 |
| 目标 | 高效管理内容 | 阅读体验 |
| 风格 | 专业、现代 | 个性、艺术 |
| 色调 | 深墨绿 + 翠绿 | 奶油色 + 粉红 |
2. 色彩系统
2.1 CSS 变量定义
:root {
/* 侧边栏 */
--admin-sidebar-bg: #0F172A;
--admin-sidebar-hover: #1E293B;
--admin-sidebar-active: #334155;
--admin-sidebar-text: #94A3B8;
--admin-sidebar-text-active: #FFFFFF;
/* 主内容区 */
--admin-bg: #F8FAFC;
--admin-card: #FFFFFF;
--admin-border: #E2E8F0;
/* 主色调 - 翠绿 */
--admin-primary: #10B981;
--admin-primary-hover: #059669;
--admin-primary-light: #D1FAE5;
--admin-primary-subtle: #ECFDF5;
/* 状态色 */
--admin-success: #10B981;
--admin-warning: #F59E0B;
--admin-danger: #EF4444;
--admin-info: #3B82F6;
/* 文字 */
--admin-text-primary: #0F172A;
--admin-text-secondary: #64748B;
--admin-text-muted: #94A3B8;
--admin-text-inverse: #FFFFFF;
/* 阴影 */
--admin-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--admin-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--admin-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--admin-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
/* 圆角 */
--admin-radius-sm: 6px;
--admin-radius: 8px;
--admin-radius-lg: 12px;
--admin-radius-xl: 16px;
}
2.2 Tailwind 配置扩展
// tailwind.config.js
theme: {
extend: {
colors: {
admin: {
sidebar: {
DEFAULT: '#0F172A',
hover: '#1E293B',
active: '#334155',
text: '#94A3B8',
'text-active': '#FFFFFF',
},
bg: '#F8FAFC',
card: '#FFFFFF',
border: '#E2E8F0',
primary: {
DEFAULT: '#10B981',
hover: '#059669',
light: '#D1FAE5',
subtle: '#ECFDF5',
},
text: {
primary: '#0F172A',
secondary: '#64748B',
muted: '#94A3B8',
}
}
},
boxShadow: {
'admin': '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
'admin-md': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'admin-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
}
}
}
3. 布局架构
3.1 整体结构
┌─────────────────────────────────────────────────────────────┐
│ ┌────────────┐ ┌─────────────────────────────────────────┐ │
│ │ │ │ Header: Logo + Search + Actions │ │
│ │ Sidebar │ ├─────────────────────────────────────────┤ │
│ │ (240px) │ │ │ │
│ │ │ │ Breadcrumbs (可选) │ │
│ │ 🏠 Dashboard│ │ │ │
│ │ 📝 Posts │ │ ┌─────────────────────────────────┐ │ │
│ │ 🏷️ Tags │ │ │ Page Title + Actions │ │ │
│ │ 📁 Categories│ │ └─────────────────────────────────┘ │ │
│ │ 🖼️ Images │ │ │ │
│ │ 💬 Comments│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ ⚙️ Settings│ │ │Stat │ │Stat │ │Stat │ │Stat │ │ │
│ │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ │ ───────────│ │ │ │
│ │ 🔗 External│ │ ┌─────────────────────────────────┐ │ │
│ │ 📊 Analytics│ │ │ Main Content │ │ │
│ │ 🛒 Shop │ │ │ │ │ │
│ └────────────┘ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.2 布局组件
// AdminLayout.jsx
const AdminLayout = ({ children }) => {
return (
<div className="min-h-screen bg-admin-bg flex">
{/* 侧边栏 */}
<Sidebar className="w-60 bg-admin-sidebar fixed h-full" />
{/* 主内容区 */}
<main className="flex-1 ml-60 min-h-screen">
<Header className="sticky top-0 z-30 bg-white border-b border-admin-border" />
<div className="p-6">
{children}
</div>
</main>
</div>
)
}
4. 组件实现规范
4.1 侧边栏 (Sidebar)
const Sidebar = () => {
const menuGroups = [
{
title: '内容管理',
items: [
{ icon: LayoutDashboard, label: 'Dashboard', path: '/admin' },
{ icon: FileText, label: '文章管理', path: '/admin/posts' },
{ icon: FolderOpen, label: '分类管理', path: '/admin/categories' },
{ icon: Tags, label: '标签管理', path: '/admin/tags' },
{ icon: Image, label: '图片管理', path: '/admin/images' },
{ icon: MessageSquare, label: '评论管理', path: '/admin/comments' },
]
},
{
title: '系统设置',
items: [
{ icon: Settings, label: '系统设置', path: '/admin/settings' },
]
}
]
return (
<aside className="w-60 bg-admin-sidebar min-h-screen flex flex-col">
{/* Logo */}
<div className="h-16 flex items-center px-6 border-b border-gray-800">
<span className="text-xl font-bold text-white">Blog Admin</span>
</div>
{/* Menu */}
<nav className="flex-1 py-4 px-3 space-y-6 overflow-y-auto">
{menuGroups.map((group, idx) => (
<div key={idx}>
<h3 className="px-3 text-xs font-semibold text-admin-sidebar-text uppercase tracking-wider mb-2">
{group.title}
</h3>
<ul className="space-y-1">
{group.items.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => `
flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
transition-colors duration-200
${isActive
? 'bg-admin-sidebar-active text-white'
: 'text-admin-sidebar-text hover:bg-admin-sidebar-hover hover:text-white'
}
`}
>
<item.icon className="w-5 h-5" />
{item.label}
</NavLink>
))}
</ul>
</div>
))}
</nav>
{/* User */}
<div className="p-4 border-t border-gray-800">
<div className="flex items-center gap-3 px-3 py-2 rounded-lg bg-admin-sidebar-hover">
<div className="w-8 h-8 rounded-full bg-admin-primary flex items-center justify-center text-white font-medium">
A
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">Admin</p>
<p className="text-xs text-admin-sidebar-text truncate">admin@blog.com</p>
</div>
<LogOut className="w-4 h-4 text-admin-sidebar-text hover:text-white cursor-pointer" />
</div>
</div>
</aside>
)
}
4.2 头部 (Header)
const Header = () => {
return (
<header className="h-16 bg-white border-b border-admin-border flex items-center justify-between px-6">
{/* Search */}
<div className="flex-1 max-w-md">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-admin-text-muted" />
<input
type="text"
placeholder="搜索文章、评论..."
className="w-full pl-10 pr-4 py-2 bg-gray-50 border border-transparent rounded-lg text-sm
placeholder:text-admin-text-muted
focus:bg-white focus:border-admin-primary/30 focus:ring-2 focus:ring-admin-primary/20
transition-all duration-200"
/>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-3">
<button className="relative p-2 text-admin-text-secondary hover:text-admin-text-primary hover:bg-gray-100 rounded-lg transition-colors">
<Bell className="w-5 h-5" />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-admin-danger rounded-full"></span>
</button>
<button className="p-2 text-admin-text-secondary hover:text-admin-text-primary hover:bg-gray-100 rounded-lg transition-colors">
<HelpCircle className="w-5 h-5" />
</button>
<div className="h-6 w-px bg-admin-border mx-1"></div>
<a href="/" target="_blank" className="flex items-center gap-2 px-3 py-1.5 text-sm text-admin-primary hover:bg-admin-primary-subtle rounded-lg transition-colors">
<ExternalLink className="w-4 h-4" />
查看网站
</a>
</div>
</header>
)
}
4.3 统计卡片 (StatsCard)
const StatsCard = ({ title, value, change, changeType, icon: Icon, color = 'primary' }) => {
const colorMap = {
primary: 'bg-admin-primary-light text-admin-primary',
success: 'bg-green-100 text-green-600',
warning: 'bg-amber-100 text-amber-600',
danger: 'bg-red-100 text-red-600',
info: 'bg-blue-100 text-blue-600',
}
return (
<div className="bg-white rounded-xl p-6 border border-admin-border shadow-admin hover:shadow-admin-md transition-shadow duration-200">
<div className="flex items-start justify-between">
<div>
<p className="text-sm font-medium text-admin-text-secondary">{title}</p>
<p className="text-2xl font-bold text-admin-text-primary mt-2">{value}</p>
{change && (
<div className="flex items-center mt-2 text-sm">
<span className={`flex items-center font-medium ${
changeType === 'up' ? 'text-admin-success' : 'text-admin-danger'
}`}>
{changeType === 'up' ? <TrendingUp className="w-4 h-4 mr-1" /> : <TrendingDown className="w-4 h-4 mr-1" />}
{change}
</span>
<span className="text-admin-text-muted ml-2">较上月</span>
</div>
)}
</div>
<div className={`p-3 rounded-xl ${colorMap[color]}`}>
<Icon className="w-6 h-6" />
</div>
</div>
</div>
)
}
4.4 数据表格 (DataTable)
const DataTable = ({ columns, data, onEdit, onDelete }) => {
return (
<div className="bg-white rounded-xl border border-admin-border shadow-admin overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-admin-border">
<tr>
{columns.map((col) => (
<th key={col.key} className="px-6 py-4 text-left text-xs font-semibold text-admin-text-secondary uppercase tracking-wider">
{col.title}
</th>
))}
<th className="px-6 py-4 text-right text-xs font-semibold text-admin-text-secondary uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody className="divide-y divide-admin-border">
{data.map((row, idx) => (
<tr key={idx} className="hover:bg-gray-50 transition-colors">
{columns.map((col) => (
<td key={col.key} className="px-6 py-4">
{col.render ? col.render(row[col.key], row) : row[col.key]}
</td>
))}
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => onEdit(row)}
className="p-2 text-admin-text-muted hover:text-admin-primary hover:bg-admin-primary-subtle rounded-lg transition-colors"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => onDelete(row)}
className="p-2 text-admin-text-muted hover:text-admin-danger hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
4.5 按钮组件 (Button)
const Button = ({
children,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
...props
}) => {
const variants = {
primary: 'bg-admin-primary text-white hover:bg-admin-primary-hover focus:ring-admin-primary',
secondary: 'bg-white text-admin-text-primary border border-admin-border hover:bg-gray-50 focus:ring-gray-200',
ghost: 'bg-transparent text-admin-text-secondary hover:bg-gray-100 hover:text-admin-text-primary',
danger: 'bg-admin-danger text-white hover:bg-red-600 focus:ring-red-500',
}
const sizes = {
sm: 'px-3 py-1.5 text-xs rounded-md',
md: 'px-4 py-2 text-sm rounded-lg',
lg: 'px-6 py-2.5 text-base rounded-lg',
}
return (
<button
disabled={disabled || loading}
className={`
inline-flex items-center justify-center gap-2 font-medium
transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
${variants[variant]}
${sizes[size]}
`}
{...props}
>
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
{children}
</button>
)
}
4.6 表单组件
// Input
const Input = ({ label, error, ...props }) => {
return (
<div className="space-y-1.5">
{label && <label className="block text-sm font-medium text-admin-text-secondary">{label}</label>}
<input
className={`
w-full px-4 py-2.5 rounded-lg border bg-white text-admin-text-primary
placeholder:text-admin-text-muted
focus:outline-none focus:ring-2 focus:ring-admin-primary/20 focus:border-admin-primary
transition-all duration-200
${error ? 'border-admin-danger focus:border-admin-danger focus:ring-red-200' : 'border-admin-border'}
`}
{...props}
/>
{error && <p className="text-sm text-admin-danger">{error}</p>}
</div>
)
}
// Select
const Select = ({ label, options, ...props }) => {
return (
<div className="space-y-1.5">
{label && <label className="block text-sm font-medium text-admin-text-secondary">{label}</label>}
<div className="relative">
<select
className="w-full px-4 py-2.5 rounded-lg border border-admin-border bg-white text-admin-text-primary
appearance-none focus:outline-none focus:ring-2 focus:ring-admin-primary/20 focus:border-admin-primary
transition-all duration-200"
{...props}
>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-admin-text-muted pointer-events-none" />
</div>
</div>
)
}
// Textarea
const Textarea = ({ label, error, rows = 4, ...props }) => {
return (
<div className="space-y-1.5">
{label && <label className="block text-sm font-medium text-admin-text-secondary">{label}</label>}
<textarea
rows={rows}
className={`
w-full px-4 py-2.5 rounded-lg border bg-white text-admin-text-primary
placeholder:text-admin-text-muted resize-vertical
focus:outline-none focus:ring-2 focus:ring-admin-primary/20 focus:border-admin-primary
transition-all duration-200
${error ? 'border-admin-danger focus:border-admin-danger focus:ring-red-200' : 'border-admin-border'}
`}
{...props}
/>
{error && <p className="text-sm text-admin-danger">{error}</p>}
</div>
)
}
4.7 卡片组件 (Card)
const Card = ({ children, className = '' }) => {
return (
<div className={`bg-white rounded-xl border border-admin-border shadow-admin ${className}`}>
{children}
</div>
)
}
const CardHeader = ({ title, description, action }) => {
return (
<div className="px-6 py-4 border-b border-admin-border flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-admin-text-primary">{title}</h3>
{description && <p className="text-sm text-admin-text-secondary mt-1">{description}</p>}
</div>
{action && <div>{action}</div>}
</div>
)
}
const CardContent = ({ children, className = '' }) => {
return <div className={`px-6 py-4 ${className}`}>{children}</div>
}
const CardFooter = ({ children, className = '' }) => {
return (
<div className={`px-6 py-4 border-t border-admin-border bg-gray-50 rounded-b-xl ${className}`}>
{children}
</div>
)
}
4.8 模态框 (Modal)
const Modal = ({ isOpen, onClose, title, children, footer }) => {
if (!isOpen) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white rounded-xl shadow-admin-lg w-full max-w-lg mx-4 animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center justify-between px-6 py-4 border-b border-admin-border">
<h3 className="text-lg font-semibold text-admin-text-primary">{title}</h3>
<button onClick={onClose} className="p-1 text-admin-text-muted hover:text-admin-text-primary rounded-lg hover:bg-gray-100 transition-colors">
<X className="w-5 h-5" />
</button>
</div>
<div className="px-6 py-4">
{children}
</div>
{footer && (
<div className="px-6 py-4 border-t border-admin-border bg-gray-50 rounded-b-xl flex justify-end gap-3">
{footer}
</div>
)}
</div>
</div>
)
}
4.9 Toast 通知
const Toast = ({ message, type = 'success', onClose }) => {
const icons = {
success: CheckCircle,
error: XCircle,
warning: AlertTriangle,
info: Info,
}
const colors = {
success: 'bg-green-50 border-green-200 text-green-800',
error: 'bg-red-50 border-red-200 text-red-800',
warning: 'bg-amber-50 border-amber-200 text-amber-800',
info: 'bg-blue-50 border-blue-200 text-blue-800',
}
const Icon = icons[type]
useEffect(() => {
const timer = setTimeout(onClose, 3000)
return () => clearTimeout(timer)
}, [onClose])
return (
<div className={`fixed top-4 right-4 z-50 flex items-center gap-3 px-4 py-3 rounded-lg border shadow-admin-lg animate-in slide-in-from-right ${colors[type]}`}>
<Icon className="w-5 h-5" />
<span className="text-sm font-medium">{message}</span>
<button onClick={onClose} className="ml-2 hover:opacity-70">
<X className="w-4 h-4" />
</button>
</div>
)
}
4.10 分页组件 (Pagination)
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
return (
<div className="flex items-center justify-between px-4 py-3 bg-white border-t border-admin-border">
<div className="text-sm text-admin-text-secondary">
第 <span className="font-medium text-admin-text-primary">{currentPage}</span> 页,共 <span className="font-medium">{totalPages}</span> 页
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="p-2 rounded-lg border border-admin-border text-admin-text-secondary hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft className="w-4 h-4" />
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const page = i + 1
return (
<button
key={page}
onClick={() => onPageChange(page)}
className={`w-9 h-9 rounded-lg text-sm font-medium transition-colors ${
currentPage === page
? 'bg-admin-primary text-white'
: 'text-admin-text-secondary hover:bg-gray-50'
}`}
>
{page}
</button>
)
})}
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="p-2 rounded-lg border border-admin-border text-admin-text-secondary hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)
}
5. 页面实现
5.1 Dashboard 页面
const Dashboard = () => {
const stats = [
{ title: '总文章数', value: '128', change: '12%', changeType: 'up', icon: FileText },
{ title: '本月发布', value: '24', change: '8%', changeType: 'up', icon: TrendingUp },
{ title: '总浏览量', value: '45.2K', change: '23%', changeType: 'up', icon: Eye },
{ title: '待审评论', value: '18', change: '5%', changeType: 'down', icon: MessageSquare },
]
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-admin-text-primary">Dashboard</h1>
<p className="text-admin-text-secondary mt-1">欢迎回来,今天也是创作的好日子!</p>
</div>
<Button>
<Plus className="w-4 h-4" />
写文章
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat) => (
<StatsCard key={stat.title} {...stat} />
))}
</div>
{/* Recent Posts */}
<Card>
<CardHeader
title="最近文章"
description="最近发布的 5 篇文章"
action={<Button variant="ghost" size="sm">查看全部 →</Button>}
/>
<CardContent>
<DataTable
columns={[
{ key: 'title', title: '标题' },
{ key: 'category', title: '分类' },
{ key: 'views', title: '浏览量' },
{ key: 'date', title: '发布时间' },
]}
data={recentPosts}
/>
</CardContent>
</Card>
</div>
)
}
6. 文件结构
frontend/src/
├── components/
│ └── admin/
│ ├── AdminLayout.jsx # 后台布局
│ ├── Sidebar.jsx # 侧边栏
│ ├── Header.jsx # 顶部导航
│ ├── StatsCard.jsx # 统计卡片
│ ├── DataTable.jsx # 数据表格
│ ├── Button.jsx # 按钮
│ ├── Card.jsx # 卡片组件
│ ├── Input.jsx # 输入框
│ ├── Select.jsx # 下拉选择
│ ├── Modal.jsx # 模态框
│ ├── Toast.jsx # 通知提示
│ └── Pagination.jsx # 分页
├── pages/
│ └── admin/
│ ├── Dashboard.jsx
│ ├── Posts.jsx
│ ├── PostEdit.jsx
│ ├── Categories.jsx
│ ├── Tags.jsx
│ ├── Images.jsx
│ ├── Comments.jsx
│ └── Settings.jsx
├── styles/
│ └── admin.css # Admin 主题样式
└── hooks/
└── useToast.js # Toast 状态管理
7. 更新日志
| 日期 | 版本 | 变更内容 |
|---|---|---|
| 2026-04-19 | v2.0 | 初始版本,完整的 Admin Dashboard 设计规范 |
本文档定义了个人博客后台管理的完整 UI 设计规范与组件实现。