mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-23 16:02:25 +00:00
26 KiB
26 KiB
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 union(serde 的 #[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 handlers(CRUD + 状态变更 + 排序)
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 记录