前端管理员面板开发

总结在另一篇文章里,比较长,主要是Bug合集 ==> 点击这里

​ 采用一种纯粹的前端视角,设计一个健壮、可维护的管理员系统架构,并将其无缝集成到 Astro 项目中。这个架构将完全独立,不依赖任何后端实现。

核心架构原则

  1. 客户端渲染隔离区:

    1. 公开的博客网站应保持 Astro 的 SSG (静态站点生成) 优势,以获得极致性能和 SEO。
    2. 而整个管理员后台(从登录到文章编辑)将是一个完全在客户端运行的**单页应用 **
    3. 我们将利用 Astro 的 client:only 指令来创建这个“SPA 隔离区”,确保后台的动态逻辑不会影响到静态站点的性能。
  2. 严格的职责分离:

    ​ 前端代码将被划分为清晰的、功能单一的模块。

    • UI 组件只负责渲染

    • 服务模块只负责业务逻辑和数据请求

    • 状态管理模块只负责维护应用状态。

      这种分离是可维护性和可测试性的基石。

  3. 无状态认证驱动:

    ​ 前端应用本身不存储任何持久化的用户状态。应用的“登录状态”完全由是否存在一个有效的、存储在 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):

    1. 接收一个 posts 数组来渲染表格,当用户点击删除按钮时,调用 props.onDelete(postId)。具体的 API 请求逻辑由父组件(DashboardPage)处理。
  • 复用组件: AdminPostCard.astro

    1. 复用游客前端的卡片样式 yukina\src\components\PostCard.astro,渲染管理员面板页面 dashboard.astro 中文章列表的布局样式

    2. 使用 import { Icon } from “astro-icon/components” 复用已有的图标样式(和PostCard.astro相同)

    3. 因为拿到的是文章的简要内容:

      1. 标题

      2. 一级分类

      3. 二级分类

      4. 日期

      5. tags

      6. 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

    前端管理员面板获取文章列表时的数据结构应该和它匹配

数据流与工作流

  1. 用户访问 /admin/dashboard:
    1. Astro 页面加载,内联的“路由守卫”脚本执行。
    2. authService.isLoggedIn() 检查 localStorage。
    3. 未登录,页面重定向到 /admin/login。
    4. 已登录,页面继续渲染,<DashboardPage> 客户端组件被挂载。
  2. DashboardPage 组件挂载:
    1. 组件的 useEffect (或等效的生命周期钩子) 被触发。
    2. 它调用 postService.getAllPosts() 来请求数据。
    3. postService 调用 apiClient.get(…)。
    4. apiClient 调用 authService.getToken(),将 JWT 放入请求头,然后发起 fetch 请求。
    5. 收到后端响应后,数据沿调用链返回到组件。
    6. 组件更新其内部状态,并将数据通过 props 传递给 <PostTable> 组件,UI 更新。

​ 通过这种架构,在不动后端的情况下,完整地构建出一个结构清晰、逻辑分离、易于扩展和维护的前端管理员系统。所有与后端通信的逻辑都被完美地封装在了 services 层,未来只需要填充 FastAPI 的接口实现即可。

具体组件调整

问题

  1. UI设计偏好:更倾向于复用现有PostCard组件的卡片式布局,还是需要表格式的管理界面 ?
  2. 编辑器功能:需要什么级别的Markdown编辑器, 简单文本框还是带预览的富文本编辑器 ?
  3. 权限设计:是否需要权限等级(如只读/编辑),还是登录用户拥有全部权限?
  4. 响应式要求:管理后台是否需要支持移动端访问?

方案

  1. 选择表格式(Table-based)的管理界面。

    在 PostTable .svelte) 组件中,使用标准的 <table> 元素。借助 Tailwind CSS ,将一个朴素的表格变得美观且专业。

  2. 使用VScode 内核 Monaco Editor 作为Markdown编辑器

  3. 登录用户直接拥有全部权限,因为管理员只会有一个人

  4. 实现响应式设计,保证在移动端显示效果,优先保证桌面端。

    ​ 借助 Tailwind CSS 这样的现代CSS框架,实现响应式布局的额外工作量非常小。只需要使用 sm:, md:, lg: 这样的断点前缀来调整布局即可。

    • 具体例子:

      ​ 桌面端上显示的表格 (Table),在移动端(小屏幕)上可以很轻松地转换为卡片列表 (List of Cards) 的展现形式,这样就不会出现横向滚动条,体验会好很多。

注意事项

  1. Monaco Editor的使用:

    ​ Monaco Editor 不是一个普通的UI组件库。它是一个非常复杂的、自成体系的“微型VS Code”。因此,将它集成到现代前端框架(如Astro/Svelte/Vite)中,比简单地import一个组件要多几个步骤。

    核心问题在于

    ​ Monaco Editor 包含大量的 Web Workers(用于不同语言的语法分析、智能提示等),它的模块加载方式与标准JavaScript模块有些不同。直接 import 常常会导致打包工具(如Vite)出错或效率低下。

    集成 Monaco Editor 的完整步骤 (Astro + Svelte):

    1. 安装依赖库

      # 安装 Monaco Editor 核心库
      pnpm add monaco-editor
      
      # 安装专门用于 Vite 的 Monaco Editor 插件
      #   这是最关键的一步,它会处理所有复杂的打包问题
      pnpm add -D vite-plugin-monaco-editor
    2. 配置 Vite (在 Astro 中)

      告诉 Astro (以及它底层的 Vite) 去使用刚刚安装的插件。

      1. 打开 Astro 配置文件:yukina/astro.config.mjs。
      2. 修改文件,引入并添加 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({})
          ]
        }
      });
    3. 在 Svelte 组件中封装 Monaco Editor

      ​ 创建一个 Editor.svelte 组件来加载和控制编辑器。在 Svelte 中与这种需要直接操作 DOM 的库进行交互,最佳实践是使用 onMount 生命周期函数和 bind:this 指令。

    4. Astro 页面中使用编辑器组件

      现在,你可以像使用任何其他 Svelte 组件一样使用 Editor.svelte 了。

      但有一个至关重要的注意事项:Monaco Editor 是一个纯客户端库,它不能在服务器端渲染 (SSR)。

      因此,必须使用 client:only 指令来加载它。

总结

  1. 安装: pnpm add monaco-editor 和 pnpm add -D vite-plugin-monaco-editor。
  2. 配置: 在 astro.config.mjs 中添加 Vite 插件。
  3. 封装: 在 Svelte 组件 (.svelte) 中,使用 onMount 和 bind:this 来初始化和控制 Monaco Editor 实例。
  4. 使用: 在 Astro 页面 (.astro) 中,必须使用<Editor client:only="svelte" />指令来加载组件。
Author

JuyaoHuang

Publish Date

10 - 01 - 2025