前端管理员面板开发
总结在另一篇文章里,比较长,主要是Bug合集 ==> 点击这里
采用一种纯粹的前端视角,设计一个健壮、可维护的管理员系统架构,并将其无缝集成到 Astro 项目中。这个架构将完全独立,不依赖任何后端实现。
核心架构原则
-
客户端渲染隔离区:
- 公开的博客网站应保持 Astro 的 SSG (静态站点生成) 优势,以获得极致性能和 SEO。
- 而整个管理员后台(从登录到文章编辑)将是一个完全在客户端运行的**单页应用 **
- 我们将利用 Astro 的 client:only 指令来创建这个“SPA 隔离区”,确保后台的动态逻辑不会影响到静态站点的性能。
-
严格的职责分离:
前端代码将被划分为清晰的、功能单一的模块。
-
UI 组件只负责渲染
-
服务模块只负责业务逻辑和数据请求
-
状态管理模块只负责维护应用状态。
这种分离是可维护性和可测试性的基石。
-
-
无状态认证驱动:
前端应用本身不存储任何持久化的用户状态。应用的“登录状态”完全由是否存在一个有效的、存储在 localStorage 中的 JWT 来决定。这是前端的唯一真相来源。
前端工程架构
Astro 项目 src/ 目录进行如下结构化扩展:
src/
├── pages/
│ ├── posts/
│ │ └── [slug].astro // (公开) 博客文章页 (SSG)
│ ├── index.astro // (公开) 博客首页 (SSG)
│ │
│ └── admin/ // [新增]理员应用的根路由
│ ├── login.astro // 管理员登录页 (后台入口)
│ ├── dashboard.astro // 管理员仪表盘/文章列表页
│ ├── editor/
│ ├── new.astro // 新建文章页
│ └── [slug].astro // 编辑文章页
│
├── components/
│ ├── admin/ // [新增]所有后台专用UI组件
│ │ ├── LoginForm.svelte // 登录表单 (交互组件)
│ │ ├── PostTable.svelte // 文章列表表格 (交互组件)
│ │ └── Editor.svelte // Markdown 编辑器 (交互组件)
│
├── script/
│ └── managePost.js // 文件修改的实际操作者(备用)
│
├── layouts/
│ ├── AdminLayout.astro // [新增]后台页面的通用布局
│
└── services/ // [新增]核心业务逻辑层 (纯TS)
├── authService.ts // 认证服务:负责登录、登出、令牌管理
├── apiClient.ts // API客户端:封装fetch, 自动附加JWT
├── postService.ts // 文章服务:负责文章的CRUD数据请求
核心模块设计与职责
1. 服务层 (Services)
整个后台应用的大脑,处理所有非 UI 的逻辑。
-
authService.ts:
- 职责: 提供 login(username, password)、logout()、getToken()、isLoggedIn() 等方法。
- login(): 向后端(未来的)/api/auth/token 发送凭据,成功后将收到的 JWT 存入 localStorage。
- logout(): 从 localStorage 中移除 JWT。
- getToken(): 从 localStorage 中读取 JWT。
- isLoggedIn(): 检查 localStorage 中是否存在 JWT(未来可增加解码检查是否过期)。
-
apiClient.ts:
-
职责:
创建一个集中的、可配置的 HTTP 请求客户端 (可以理解为是一个自定义的 Axios 实例)。
-
核心功能:
封装原生 fetch API。在其内部,对所有非公开的请求,自动调用
authService.getToken(),并将获取到的 JWT 添加到Authorization: Bearer <token>请求头中。 -
好处:
将认证头部的附加逻辑集中在一处,避免在每个组件的每次请求中重复编写。同时,它还可以统一处理 API 的基础 URL、超时和错误格式化。
-
-
postService.ts:
-
职责:
提供 getAllPosts()、getPostBySlug(slug)、createPost(data)、updatePost(slug, data)、deletePost(slug) 等方法。
-
实现:
该模块不直接执行 fetch。它会调用 apiClient 来发起请求,从而自动享受 JWT 的注入。例如 getAllPosts() 内部会调用 apiClient.get(‘/api/admin/posts’)。
-
2. 页面与路由层 (src/pages/admin/)
-
login.astro:
-
架构: 页面本身是一个 Astro 文件,负责提供基础 HTML 结构和 SEO 元数据。
-
核心:
页面内部会引入一个交互式组件(如
<LoginForm client:svelte:only />,框架可以是React/Vue/Svelte/Lit)。所有用户交互、状态管理和 API 调用都在这个客户端组件内部完成,它会调用 authService.login()。
-
-
受保护的页面 (e.g., dashboard.astro):
-
架构: 同样是一个 Astro 文件作为外壳,加载一个主客户端组件
<DashboardPage client:only="react" />。 -
路由守卫实现: Astro 页面本身是静态的,无法直接实现服务端重定向。因此,在页面顶部嵌入一个引导脚本 (Bootstrap Script)。----只供参考
<!-- dashboard.astro --> <script is:inline> // 这个脚本会优先、同步执行 import { isLoggedIn } from '../../services/authService'; if (!isLoggedIn()) { // 如果未登录,在任何内容渲染之前,立即重定向 window.location.href = '/admin/login'; } </script> <AdminLayout> <!-- 只有通过了检查,下面的组件才会被渲染和执行 --> <DashboardPage client:only="react" /> </AdminLayout> -
逻辑: 这个内联脚本构成了客户端的“路由守卫”。它在页面渲染的最初阶段检查登录状态,从而保护了整个页面。
-
3. 组件层 (src/components/admin/)
-
职责: 构建可复用的、交互式的 UI 单元。这些组件应设计为“受控组件”或“展示性组件”。
-
交互逻辑:
组件通过 props 接收数据,通过回调函数(如 onSave, onDelete)向上传递用户意图。组件自身不直接调用 postService,而是调用由父页面/组件传递下来的函数。
-
例子 (PostTable.svelte):
- 接收一个 posts 数组来渲染表格,当用户点击删除按钮时,调用 props.onDelete(postId)。具体的 API 请求逻辑由父组件(DashboardPage)处理。
-
复用组件: AdminPostCard.astro
-
复用游客前端的卡片样式 yukina\src\components\PostCard.astro,渲染管理员面板页面 dashboard.astro 中文章列表的布局样式
-
使用 import { Icon } from “astro-icon/components” 复用已有的图标样式(和PostCard.astro相同)
-
因为拿到的是文章的简要内容:
-
标题
-
一级分类
-
二级分类
-
日期
-
tags
-
author
没有 图片 参数,所以和 PostCard.astro 的接收参数不同
-
-
-
后端传输的数据格式
class PostMetadata(BaseModel): """ Post metadata model - matches frontend AdminPostCard.astro Props interface Used for article list display without content body """ slug: str title: str published: date description: Optional[str] = None tags: Optional[List[str]] = None first_level_category: str second_level_category: str author: Optional[str] = None draft: Optional[bool] = False cover: Optional[str] = None sourceLink: Optional[str] = None licenseName: Optional[str] = None licenseUrl: Optional[str] = None前端管理员面板获取文章列表时的数据结构应该和它匹配
数据流与工作流
- 用户访问 /admin/dashboard:
- Astro 页面加载,内联的“路由守卫”脚本执行。
- authService.isLoggedIn() 检查 localStorage。
- 若未登录,页面重定向到 /admin/login。
- 若已登录,页面继续渲染,
<DashboardPage>客户端组件被挂载。
- DashboardPage 组件挂载:
- 组件的 useEffect (或等效的生命周期钩子) 被触发。
- 它调用 postService.getAllPosts() 来请求数据。
- postService 调用 apiClient.get(…)。
- apiClient 调用 authService.getToken(),将 JWT 放入请求头,然后发起 fetch 请求。
- 收到后端响应后,数据沿调用链返回到组件。
- 组件更新其内部状态,并将数据通过 props 传递给
<PostTable>组件,UI 更新。
通过这种架构,在不动后端的情况下,完整地构建出一个结构清晰、逻辑分离、易于扩展和维护的前端管理员系统。所有与后端通信的逻辑都被完美地封装在了 services 层,未来只需要填充 FastAPI 的接口实现即可。
具体组件调整
问题
- UI设计偏好:更倾向于复用现有PostCard组件的卡片式布局,还是需要表格式的管理界面 ?
- 编辑器功能:需要什么级别的Markdown编辑器, 简单文本框还是带预览的富文本编辑器 ?
- 权限设计:是否需要权限等级(如只读/编辑),还是登录用户拥有全部权限?
- 响应式要求:管理后台是否需要支持移动端访问?
方案
-
选择表格式(Table-based)的管理界面。
在 PostTable .svelte) 组件中,使用标准的
<table>元素。借助 Tailwind CSS ,将一个朴素的表格变得美观且专业。 -
使用VScode 内核 Monaco Editor 作为Markdown编辑器
-
登录用户直接拥有全部权限,因为管理员只会有一个人
-
实现响应式设计,保证在移动端显示效果,优先保证桌面端。
借助 Tailwind CSS 这样的现代CSS框架,实现响应式布局的额外工作量非常小。只需要使用 sm:, md:, lg: 这样的断点前缀来调整布局即可。
-
具体例子:
桌面端上显示的表格 (Table),在移动端(小屏幕)上可以很轻松地转换为卡片列表 (List of Cards) 的展现形式,这样就不会出现横向滚动条,体验会好很多。
-
注意事项
-
Monaco Editor的使用:
Monaco Editor 不是一个普通的UI组件库。它是一个非常复杂的、自成体系的“微型VS Code”。因此,将它集成到现代前端框架(如Astro/Svelte/Vite)中,比简单地import一个组件要多几个步骤。
核心问题在于:
Monaco Editor 包含大量的 Web Workers(用于不同语言的语法分析、智能提示等),它的模块加载方式与标准JavaScript模块有些不同。直接 import 常常会导致打包工具(如Vite)出错或效率低下。
集成 Monaco Editor 的完整步骤 (Astro + Svelte):
-
安装依赖库
# 安装 Monaco Editor 核心库 pnpm add monaco-editor # 安装专门用于 Vite 的 Monaco Editor 插件 # 这是最关键的一步,它会处理所有复杂的打包问题 pnpm add -D vite-plugin-monaco-editor -
配置 Vite (在 Astro 中)
告诉 Astro (以及它底层的 Vite) 去使用刚刚安装的插件。
- 打开 Astro 配置文件:yukina/astro.config.mjs。
- 修改文件,引入并添加 monacoEditorPlugin。
例如:
// 引入插件 import monacoEditorPlugin from 'vite-plugin-monaco-editor'; // https://astro.build/config export default defineConfig({ integrations: [svelte(), tailwind()], // 添加 Vite 配置 vite: { plugins: [ // 注意:插件需要调用 monacoEditorPlugin.default() monacoEditorPlugin.default({}) ] } }); -
在 Svelte 组件中封装 Monaco Editor
创建一个 Editor.svelte 组件来加载和控制编辑器。在 Svelte 中与这种需要直接操作 DOM 的库进行交互,最佳实践是使用 onMount 生命周期函数和 bind:this 指令。
-
Astro 页面中使用编辑器组件
现在,你可以像使用任何其他 Svelte 组件一样使用 Editor.svelte 了。
但有一个至关重要的注意事项:Monaco Editor 是一个纯客户端库,它不能在服务器端渲染 (SSR)。
因此,必须使用 client:only 指令来加载它。
-
总结
- 安装: pnpm add monaco-editor 和 pnpm add -D vite-plugin-monaco-editor。
- 配置: 在 astro.config.mjs 中添加 Vite 插件。
- 封装: 在 Svelte 组件 (.svelte) 中,使用 onMount 和 bind:this 来初始化和控制 Monaco Editor 实例。
- 使用: 在 Astro 页面 (.astro) 中,必须使用
<Editor client:only="svelte" />指令来加载组件。