From 89aa8e22bc37985c59d2a979a0849e4be211a625 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 17 Sep 2026 22:17:20 +0800 Subject: [PATCH 1/3] feat: add ImageLightbox component for image viewing with zoom and drag functionality feat: implement MusicPlayer component for audio playback with playlist support feat: create AnnouncementModal component for displaying announcements with dismiss functionality feat: develop Modal component for general use with customizable title and footer feat: introduce UploadProgress component for tracking file upload status with automatic dismissal feat: add useUploadTasks hook for managing upload tasks and their states --- demo/uiux-spec.md | 2025 +++++++++++++++++ frontend/src/app/(admin)/admin/layout.tsx | 360 +-- frontend/src/app/(admin)/admin/page.tsx | 455 ++-- .../src/app/(auth)/forgot-password/page.tsx | 17 + frontend/src/app/(auth)/layout.tsx | 24 + frontend/src/app/(auth)/login/page.tsx | 16 + frontend/src/app/(auth)/register/page.tsx | 13 + frontend/src/app/(blog)/ArticleList.tsx | 414 ++-- .../src/app/(blog)/articles/[id]/page.tsx | 430 ++-- frontend/src/app/(blog)/layout.tsx | 12 +- frontend/src/app/(blog)/page.tsx | 57 +- frontend/src/app/globals.css | 607 +++-- frontend/src/app/not-found.tsx | 26 + frontend/src/components/blog/ArticleCard.tsx | 326 +++ .../src/components/blog/CommentSection.tsx | 264 +++ frontend/src/components/blog/Hero.tsx | 124 +- .../src/components/blog/SearchPopover.tsx | 171 ++ frontend/src/components/blog/SidebarCard.tsx | 88 + frontend/src/components/blog/Sidebars.tsx | 202 ++ .../src/components/blog/TableOfContents.tsx | 121 +- .../components/dashboard/DashboardHeader.tsx | 144 ++ frontend/src/components/dashboard/Sidebar.tsx | 275 +++ frontend/src/components/forms/AuthCard.tsx | 415 ++++ frontend/src/components/layout/Footer.tsx | 165 +- frontend/src/components/layout/Header.tsx | 393 ++++ .../src/components/lightbox/ImageLightbox.tsx | 198 ++ .../src/components/player/MusicPlayer.tsx | 241 ++ .../src/components/ui/AnnouncementModal.tsx | 125 + frontend/src/components/ui/Modal.tsx | 145 ++ .../src/components/upload/UploadProgress.tsx | 174 ++ frontend/src/lib/utils.ts | 82 +- 31 files changed, 6666 insertions(+), 1443 deletions(-) create mode 100644 demo/uiux-spec.md create mode 100644 frontend/src/app/(auth)/forgot-password/page.tsx create mode 100644 frontend/src/app/(auth)/layout.tsx create mode 100644 frontend/src/app/(auth)/login/page.tsx create mode 100644 frontend/src/app/(auth)/register/page.tsx create mode 100644 frontend/src/app/not-found.tsx create mode 100644 frontend/src/components/blog/ArticleCard.tsx create mode 100644 frontend/src/components/blog/CommentSection.tsx create mode 100644 frontend/src/components/blog/SearchPopover.tsx create mode 100644 frontend/src/components/blog/SidebarCard.tsx create mode 100644 frontend/src/components/blog/Sidebars.tsx create mode 100644 frontend/src/components/dashboard/DashboardHeader.tsx create mode 100644 frontend/src/components/dashboard/Sidebar.tsx create mode 100644 frontend/src/components/forms/AuthCard.tsx create mode 100644 frontend/src/components/layout/Header.tsx create mode 100644 frontend/src/components/lightbox/ImageLightbox.tsx create mode 100644 frontend/src/components/player/MusicPlayer.tsx create mode 100644 frontend/src/components/ui/AnnouncementModal.tsx create mode 100644 frontend/src/components/ui/Modal.tsx create mode 100644 frontend/src/components/upload/UploadProgress.tsx diff --git a/demo/uiux-spec.md b/demo/uiux-spec.md new file mode 100644 index 0000000..7c83126 --- /dev/null +++ b/demo/uiux-spec.md @@ -0,0 +1,2025 @@ +# 博客前端 UI/UX 设计规范(详细版) + +> 基于 **次元栈 · Dim Stack** 项目提炼的完整 UI/UX 规范,供后续博客前端开发参考。 +> 本规范**仅聚焦视觉与交互**(不涉及后端实现、状态管理、具体业务逻辑)。 +> 内容覆盖:设计系统、前台门户、后台 Dashboard、音乐播放器、图片灯箱、Markdown 编辑器、表单、加载/错误/空态等全场景。 + +--- + +## 目录 + +- [0. 设计哲学速览](#0-设计哲学速览) +- [1. 设计系统(Design System)](#1-设计系统design-system) +- [2. 全局布局](#2-全局布局) +- [3. 路由结构](#3-路由结构) +- [4. 前台门户组件](#4-前台门户组件) +- [5. 后台 Dashboard 模块](#5-后台-dashboard-模块) +- [6. 全局元素](#6-全局元素) +- [7. 暗色模式](#7-暗色模式) +- [8. 响应式断点](#8-响应式断点) +- [9. 微交互与动效汇总](#9-微交互与动效汇总) +- [10. 可访问性](#10-可访问性) +- [11. 错误/空/加载状态规范](#11-错误空加载状态规范) +- [12. 复用资产清单](#12-复用资产清单) +- [附录 A:图标策略](#附录-a图标策略) +- [附录 B:常用 Tailwind 类速查](#附录-b常用-tailwind-类速查) +- [附录 C:键盘快捷键汇总](#附录-c键盘快捷键汇总) + +--- + +## 0. 设计哲学速览 + +| 关键词 | 描述 | +| --- | --- | +| **清爽留白** | 大量灰色背景 + 白色卡片阴影,弱化分隔线 | +| **蓝灰主色** | 主操作蓝(blue-500/blue-600),文本以中性灰阶为主 | +| **内容优先** | 卡片化布局,弱化边框、突出排版与图片 | +| **柔和过渡** | 全局以 200–300ms ease 过渡,避免突兀动画 | +| **响应式优先** | 桌面两栏/移动单栏,所有组件具备 Mobile-First 行为 | +| **玻璃态点缀** | 弹层、播放器等使用 `backdrop-blur` + 半透明白底 | +| **状态友好** | 所有异步操作都有 loading/error/empty 三态视觉 | + +--- + +## 1. 设计系统(Design System) + +### 1.1 颜色 Token + +#### 浅色模式(默认) + +| 用途 | 类名 | HEX | +| --- | --- | --- | +| 页面背景 | `bg-gray-50` | `#F9FAFB` | +| 卡片/容器 | `bg-white` | `#FFFFFF` | +| 卡片次级背景 | `bg-gray-50 / bg-gray-100` | `#F9FAFB / #F3F4F6` | +| 一级文字(标题) | `text-gray-900` | `#111827` | +| 二级文字(正文) | `text-gray-700` | `#374151` | +| 弱文字(描述) | `text-gray-600` | `#4B5563` | +| 提示文字 | `text-gray-500` | `#6B7280` | +| 占位/disabled | `text-gray-400` | `#9CA3AF` | +| **主色(强调)** | `bg-blue-500` | `#3B82F6` | +| 主色悬停 | `hover:bg-blue-600` | `#2563EB` | +| 主色按下 | `active:bg-blue-700` | `#1D4ED8` | +| 主色文字 | `text-blue-600` | `#2563EB` | +| 主色悬停文字 | `hover:text-blue-700` | `#1D4ED8` | +| 选中态背景 | `bg-blue-100` | `#DBEAFE` | +| 选中态文字 | `text-blue-800` | `#1E40AF` | +| 主色 50 背景 | `bg-blue-50` | `#EFF6FF` | +| 主色 400(站点名) | `text-blue-400` | `#60A5FA` | +| 成功 | `bg-green-100 / text-green-800` | `#D1FAE5 / #065F46` | +| 警告 | `bg-yellow-50 / text-yellow-800` | `#FFFBEB / #92400E` | +| 危险 | `bg-red-500 / bg-red-50 / text-red-800` | `#EF4444 / #FEF2F2 / #991B1B` | +| 信息 | `bg-blue-50` | `#EFF6FF` | +| 卡片边框 | `border-gray-200` | `#E5E7EB` | +| 输入边框 | `border-gray-300` | `#D1D5DB` | +| 输入框背景 | `bg-white` | `#FFFFFF` | +| 浅灰 hover | `hover:bg-gray-50 / 100` | `#F9FAFB / #F3F4F6` | + +#### 暗色模式(`.dark` 作用域) + +| 用途 | HEX | +| --- | --- | +| 页面背景 | `#111827` | +| 卡片背景 | `#1F2937` | +| 输入框背景 | `#374151` | +| 主色悬停 | `#60A5FA` | +| 主色文字 | `#93C5FD` | +| 主色按下 | `#1D4ED8` | +| 选中态背景 | `#1E3A8A` | +| 选中态文字 | `#93C5FD` | +| 主色 50 背景 | `rgba(59,130,246,0.12)` | +| 一级文字 | `#F9FAFB` | +| 二级文字 | `#E5E7EB` | +| 弱文字 | `#D1D5DB` | +| 提示文字 | `#9CA3AF` | +| 边框 | `#374151 / #4B5563` | +| 危险背景 | `rgba(239,68,68,0.12)`,描边 `rgba(239,68,68,0.3)` | +| 警告背景 | `rgba(245,158,11,0.12)` | +| 信息背景 | `rgba(59,130,246,0.12)` | +| Toast 错误 | `rgba(127,29,29,0.92)` + 红色描边 | +| Toast 警告 | `rgba(120,53,15,0.92)` + 黄色描边 | +| Toast 信息 | `rgba(30,58,138,0.92)` + 蓝色描边 | +| 阴影透明度 | 加重到 `0.5` | + +### 1.2 字体 + +| 用途 | 字号/字重 | 备注 | +| --- | --- | --- | +| 站点名称/仪表盘标题 | `text-xl sm:text-2xl font-bold` | 颜色 `text-blue-400` / `text-blue-600` | +| 页面大标题 | `text-2xl font-bold` | 卡片内标题 `filter-title` | +| 文章卡片标题 | `text-lg font-bold` | `line-clamp-1` | +| 文章详情 H1 | `text-4xl md:text-5xl font-bold` | 白字,封面图上 | +| 侧栏标题 | `text-lg font-bold` | `text-gray-900` | +| 表单标题 | `text-3xl font-extrabold` | 登录页主标题 | +| 区块标题(卡片) | `text-xl font-semibold` | 后台 DataTable 等 | +| 子区块标题 | `text-lg font-semibold` | 通知标题等 | +| 正文摘要 | `text-sm leading-relaxed` | `text-gray-500` | +| Meta 信息 | `text-xs` | `text-gray-400 / 500` | +| 标签徽章 | `text-xs` | `text-gray-800` | +| 按钮 | `text-sm font-medium` | 主按钮白字 | +| 徽章数字 | `text-xs font-medium` | 数量徽章 | +| 提示文字 | `text-xs` | 辅助说明 | + +### 1.3 间距/圆角/阴影/边框 + +| 元素 | 规范 | +| --- | --- | +| 卡片圆角 | `rounded-lg`(8px) / 播放器等用 `rounded-xl`(12px) | +| 输入框/按钮 | `rounded-md`(6px) | +| 标签徽章 | `rounded`(4px) / 标签 `rounded-full` | +| 头像/圆形按钮 | `rounded-full` | +| 卡片阴影 | `shadow-sm / shadow-md` / `hover:shadow-lg` | +| 弹窗阴影 | `shadow-xl` | +| 卡片间距 | `gap-4`(卡片网格) / `gap-6`(主区) | +| 主区容器 | `px-4 py-8` 或 `p-6` | +| 卡片内边距 | `p-4 / p-5 / p-6` | +| 主区域最大宽度 | `max-w-[1240px]`(项目约定) | +| Dashboard 主区 | `p-6` | +| 表单输入框 | `py-2 px-3` | +| 区块分隔 | `border-b border-gray-200` / `divide-y divide-gray-200` | + +### 1.4 动效时长与曲线 + +| 场景 | 时长 | 曲线 | +| --- | --- | --- | +| 链接/按钮 hover | `150ms` | `ease` | +| 卡片 hover 阴影 | `200ms` | `ease` | +| 下拉菜单出现 | `200ms` | `ease` | +| 移动菜单展开 | `300ms` | `ease-in-out` | +| 侧栏滑入 | `300ms` | `ease-in-out` | +| 主题切换涟漪 | `500ms` | `ease-out` | +| 上传进度面板滑入 | `300ms` | `cubic-bezier(0.16, 1, 0.3, 1)` | +| Toast 出现/消失 | `300ms` | `ease` | +| 弹窗淡入 | `250ms` | `ease-in-out` | +| 图片缩放(hover) | `300ms` | `ease` | +| 标签页切换 | `200ms` | `ease` | +| 输入框聚焦 | `200ms` | `ease` | + +### 1.5 透明度与玻璃态 + +| 场景 | 实现 | +| --- | --- | +| 顶部搜索弹窗 | `bg-white/90 backdrop-blur-md` | +| 音乐播放器 | `rgba(255,255,255,0.85) backdrop-blur(20px)` | +| 公告/弹窗遮罩 | `backdrop-blur-sm bg-black/20 ~ bg-opacity-30` | +| Lightbox | `bg-black/30 backdrop-blur-lg z-[9999]` | +| Toast | `backdrop-filter: blur(6px)` | +| 暗色模式遮罩 | `rgba(0,0,0,0.55)` | + +### 1.6 渐变色与背景 + +| 用途 | 实现 | +| --- | --- | +| 文章封面渐变 | `bg-gradient-to-b from-black/30 to-black/70` | +| 音乐进度条 | `linear-gradient(90deg, #3b82f6, #60a5fa)` | +| 顶栏 gradient | 仪表盘标题区背景 `linear-gradient(to right, #1f2937, #111827)` | +| 暗色模式封面 | `rgba(0,0,0,0.55)` + 半透蒙层 | + +--- + +## 2. 全局布局 + +### 2.1 页面骨架(前台) + +``` +┌─────────────────────────────────────────────┐ +│ Header(fixed,64px 高) │ ← z-50 +├─────────────────────────────────────────────┤ +│
│ +│ Hero 区(首页独有,可选) │ +│
│ +│ ┌─────────────────────┐ ┌────────────┐ │ +│ │ 内容主区(flex-1) │ │ 侧栏 310 │ │ +│ │ │ │ top-28 │ │ +│ │ │ │ sticky │ +│ └─────────────────────┘ └────────────┘ │ +│
│ +│
│ +├─────────────────────────────────────────────┤ +│ Footer │ +└─────────────────────────────────────────────┘ +``` + +- **Header 固定**:`fixed top-0 left-0 right-0 z-50`,高度 `64px`。 +- **主区域**:`
` 占满剩余高度。 +- **侧栏**:宽度 `310px`,`sticky top-28`。 +- **移动端**:折叠为单列,侧栏置于主区之下。 + +### 2.2 页面骨架(后台 Dashboard) + +``` +┌─────────────────────────────────────────────┐ +│ DashboardHeader(h-16,shadow-sm) │ +├────────┬────────────────────────────────────┤ +│Sidebar │
│ +│(w-64) │ 统计卡片(4 列网格) │ +│ │ 快捷操作(3 列网格) │ +│ 菜单 │ 通知区(标签 + 列表 + 无限滚动) │ +│ (sticky)│ … │ +│ │ │ +├────────┴────────────────────────────────────┤ +│ 底部用户信息(Sidebar 内 mt-auto) │ +└─────────────────────────────────────────────┘ +``` + +- **后台容器**:`min-h-screen bg-gray-50`,`flex h-screen`。 +- **侧栏**:固定/相对(`fixed md:relative`),可收起为 `w-16`。 +- **主区域**:`flex-1 flex flex-col overflow-hidden w-full md:w-auto`。 + +--- + +## 3. 路由结构 + +| 路径 | 页面 | 备注 | +| --- | --- | --- | +| `/` | 首页 | Hero + 列表 + 侧栏 | +| `/?category=xxx` | 分类筛选 | URL 同步 | +| `/tag/:tagName` | 标签筛选 | | +| `/article/:articleId` | 文章详情 | 三栏布局 | +| `/login` | 登录 | | +| `/register` | 注册 | | +| `/forgot-password` | 找回密码 | | +| `/dashboard` | 仪表盘首页 | | +| `/dashboard/articles` | 文章管理 | | +| `/dashboard/articles/new` | 新建文章 | | +| `/dashboard/articles/:id` | 编辑文章 | | +| `/dashboard/profile` | 个人资料 | | +| `/dashboard/comments` | 我的评论 | | +| `/dashboard/comments-review` | 评论审核 | | +| `/dashboard/articles-review` | 文章审核 | | +| `/dashboard/menus` | 菜单管理 | | +| `/dashboard/users` | 用户管理 | | +| `/dashboard/settings` | 站点设置 | | +| `/dashboard/tags` | 标签管理 | | +| `/dashboard/categories` | 分类管理 | | +| `/dashboard/themes` | 主题商店 | | +| `/dashboard/friend-links` | 友情链接管理 | | +| `/dashboard/update` | 更新管理 | | +| `/dashboard/custom-pages` | 自定义页面 | | +| `/dashboard/announcement` | 公告管理 | | +| `/dashboard/global-attachments` | 全局附件 | | +| `/dashboard/my-attachments` | 我的附件 | | +| `/dashboard/rbac-editor` | 角色权限管理 | | +| `/friend-links` | 友情链接页 | 前台 | +| `/custom-page/:alias` | 自定义 HTML 页 | 前台 | +| `*` | 404 | | + +--- + +## 4. 前台门户组件 + +### 4.1 Header 顶部导航 + +**结构**: + +``` +[站点名 blue-400 bold] [── 菜单居中 flex-1 ──] [🔍 搜索] [🌓 主题切换] [☰ 移动菜单] [👤 用户] +``` + +**容器**: +```jsx +
+
+
+``` + +**站点名**: +- `text-xl sm:text-2xl font-bold text-blue-400 truncate max-w-[200px] sm:max-w-none` +- 点击 `window.location.href = '/'` + +**菜单区**: +- `hidden md:flex flex-1 min-w-0 items-center justify-center mx-6` +- 菜单项:`text-gray-600 hover:text-blue-600 font-medium transition-colors duration-200` +- 间距:`space-x-8`(32px) + +**菜单溢出策略(重点)**: + +- 用「不可见测量副本」+ `ResizeObserver` 测量所有菜单项宽度。 +- 计算容器宽度能容纳的菜单项数 `visibleCount`。 +- 溢出项归入「更多 ▾」下拉: + +``` + +
+``` + +- 容器:测量 `
    ` 用 `aria-hidden="true"` 离屏,`absolute -left-[9999px] top-0`。 +- 「更多」按钮固定占位 `88px`。 + +**登录态菜单**: + +- 已登录:头像(`w-8 h-8 rounded-full`) + 用户名(`max-w-[120px] truncate`)+ 下拉。 +- 下拉:`absolute right-0 mt-2 w-48` + 阴影 + 边框。 +- 项:「进入控制台」「登出」(登出项加 `border-t border-gray-100` 区分)。 +- 未登录:胶囊按钮 `header-login-btn`: + ``` + rounded-full bg-gray-100 hover:bg-blue-50 + text-gray-600 hover:text-blue-600 + px-3 py-1.5 text-sm font-medium + inline-flex items-center gap-1.5 + transition-all duration-200 + ``` + +**移动端(< md 768px)**: + +- 汉堡按钮:`md:hidden`,右对齐,`p-2 rounded hover:bg-gray-100`。 +- 点击展开(旋转 90° 切换图标): + ``` +
    + ``` +- 菜单项:`block w-full text-left py-3 px-2 hover:bg-blue-50 rounded-lg mobile-menu-link`。 +- 登录按钮(移动):`w-full py-3 bg-blue-400 hover:bg-blue-500 text-white`。 +- 登出按钮:`text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg mobile-menu-link-logout`。 +- 背景遮罩:`fixed inset-0 bg-black/20 backdrop-blur-sm`,点击关闭。 + +**外部交互关闭**: +```js +useEffect(() => { + const handler = (e) => { + if (isMobileMenuOpen && !e.target.closest('header')) setIsMobileMenuOpen(false); + if (isUserMenuOpen && !e.target.closest('.user-menu-container')) setIsUserMenuOpen(false); + if (isMoreMenuOpen && !e.target.closest('.more-menu-container')) setIsMoreMenuOpen(false); + }; + document.addEventListener('click', handler); + return () => document.removeEventListener('click', handler); +}, [isMobileMenuOpen, isUserMenuOpen, isMoreMenuOpen]); +``` + +### 4.2 Hero 头图 + +**结构**: +```jsx +
    +
    + +
    +
    +
    +

    {title}

    + {subtitle &&

    {subtitle}

    } +
    +
    +
    +``` + +**规范**: + +- 全屏宽背景图,`bg-gray-900` 兜底。 +- 文字最大宽度 `max-w-2xl`,垂直内边距 `py-24 md:py-32`。 +- 加载态:`absolute inset-0 bg-gray-300 animate-pulse` + 内部骨架 `h-12 bg-gray-200 rounded w-3/4 mb-4 animate-pulse`。 +- 失败态:暗色背景 `bg-gray-800` + 「欢迎来到站点」标题 `text-4xl md:text-5xl font-bold text-white`。 +- 图片 `onError` 回退 `/image_error.svg`。 + +### 4.3 ArticleCard 文章卡片 + +#### 桌面版(`md` 及以上,3 列网格) + +``` +┌───────────────────────────┐ +│ [封面图 16:9 h-40] │ ← group-hover:scale-105 +├───────────────────────────┤ +│ 标题 (line-clamp-1) │ +│ 摘要 (line-clamp-2) │ +│ [分类 chip] [标签1] [标签2]│ +│ ──────────────────── │ +│ [头像] 作者 · 日期 │ +└───────────────────────────┘ +``` + +- 容器:`article-card bg-white rounded-lg shadow-md hover:shadow-lg border border-gray-200 cursor-pointer` +- 封面:`h-40 overflow-hidden`,图片 `rounded-t-lg group-hover:scale-105 transition-transform duration-300` +- 标题:`text-lg font-bold text-gray-900 line-clamp-1`,最大 30 字符截断 `safeTruncate(title, 30)` +- 摘要:`text-gray-500 text-sm line-clamp-2 leading-relaxed` +- 标签:`article-tag bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded hover:bg-gray-200 transition-colors shrink-0` +- 头像:`w-7 h-7 rounded-full object-cover` +- 整卡片可点击;标签/分类 `e.preventDefault() + e.stopPropagation()` 阻止冒泡。 + +#### 移动版(`forceMobile`,高度 `h-40` 横排) + +``` +┌──────────────────────────────────────────┐ +│ [缩略图 1/3] 标题 (line-clamp-1) │ +│ w-40 │ 摘要 (line-clamp-3) │ +│ h-40 │ [分类] [标签] [标签] │ +│ │ [头像] 作者 · 日期 │ +└──────────────────────────────────────────┘ +``` + +- 高度固定 `h-40`,缩略图占 1/3 宽度 + `m-2` 边距。 +- 标签行横向滚动:`flex items-center gap-1.5 overflow-x-auto tags-scroll`,隐藏滚动条。 +- 头像 `w-5 h-5`,标题 `text-sm`。 + +**暗色模式**: +- 卡片:`bg-gray-800 border-gray-700`;标题 `#F9FAFB`;摘要 `#D1D5DB`;分类 `#1E3A8A / #93C5FD`。 + +### 4.4 ArticleList 文章列表 + +**顶部工具栏**: +``` +┌──────────────────────────────────────────────────┐ +│ [筛选标题 text-2xl font-bold] [图片开关] [清除筛选] │ +└──────────────────────────────────────────────────┘ +``` + +- 标题:`text-2xl font-bold text-gray-900 filter-title` +- 图片开关(仅 `md` 及以上可见)—— 自定义 Switch: + ``` +
    + +
    +
    +
    + ``` +- 文字:`mr-3 text-sm font-medium text-gray-600 switch-label select-none` +- 清除筛选:`text-sm text-blue-600 hover:text-blue-800` —— 出现条件:`categoryName || tagName` + +**网格**: +``` +
    +``` + +**加载态**: +``` +
    + 加载中... +
    +``` +> `holdHeight` 在切换前记录旧高度,避免布局抖动。 + +**空态**:`text-center py-12 text-gray-500` + 「暂无文章」 + +**分页器**: +``` +[上一页] [1 / 5] [下一页] +``` +- 按钮:`px-4 py-2 mx-1 bg-gray-200 rounded disabled:opacity-50 pagination-button` +- 中间信息:`pagination-info`:`px-4 py-2 mx-1 pagination-info text-gray-600`,暗色 `bg-gray-700 border-gray-600` +- 切换不滚动到顶部(保留上下文)。 + +**Abort 策略**: +```js +const abortController = new AbortController(); +await apiClient.get(url, { signal: abortController.signal }); +// 切换条件时 abort 上一次 +``` + +### 4.5 侧栏三件套 + +#### 4.5.1 统一外壳 + +``` +┌──────────────────────────┐ +│ [图标] 标题 │ +│ ───────── │ +│ 列表/内容 │ +│ [▼ 渐隐遮罩] │ +└──────────────────────────┘ +``` + +- 容器:`bg-white rounded-lg shadow-md p-5 mb-6 border border-gray-200` +- 标题:`text-lg font-bold text-gray-900 mb-4` + +#### 4.5.2 CategorySidebar(文章分类) + +- 列表项:`flex justify-between items-center w-full text-left px-2 py-1 rounded` +- 选中态:`bg-blue-100 text-blue-800` +- 未选中:`text-gray-600 hover:bg-gray-50` +- 数量徽章:`bg-gray-100 text-gray-800 text-xs font-medium px-2 py-1 rounded-full` +- 滚动容器:`max-h-60 overflow-y-auto pr-2`,隐藏滚动条(`.hide-scrollbar`)。 + +**渐隐遮罩(细节)**: +```jsx +
    +
    +
      + ... +
    +
    +
    +``` +- 顶部渐变 `linear-gradient(to top, transparent, #ffffff)`,底部反之。 +- 监听 `scroll` + `ResizeObserver` 动态切换 opacity。 +- 暗色模式遮罩变为 `transparent → #1F2937`。 + +#### 4.5.3 TagSidebar(文章标签) + +- 布局:`flex flex-wrap gap-2 max-h-60 overflow-y-auto pr-2`。 +- 徽章:`px-3 py-1 text-sm rounded-full` +- 选中:`bg-blue-500 text-white` +- 未选中:`bg-gray-100 text-gray-800 hover:bg-gray-200` + +#### 4.5.4 RecommendedArticles(热门文章) + +- 容器:`rounded-xl shadow-sm p-5`(圆角更柔) +- 列表项:`block w-full text-left p-3 rounded-lg hover:bg-gray-50` +- 标题:`font-medium text-gray-900 group-hover:text-blue-600 line-clamp-2 break-words` +- Meta 行: + - 日期 `whitespace-nowrap truncate` + - 分隔符 `•` + - 作者 `truncate min-w-0` + - 眼睛图标 + 浏览量(`1.2k` 简写函数 `formatViews`) +- 同样使用渐隐遮罩。 + +### 4.6 ArticleReader 文章详情 + +**布局(桌面 `lg` 及以上,三栏)**: +``` +┌─────────────────────────────────────────────────────┐ +│ [封面图 h-96,渐变蒙层 to-black/70] │ +├──────────┬─────────────────────────────┬────────────┤ +│ 目录 TOC │ 文章正文(卡片) │ 分类 │ +│ (sticky) │ [评论] │ 标签 │ +│ │ │ 热门 │ +└──────────┴─────────────────────────────┴────────────┘ +``` + +- 卡片「浮」在封面上方:`"-mt-20 relative z-10"` +- 左:`hidden lg:block lg:w-1/4` +- 中:`w-full lg:w-1/2` +- 右:`hidden lg:block lg:w-1/4`,`sticky top-28` + +**封面**: +- 全宽 `h-96`,`object-cover`,渐变 `bg-gradient-to-b from-black/30 to-black/70`。 + +**TableOfContents**: +- 容器:`bg-white rounded-lg shadow-md p-5` +- 标题按钮 + 折叠列表(`translate-y` 平滑过渡) +- 缩进根据 `h2/h3/h4` 级别 +- 激活项 `bg-blue-50 text-blue-600` +- 滚动时联动 IntersectionObserver 高亮 + +**密码保护**: +- 中央卡片 `max-w-md mx-auto mt-10`,`bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4` +- 标签:`block text-gray-700 text-sm font-bold mb-2` +- 输入:`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700` +- 按钮:`bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded` + +**回到顶部**: +- 滚动 `> 300px` 后右下角显示圆形按钮 +- `fixed bottom-8 right-8 bg-white hover:bg-gray-100 text-black rounded-full p-3 shadow-lg transition-all duration-300 z-50` +- 平滑滚动 `behavior: 'smooth'` +- 暗色 `bg-gray-800 text-white` + +### 4.7 CommentSection 评论区 + +**结构**: +``` +┌─────────────────────────────────────┐ +│ 评论 │ +├─────────────────────────────────────┤ +│ [textarea 4 行:写下你的评论...] │ +│ [清空] [发表] │ +│ Enter发送,Shift+Enter换行 │ +├─────────────────────────────────────┤ +│ 评论项: │ +│ [头像 w-10] 用户名 • 时间 │ +│ 内容 (whitespace-pre-wrap) │ +│ 👍 12 回复 删除 │ +│ │ +│ └─ [头像 w-8] 用户名 │ +│ 回复 @父级 • 时间 │ +│ 内容 │ +│ 👍 12 回复 删除 │ +└─────────────────────────────────────┘ +``` + +- 容器:`mt-10 bg-white rounded-lg shadow p-6` +- 输入框:`border border-gray-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-blue-500` +- 操作按钮组:`absolute bottom-3 right-3 flex space-x-2` +- 评论项:`border-b border-gray-200 py-4` +- 头像:顶级 `w-10 h-10`,嵌套 `w-8 h-8`,`rounded-full object-cover` +- 嵌套缩进:`ml-8 mt-3` +- 点赞按钮:激活 `text-red-500`,未激活 `text-gray-500 hover:text-red-500` +- 文本溢出:`break-words whitespace-pre-wrap overflow-hidden` +- 时间显示函数:`formatTime`: + - `< 60s` → 刚刚 + - `< 3600s` → X 分钟前 + - `< 86400s` → X 小时前 + - 否则 → X 天前 +- 键盘:`Enter` 发送,`Shift+Enter` 换行。 + +### 4.8 Search 搜索 + +**触发**:Header 右侧放大镜按钮。 + +**弹出层(桌面)**: +- `absolute right-0 mt-2 w-96 max-w-[calc(100vw-2rem)]` +- 容器:`bg-white/90 backdrop-blur-md shadow-xl rounded-xl z-50 border border-gray-200` + +**弹出层(移动)**: +- `fixed right-4 left-4 top-20 w-auto max-w-none` +- `max-h-[60vh]` + +**输入框**: +- `px-5 py-3 pl-12 rounded-xl border bg-white/80 focus:ring-2 focus:ring-blue-500` +- 左侧搜索图标内嵌(`absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400`) +- 自动聚焦:弹层打开后 100ms。 + +**结果列表**: +- 项:`block p-3 rounded-xl hover:bg-blue-50 border border-transparent hover:border-blue-100` +- 标题 `line-clamp-1`,摘要 `line-clamp-2 text-xs` +- 滚动容器:`mt-3 max-h-96 overflow-y-auto space-y-1` + +**加载**: +- `animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-blue-500` + 「搜索中...」文字 + +**空结果**:居中 + 表情图标 + 「未找到相关文章」 + +**遮罩**:`fixed inset-0 bg-black/20 z-40 backdrop-blur-sm` + +**防抖**:`debounce 300ms` 触发接口 + +### 4.9 ThemeToggle 主题切换 + +**按钮**: +- `p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-all duration-200` +- 太阳/月亮 SVG,`h-6 w-6`,`strokeWidth={2}` +- 持久化:localStorage `theme = 'dark' | 'light'` + +**涟漪动画(View Transitions API)**: + +1. 计算按钮中心 `(x, y)`,计算到屏幕最远角的最大半径 `maxRadius`。 +2. 注入 CSS 自定义属性: + ```js + document.documentElement.style.setProperty('--ripple-x', `${x}px`); + document.documentElement.style.setProperty('--ripple-y', `${y}px`); + document.documentElement.style.setProperty('--ripple-r', `${maxRadius}px`); + ``` +3. 调用 `document.startViewTransition(applyTheme)`: + ```css + ::view-transition-old(root), + ::view-transition-new(root) { animation: none; mix-blend-mode: normal; } + ::view-transition-new(root) { + clip-path: circle(0px at var(--ripple-x) var(--ripple-y)); + animation: theme-ripple 0.5s ease-out forwards; + } + @keyframes theme-ripple { + to { clip-path: circle(var(--ripple-r) at var(--ripple-x) var(--ripple-y)); } + } + ``` +4. **降级**:无 `startViewTransition` 时直接切换 `.dark`。 + +### 4.10 Login / Register / ForgotPassword 表单 + +**结构**: +``` +[圆形图标 h-12 w-12 bg-blue-400] (Login) +[大标题 text-3xl font-extrabold] +[副标题] +[错误条] +[输入框块 × N] +[验证码:输入 + 图片 h-10 w-24] +[主按钮 w-full] +[副链接:注册 • 忘记密码?] +``` + +**容器**: +- 居中布局:`flex items-center justify-center py-12 px-4`,`paddingTop: 64` 避让 Header +- 卡片:`max-w-md w-full space-y-8`,内含图标 + 表单卡 `bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10` + +**图标头**: +- `mx-auto h-12 w-12 rounded-full bg-blue-400 flex items-center justify-center` +- 内嵌 SVG `h-6 w-6 text-white` + +**标题**:`mt-6 text-center text-3xl font-extrabold text-gray-900` + +**错误条**: +- `mb-4 bg-red-50 text-red-700 p-3 rounded-md text-sm` +- **5 秒后自动消失**(setTimeout clearError) +- 暗色 `bg-red-900/12 text-red-300` + +**输入框**: +- `appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm` +- `placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500` +- 暗色 `bg-gray-700 border-gray-600 text-white placeholder-gray-500` + +**验证码块**: +- `flex space-x-2`,左侧输入 `flex-1`,右侧图片 `h-10 w-24 border rounded cursor-pointer` +- 点击图片触发 `fetchCaptcha()` + +**主按钮**: +- `w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 disabled:opacity-50` +- 加载中按钮内嵌旋转 SVG: + ```jsx + + + + + ``` + +**副链接**: +- `mt-2 text-center text-sm text-gray-600` +- 「注册」+「忘记密码?」:`font-medium text-blue-600 hover:text-blue-500 ml-1` + +### 4.11 FriendLinks 友情链接 + +- 页面布局同后台表单,最大宽度居中卡片。 +- 分类(友人/站点)+ 描述 + 头像 + 链接。 +- 卡片网格 + hover 阴影 + 跳转外部。 + +### 4.12 Footer + +**结构**: +```jsx +
    +
    +
    +

    {copyright}

    + {icpRecord &&

    ...

    } + {mpsRecord &&

    ...

    } + {footerCode && } +
    +
    +
    +``` + +**规范**: +- `bg-white`,顶部留出 `mt-auto`。 +- 版权信息居中 `text-gray-600`。 +- ICP 备案链接:`text-gray-600 hover:text-blue-600 text-sm`,指向 `https://beian.miit.gov.cn`。 +- 公安备案链接:指向 `http://www.beian.gov.cn`。 +- 自定义 footer HTML 通过 `ScriptAwareHtml` 安全渲染(DOMPurify)。 + +### 4.13 PageNotFound + +- 居中布局 + 大号 404 + 「返回首页」按钮。 +- 按钮 `bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-md`。 + +--- + +## 5. 后台 Dashboard 模块 + +### 5.1 Dashboard 总体布局 + +**容器**: +```jsx +
    +
    + +
    + +
    + {activeTab === 'dashboard' && } + ... +
    +
    +
    +
    +``` + +**Suspense + FadeIn 过渡**: +- 每个视图都 `lazy(() => import(...))` 加载。 +- 包裹 `` 实现 `opacity 0 → 1` 淡入。 + +**自动检查更新**:登录后自动 `GET /update/check`,新版本 toast 提示。 +- 兼容:`info`,5000ms +- 不兼容:`warning`,5000ms + +### 5.2 Sidebar 侧边栏 + +**布局**: +```jsx +
    +``` + +**尺寸变化**: +- 展开:`w-64`(256px) +- 收起:`w-16`(64px,仅图标) +- 移动端:从 `-translate-x-full` → `translate-x-0` + +**折叠按钮(hover 才显示)**: +```jsx + +``` + +**子菜单**: +- `mt-1 space-y-1` +- 子项:`px-3 py-2 ml-6`,图标 `text-base` +- 收起时不展开子菜单(点击父级自动展开) + +**底部用户信息**: +- `mt-auto p-4 border-t border-gray-200 sidebar-user-info-border` +- 头像 `w-8 h-8 rounded-full` +- 用户名:`text-sm font-medium text-gray-900 truncate` +- 设置图标按钮:`text-gray-500 hover:text-gray-700 p-1 rounded-md hover:bg-gray-100` + +**移动端遮罩**: +```jsx +
    +``` + +**暗色模式**: +- `sidebar-bg: #1F2937` +- `sidebar-nav-item-active: bg-blue-800 text-blue-200` +- `sidebar-user-name: #F3F4F6` + +### 5.3 DashboardHeader 顶部条 + +**结构**: +```jsx +
    +
    +
    + +

    仪表板

    +
    +
    + +
    + + {userMenuOpen && ( +
    + +
    + +
    + )} +
    +
    +
    +
    +``` + +**规范**: +- 高度 `h-16`,`shadow-sm`(比前台 Header 弱),底部边框。 +- 汉堡按钮:`md:hidden inline-flex items-center justify-center h-9 w-9 mr-3 rounded-lg border`,`active:scale-95` 按下反馈。 +- 标题:`text-xl font-bold text-blue-600`(按 tab 动态变化)。 +- 用户菜单下拉:`w-48`,「返回网站」+「登出」分隔。 + +### 5.4 DashboardView 仪表盘首页 + +**结构**: +``` +┌── 统计卡片(4 列 grid) ────────────────┐ +│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │总文章数│ │总用户数│ │总评论数│ │总访问量│ │ +│ └──────┘ └──────┘ └──────┘ └──────┘ │ +└────────────────────────────────────────┘ + +┌── 快捷操作(3 列 grid) ─────────────────┐ +│ 卡片:图标 + 标题 + 描述 + → │ +└────────────────────────────────────────┘ + +┌── 通知 ────────────────────────────────┐ +│ [通知标题] [全部] [未读] [已读]│ +│ ┌──────────────────────────────┐ │ +│ │ 未读高亮蓝 / 已读白 │ │ +│ │ [标为已读] [删除] │ │ +│ └──────────────────────────────┘ │ +│ ... 无限滚动 IntersectionObserver │ +└────────────────────────────────────────┘ +``` + +#### 5.4.1 统计卡片 + +```jsx +
    + {stats.map(stat => ( +
    +
    +
    +

    {stat.label}

    +

    {stat.value}

    +
    +
    + {getIcon(stat.icon)} +
    +
    +
    + ))} +
    +``` + +- 卡片:`bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md` +- 图标块:`p-3 bg-blue-50 rounded-lg` +- 数字:`text-2xl font-bold text-gray-900 mt-1` +- 标签:`text-sm font-medium text-gray-500` + +#### 5.4.2 快捷操作 + +```jsx +
    +
    + {quickActions.map(action => ( +
    +
    +
    + {getIcon(action.icon)} +
    +
    +

    {action.title}

    +

    {action.description}

    +
    + ...→ +
    +
    + ))} +
    +
    +``` + +- 卡片:`border border-gray-200 rounded-lg p-4 hover:bg-gray-50` +- 右侧箭头 `text-gray-400`,hover 时变 `text-blue-500`(可选)。 + +#### 5.4.3 通知面板 + +- 容器:`notification-container bg-white rounded-lg shadow-sm border border-gray-200 p-6` +- 标题行:`flex items-center justify-between mb-4` +- 标签切换: + ``` + } +
    +
    +``` + +**错误条**: +```jsx +{error && ( +
    +

    {error}

    +
    +)} +``` + +**表格**: +```jsx +
    + + + {columns.map(col => )} + + + {data.map(item => ( + + {columns.map(col => )} + + ))} + +
    {col.label}
    {col.render ? col.render(...) : cellValue}
    +
    +``` + +- 表头:`bg-gray-50`,`text-xs font-medium text-gray-500 uppercase tracking-wider` +- 行 hover:`hover:bg-gray-50` +- 单元格:`px-6 py-4 whitespace-nowrap text-sm` +- 支持 `columns[i].render(value, item, index)` 自定义渲染。 +- 支持 `columns[i].headerClassName / className` 自定义样式。 + +**加载态**: +```jsx +
    +
    +
    +``` + +**空态**:`text-center py-12 text-gray-500` + `emptyText`。 + +### 5.6 Pagination 分页器 + +**容器**:`flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 mt-4` + +**移动端**: +```jsx +
    + + +
    +``` + +**桌面端**: +```jsx +
    +
    +

    + 显示第 {(currentPage-1)*pageSize+1} 到{' '} + {Math.min(currentPage*pageSize, totalItems)} 条结果, + 共 {totalItems} 条 +

    +
    +
    + {/* 分页按钮组 */} + + + {/* 跳转 */} +
    + 跳转到 + + +
    +
    +
    +``` + +**逻辑**: +- `delta = 2`:当前页前后显示 2 个数字页码。 +- `pageItems = [1, ..., currentPage-delta ~ currentPage+delta, ..., totalPages]`,省略号自动插入。 +- `if (totalPages <= 1) return null` —— 单页不渲染。 + +### 5.7 AvatarUpload 头像上传 + +**结构**: +``` +┌─────────────┐ +│ [头像预览] │ ← w-32 h-32 rounded-full +│ 上传中遮罩 │ border-2 border-gray-300 +└─────────────┘ +[选择头像 按钮] +[错误提示] +[格式说明:JPG/PNG/GIF/WebP ≤5MB] +``` + +**预览**: +```jsx +
    + 头像预览 + {uploading && ( +
    +
    +
    + )} +
    +``` + +**按钮**: +```jsx + +``` + +**校验**: +- 类型限制:JPG/JPEG/PNG/GIF/WebP +- 大小限制:5MB +- 错误:`text-red-600 text-sm` + +**进度**: +- 通过 `uploadProgress.start/progress/processing/done/error` 联动全局进度面板。 + +### 5.8 AnnouncementManager 公告管理 + +**结构**: +``` +┌── 头部 ─────────────────────────────┐ +│ [公告管理] [上传图片] [保存公告] │ +├────────────────────────────────────┤ +│ [编辑 50%] │ [预览 50%] │ +│