Files
raisfast/docs/page-block-design.md
T
2026-04-23 19:20:53 +08:00

26 KiB
Raw Blame History

Page + Block 设计方案

版本:v1.0 · 日期:2026-04-23

一、目标

在现有博客系统基础上,增加 页面(Page块(Block 支持,使其能够构建绝大部分企业官网场景:

  • 首页(Hero + 统计 + 服务介绍 + 客户评价 + CTA)
  • 关于我们(公司介绍 + 发展历程 + 团队成员)
  • 产品/服务(特性展示 + 定价方案)
  • 联系我们(表单 + 地图)
  • 案例/作品集(画廊 + 评价)
  • FAQ、博客、新闻等

设计参考

CMS 块系统 借鉴点
WordPress Gutenberg 块编辑器 + 可复用块 块类型体系、拖拽排序
Strapi Dynamic Zones 组件化 JSON 字段 tagged union 数据结构
Payload CMS Blocks Layout 自定义块 + 布局 嵌套栏目(Columns
Notion 分栏 + 嵌套 多栏布局递归嵌套

二、架构概览

┌───────────────────────────────────────────┐
│                  Page                      │
│                                           │
│  title / slug / status / template / SEO   │
│                                           │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐  │
│  │ Block 0  │ │ Block 1  │ │ Block N  │  │
│  │  (hero)  │ │ (text)   │ │ (gallery)│  │
│  │ sort=0   │ │ sort=1   │ │ sort=N   │  │
│  └──────────┘ └──────────┘ └──────────┘  │
│                                           │
│  blocks: JSON ARRAY(有序)               │
└───────────────────────────────────────────┘

┌───────────────────────────────────────────┐
│            Reusable Block                 │
│                                           │
│  name / type / content(JSON) / global     │
│                                           │
│  → 可被任意 Page 引用:                   │
│    { "type": "reusable", "ref_id": "..." }│
└───────────────────────────────────────────┘

为什么原生实现(而非 Content Type

维度 Content Type 方案 原生方案(推荐)
公开路由 需要额外插件/中间件拦截 slug 直接注册 /pages/{slug} 路由
Block 嵌套 JSON 字段无类型校验 Rust enum 强类型校验
SEO 元数据 需自定义字段 内建 meta_title / meta_description / og_image
模板系统 无法在 Rust 层控制 可注册自定义模板渲染器
版本控制 content_revisions 表已支持 复用已有 revision 系统
层级页面 需手动实现 parent_id 内建 hierarchy
排序控制 无内建支持 sort_order 字段

三、数据库设计

Migration 023_create_pages.sql

-- ============================================================
-- 页面表
-- ============================================================
CREATE TABLE IF NOT EXISTS pages (
    id               TEXT PRIMARY KEY,           -- UUID v7
    tenant_id        TEXT NOT NULL DEFAULT 'default',
    title            TEXT NOT NULL,
    slug             TEXT NOT NULL,

    -- 内容模式(二选一,或并存)
    content          TEXT,                       -- 简单模式:纯 markdown
    blocks           TEXT,                       -- 块模式:JSON array

    -- SEO
    meta_title       TEXT,
    meta_description TEXT,
    og_image         TEXT,

    -- 布局
    template         TEXT NOT NULL DEFAULT 'default',  -- default / full / landing / contact / ...

    -- 层级(支持树形页面)
    parent_id        TEXT REFERENCES pages(id) ON DELETE SET NULL,
    sort_order       INTEGER NOT NULL DEFAULT 0,

    -- 状态
    status           TEXT NOT NULL DEFAULT 'draft',    -- draft / published / archived
    author_id        TEXT NOT NULL REFERENCES users(id),

    -- 封面图
    cover_image      TEXT,

    -- 时间戳
    published_at     TEXT,
    created_at       TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at       TEXT NOT NULL DEFAULT (datetime('now')),

    UNIQUE(tenant_id, slug)
);

CREATE INDEX idx_pages_slug      ON pages(tenant_id, slug);
CREATE INDEX idx_pages_status    ON pages(tenant_id, status);
CREATE INDEX idx_pages_parent    ON pages(tenant_id, parent_id);
CREATE INDEX idx_pages_author    ON pages(author_id);

-- ============================================================
-- 可复用块(全局组件,跨页面引用)
-- ============================================================
CREATE TABLE IF NOT EXISTS reusable_blocks (
    id          TEXT PRIMARY KEY,
    tenant_id   TEXT NOT NULL DEFAULT 'default',
    name        TEXT NOT NULL,          -- 显示名称,如 "公司介绍"
    type        TEXT NOT NULL,          -- hero / text / gallery / ...
    content     TEXT NOT NULL,          -- JSON:该类型块的数据
    description TEXT,                   -- 备注
    created_at  TEXT NOT NULL DEFAULT (datetime('now')),
    updated_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_reusable_blocks_tenant ON reusable_blocks(tenant_id);

表关系

pages.author_id → users.id
pages.parent_id → pages.id (自引用,支持树形)

reusable_blocks 独立表,通过 blocks JSON 中的 { type: "reusable", ref_id } 引用

四、Block 类型系统(Rust Enum

核心设计

每个 Block 是一个 tagged unionserde#[serde(tag = "type")]),序列化为 JSON 时自动携带 type 字段:

[
  { "type": "hero", "title": "...", "subtitle": "..." },
  { "type": "richtext", "content": "# Hello" },
  { "type": "stats", "items": [...] },
  { "type": "reusable", "ref_id": "..." }
]

类型定义

// src/models/page.rs

use serde::{Deserialize, Serialize};

/// 页面中的一个块
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PageBlock {
    // ── 内容类 ──

    /// 大图横幅(首页 Hero
    Hero {
        title: String,
        subtitle: Option<String>,
        background_image: Option<String>,
        cta_text: Option<String>,      // 按钮文字
        cta_url: Option<String>,       // 按钮链接
        alignment: Option<String>,     // left / center / right
        overlay: Option<bool>,         // 暗色遮罩
        height: Option<String>,        // sm / md / lg / full
    },

    /// 富文本(Markdown
    Richtext {
        content: String,
    },

    /// 单张图片
    Image {
        url: String,
        alt: Option<String>,
        caption: Option<String>,
        link: Option<String>,
        width: Option<String>,         // full / half / third / quarter
    },

    /// 图片画廊
    Gallery {
        images: Vec<GalleryImage>,
        columns: Option<u32>,          // 2 / 3 / 4
        gap: Option<String>,           // sm / md / lg
    },

    /// 视频
    Video {
        url: String,
        provider: Option<String>,      // youtube / bilibili / vimeo / custom
        title: Option<String>,
        autoplay: Option<bool>,
    },

    // ── 转化类 ──

    /// 行动号召(Call to Action
    Cta {
        title: String,
        description: Option<String>,
        button_text: String,
        button_url: String,
        style: Option<String>,         // primary / outline / banner
        background_image: Option<String>,
    },

    /// 客户评价
    Testimonial {
        items: Vec<TestimonialItem>,
        layout: Option<String>,        // carousel / grid / masonry
    },

    // ── 信息展示类 ──

    /// 常见问题
    Faq {
        items: Vec<FaqItem>,
    },

    /// 数据统计
    Stats {
        items: Vec<StatItem>,
        background: Option<String>,    // light / dark / primary / image
    },

    /// 时间线
    Timeline {
        items: Vec<TimelineItem>,
    },

    /// 团队成员
    Team {
        members: Vec<TeamMember>,
        columns: Option<u32>,          // 2 / 3 / 4
    },

    /// 价格表
    Pricing {
        plans: Vec<PricingPlan>,
        highlight_index: Option<usize>,  // 高亮哪个方案
    },

    // ── 交互类 ──

    /// 联系表单
    ContactForm {
        email_to: Option<String>,
        fields: Option<Vec<FormFieldDef>>,
        submit_text: Option<String>,
        success_message: Option<String>,
    },

    /// 地图
    Map {
        address: Option<String>,
        lat: Option<f64>,
        lng: Option<f64>,
        zoom: Option<u32>,
    },

    // ── 排版类 ──

    /// 代码块
    Code {
        code: String,
        language: Option<String>,
        show_line_numbers: Option<bool>,
    },

    /// 引用
    Quote {
        content: String,
        author: Option<String>,
        source: Option<String>,
    },

    /// 分隔线
    Divider {
        style: Option<String>,         // solid / dashed / dotted / space
    },

    /// 间距
    Spacer {
        height: Option<String>,        // sm / md / lg / xl 或 "48px"
    },

    // ── 布局类 ──

    /// 多栏布局(可递归嵌套子块)
    Columns {
        columns: Vec<ColumnDef>,
        gap: Option<String>,
    },

    /// 自定义 HTML(仅管理员可用)
    Html {
        content: String,
    },

    /// 引用可复用块
    Reusable {
        ref_id: String,
    },
}

辅助结构体

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GalleryImage {
    pub url: String,
    pub alt: Option<String>,
    pub caption: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestimonialItem {
    pub quote: String,
    pub author: String,
    pub company: Option<String>,
    pub avatar: Option<String>,
    pub rating: Option<u32>,           // 1-5
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FaqItem {
    pub question: String,
    pub answer: String,
    pub is_open: Option<bool>,         // 默认展开
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatItem {
    pub label: String,
    pub value: String,
    pub suffix: Option<String>,        // + / 万 / % / +
    pub icon: Option<String>,
    pub description: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineItem {
    pub date: String,
    pub title: String,
    pub description: Option<String>,
    pub icon: Option<String>,
    pub color: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamMember {
    pub name: String,
    pub role: Option<String>,
    pub avatar: Option<String>,
    pub bio: Option<String>,
    pub social_links: Option<Vec<SocialLink>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SocialLink {
    pub platform: String,              // twitter / github / linkedin / email / website
    pub url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricingPlan {
    pub name: String,
    pub price: String,
    pub period: Option<String>,        // 月 / 年 / 次
    pub description: Option<String>,
    pub features: Vec<String>,
    pub button_text: Option<String>,
    pub button_url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormFieldDef {
    pub name: String,
    pub label: String,
    pub field_type: String,            // text / email / phone / textarea / select
    pub required: Option<bool>,
    pub options: Option<Vec<String>>,  // select 类型的选项
    pub placeholder: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDef {
    pub width: Option<String>,         // "1/3" "1/2" "2/3" "1/4" "3/4"
    pub blocks: Vec<PageBlock>,        // 递归嵌套
}

五、API 设计

公开 API

方法 路径 说明 认证
GET /pages 页面列表(仅 published
GET /pages/{slug} 按 slug 获取页面(含解析后的 blocks)
GET /pages/sitemap 站点地图数据(所有已发布页 slug + 更新时间)

管理 API

方法 路径 说明 认证
GET /admin/pages 列表(全部状态,支持筛选) Author
GET /admin/pages/{id} 详情 Author
POST /admin/pages 创建 Author
PUT /admin/pages/{id} 更新 Author
DELETE /admin/pages/{id} 删除 Author
PUT /admin/pages/{id}/status 发布 / 下线 / 归档 Author
PUT /admin/pages/reorder 批量调整排序 Author

可复用块

方法 路径 说明 认证
GET /admin/reusable-blocks 列表 Author
POST /admin/reusable-blocks 创建 Author
PUT /admin/reusable-blocks/{id} 更新 Author
DELETE /admin/reusable-blocks/{id} 删除 Author

块元数据

方法 路径 说明 认证
GET /admin/block-templates 返回所有可用块类型定义 Author

API 响应格式

遵循项目约定(code: 0 表示成功):

// GET /pages/about
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "01923abc...",
    "title": "关于我们",
    "slug": "about",
    "status": "published",
    "template": "default",
    "content": null,
    "blocks": [
      { "type": "hero", "title": "关于我们", "subtitle": "...", "height": "lg" },
      { "type": "richtext", "content": "公司成立于..." },
      { "type": "stats", "items": [...] },
      { "type": "timeline", "items": [...] }
    ],
    "meta_title": "关于我们 - XX公司",
    "meta_description": "...",
    "og_image": "/uploads/...",
    "parent_id": null,
    "sort_order": 0,
    "author_id": "...",
    "cover_image": null,
    "published_at": "2026-04-20T08:00:00Z",
    "created_at": "2026-04-19T10:00:00Z",
    "updated_at": "2026-04-20T08:00:00Z"
  }
}
// GET /pages?page=1&page_size=10
{
  "code": 0,
  "message": "ok",
  "data": {
    "items": [...],
    "total": 15,
    "page": 1,
    "page_size": 10
  }
}

六、前端结构

Admin 页面

web/src/app/admin/pages/
  page.tsx                        — 页面列表(表格 + 状态筛选 + 搜索)
  new/page.tsx                    — 新建页面
  [id]/edit/page.tsx              — 编辑页面

web/src/app/admin/reusable-blocks/
  page.tsx                        — 可复用块列表

web/src/components/admin/
  block-editor/
    index.tsx                     — 块编辑器主组件(排序 + 添加 + 删除)
    block-wrapper.tsx             — 单个块的外壳(拖拽手柄 + 删除 + 排序按钮)
    add-block-menu.tsx            — 添加块的弹出菜单
    preview/
      index.tsx                   — 块预览渲染器
  blocks/
    hero-editor.tsx               — Hero 块编辑表单
    richtext-editor.tsx           — 富文本块编辑(复用 MarkdownEditor
    image-editor.tsx              — 图片块编辑(复用媒体上传)
    gallery-editor.tsx            — 画廊块编辑
    video-editor.tsx              — 视频块编辑
    cta-editor.tsx                — CTA 块编辑
    testimonial-editor.tsx        — 评价块编辑
    faq-editor.tsx                — FAQ 块编辑
    stats-editor.tsx              — 统计块编辑
    timeline-editor.tsx           — 时间线块编辑
    team-editor.tsx               — 团队块编辑
    pricing-editor.tsx            — 价格块编辑
    contact-form-editor.tsx       — 联系表单块编辑
    map-editor.tsx                — 地图块编辑
    code-editor.tsx               — 代码块编辑
    quote-editor.tsx              — 引用块编辑
    divider-editor.tsx            — 分隔线块编辑
    spacer-editor.tsx             — 间距块编辑
    columns-editor.tsx            — 多栏布局块编辑
    html-editor.tsx               — 自定义 HTML 块编辑
    reusable-editor.tsx           — 可复用块引用编辑

公开页面

web/src/app/(public)/pages/
  [slug]/page.tsx                 — 公开页面渲染

web/src/components/public/
  block-renderer.tsx              — 块渲染调度器(根据 type 渲染对应组件)
  blocks/
    hero-block.tsx                — Hero 渲染
    richtext-block.tsx            — 富文本渲染
    image-block.tsx               — 图片渲染
    gallery-block.tsx             — 画廊渲染
    video-block.tsx               — 视频渲染
    cta-block.tsx                 — CTA 渲染
    testimonial-block.tsx         — 评价渲染
    faq-block.tsx                 — FAQ 渲染
    stats-block.tsx               — 统计渲染
    timeline-block.tsx            — 时间线渲染
    team-block.tsx                — 团队渲染
    pricing-block.tsx             — 价格渲染
    contact-form-block.tsx        — 联系表单渲染
    map-block.tsx                 — 地图渲染
    code-block.tsx                — 代码渲染
    quote-block.tsx               — 引用渲染
    divider-block.tsx             — 分隔线渲染
    spacer-block.tsx              — 间距渲染
    columns-block.tsx             — 多栏布局渲染(递归)
    html-block.tsx                — 自定义 HTML 渲染

块编辑器交互

┌─ Page Editor ───────────────────────────────────────┐
│                                                     │
│  Title:  [关于我们                              ]   │
│  Slug:   [about-us                             ]   │
│  Template: [default ▾]    Status: [draft ▾]         │
│                                                     │
│  ── SEO ─────────────────────────────────────────── │
│  Meta Title: [关于我们 - XX公司                ]    │
│  Meta Desc:  [专业XX服务,值得信赖...            ]  │
│  OG Image:   [上传图片]                             │
│                                                     │
│  ── Blocks ──────────────────────────────────────── │
│                                                     │
│  ┌─ Hero ──────────────────────────── [↑][↓][✕] ┐  │
│  │  Title:    [欢迎来到我们的公司]              │  │
│  │  Subtitle: [专业服务,值得信赖]              │  │
│  │  BG Image: [📷 hero-bg.jpg]                 │  │
│  │  CTA:      [了解更多] → [/contact]           │  │
│  │  Height:   [lg ▾]  Overlay: [✓]              │  │
│  └──────────────────────────────────────────────┘  │
│                                                     │
│  ┌─ Richtext ──────────────────────── [↑][↓][✕] ┐  │
│  │  [富文本编辑器 / Markdown 编辑器...]         │  │
│  └──────────────────────────────────────────────┘  │
│                                                     │
│  ┌─ Stats ─────────────────────────── [↑][↓][✕] ┐  │
│  │  [10年经验] [500+客户] [99%满意] [24/7支持]  │  │
│  └──────────────────────────────────────────────┘  │
│                                                     │
│  ┌─ Team ──────────────────────────── [↑][↓][✕] ┐  │
│  │  👤 张三 CEO     👤 李四 CTO                  │  │
│  │  👤 王五 设计    [+ 添加成员]                 │  │
│  └──────────────────────────────────────────────┘  │
│                                                     │
│  [+ 添加块 ▾]                                       │
│  ┌─────────────────────────────────────────┐        │
│  │  🖼 Hero      📝 文本      📷 图片      │        │
│  │  🎬 视频      📢 CTA       ⭐ 评价      │        │
│  │  ❓ FAQ       📊 统计      ⏱ 时间线     │        │
│  │  👥 团队      💰 价格      📮 表单      │        │
│  │  📍 地图      📋 代码      💬 引用      │        │
│  │  ➖ 分隔线    ↕️ 间距      📦 栏目布局   │        │
│  │  🔄 可复用    🌐 自定义HTML              │        │
│  └─────────────────────────────────────────┘        │
│                                                     │
│  ── 侧栏 ─────────────────────────────────────────  │
│  Parent:  [无(顶级页面) ▾]                        │
│  Sort:    [0]                                       │
│  Cover:   [上传封面图]                               │
│                                                     │
│            [保存草稿]    [发布]                       │
└─────────────────────────────────────────────────────┘

公开页面渲染流程

用户访问 /about
       ↓
Next.js [slug] page.tsx
       ↓
GET /api/v1/pages/about
       ↓
返回 { title, template, blocks: [...], meta_title, ... }
       ↓
设置 <title> meta 信息
       ↓
BlockRenderer 遍历 blocks 数组
       ↓
根据 block.type 匹配渲染组件
       ↓
若 type = "reusable" → 请求 GET /api/v1/admin/reusable-blocks/{ref_id}
       ↓
若 type = "columns" → 递归渲染子块
       ↓
组合渲染完整页面

七、20 种块类型 vs 企业网站场景

块类型 典型页面 用途
Hero 首页、产品页 大图横幅,核心标语 + CTA 按钮
Richtext 所有页面 通用富文本内容
Image 产品介绍、案例 单张图片展示
Gallery 作品集、案例 图片画廊/灯箱效果
Video 首页、产品演示 视频 embed
Cta 首页、服务页 "立即咨询""免费试用"
Testimonial 首页、关于我们 客户评价/好评
Faq FAQ 页 常见问题折叠面板
Stats 首页、关于我们 数据亮点(年限、客户数)
Timeline 关于我们 公司发展历程
Team 关于我们 团队成员卡片
Pricing 产品/服务 定价方案对比
ContactForm 联系我们 表单提交
Map 联系我们 公司地址
Code 技术文档 代码示例
Quote 关于我们、首页 客户语录
Divider 任意页面 视觉分隔
Spacer 任意页面 间距控制
Columns 任意页面 多栏布局(图文并排等)
Html 任意页面 第三方组件嵌入
Reusable 任意页面 跨页面复用组件(页脚公告等)

八、后端文件结构

src/
  models/
    page.rs                  — Page / PageBlock / 辅助结构体 + 查询函数
  services/
    page.rs                  — 页面业务逻辑(slug生成、状态管理、block校验)
  handlers/
    page.rs                  — API handlersCRUD + 状态变更 + 排序)
  server.rs                  — 注册路由(追加 /pages 相关路由)

路由注册

// src/server.rs — 在 posts 路由之后添加

.route("/pages", get(page::list).post(page::create))
.route("/pages/sitemap", get(page::sitemap))
.route("/pages/{slug}", get(page::get_by_slug))
.route("/admin/pages", get(page::admin_list))
.route("/admin/pages/{id}", get(page::admin_get).put(page::update).delete(page::delete))
.route("/admin/pages/{id}/status", put(page::update_status))
.route("/admin/pages/reorder", put(page::reorder))
.route("/admin/reusable-blocks", get(page::list_reusable).post(page::create_reusable))
.route("/admin/reusable-blocks/{id}", put(page::update_reusable).delete(page::delete_reusable))
.route("/admin/block-templates", get(page::block_templates))

九、实现步骤

步骤 内容 预估
1 Migration 023_create_pages.sql 0.5 天
2 src/models/page.rs — 模型 + 查询函数 1 天
3 src/services/page.rs — 业务逻辑 0.5 天
4 src/handlers/page.rs — API handlers 0.5 天
5 src/server.rs — 注册路由 0.5 天
6 后端测试(clippy + fmt + test 0.5 天
7 前端 admin 页面编辑器(块编辑器 + 各块编辑组件) 2-3 天
8 前端公开页面渲染器(各块渲染组件) 1 天
9 i18n + admin sidebar 更新 0.5 天
总计 约 7 天

十、扩展方向(后续迭代)

  • 主题系统:不同 template 对应不同的块渲染样式
  • 块模板市场:预设行业模板(科技公司/餐饮/教育...)
  • 拖拽排序:前端块编辑器支持 drag & drop
  • 块动画:进入动画(fade-in / slide-up)配置
  • A/B 测试:同一页面多版本 blocks 对比
  • 定时发布scheduled_at 字段 + Cron 自动切换状态
  • 多语言页面:关联不同 locale 的 page 记录