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

26 KiB
Raw Permalink Blame History

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 设计规范与组件实现。