Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

764 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 变量定义
```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 配置扩展
```javascript
// 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 布局组件
```jsx
// 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)
```jsx
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)
```jsx
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)
```jsx
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)
```jsx
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)
```jsx
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 表单组件
```jsx
// 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)
```jsx
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)
```jsx
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 通知
```jsx
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)
```jsx
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 页面
```jsx
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 设计规范与组件实现。*