投稿模板

模板本质上是一个 HTML 文件,加上一份参数声明和可选的数据表声明。用户从模板创建作品时,PlayPage 会把 HTML 里的占位符替换为用户填写的参数。

建议先点“套用静态页面示例”或“套用留言板示例”,再按需要修改。

1. 模板基本信息

2. 参数声明

参数会出现在创建作品页面。比如声明了 themeColor,HTML 里写 {{themeColor}},创建时就会被替换成用户选择的颜色。

参数 1
参数 2
参数 3
高级:直接编辑参数声明 JSON

3. 数据表声明

只有留言板、论坛、排行榜、云存档这类需要在线保存数据的模板才需要声明数据表。静态页面保持空数组 [] 即可。

数据表声明不是“示例数据”,而是告诉 PlayPage:用户从这个模板创建作品时,要自动为这个作品创建哪些数据集合。
数据集合声明怎么写

整体必须是数组。数组里的每一项代表一个数据集合。集合创建后,模板 HTML 里的 JS 可以通过互动 API 读写它。

  • name:集合名,只建议用英文、数字、下划线,例如 messagespostsscores
  • permissions.publicRead:访客是否可以读取。留言板、论坛、排行榜通常设为 true
  • permissions.publicWrite:访客是否可以新增或修改。允许发帖、评论、留言时必须设为 true
  • fields:字段声明数组。每个字段描述一项业务数据,比如标题、正文、作者、分数。

字段支持常用类型:string 短文本、text 长文本、number 数字、boolean 布尔值。required 表示写入时必须提供,isList 表示这个字段是否是数组,普通字段一般写 false

例子:论坛通常需要两个集合。posts 保存帖子,comments 保存回复;回复通过 postId 关联到帖子 ID。

[
  {
    "name": "posts",
    "permissions": { "publicRead": true, "publicWrite": true },
    "fields": [
      { "name": "title", "type": "string", "required": true, "isList": false },
      { "name": "content", "type": "text", "required": true, "isList": false },
      { "name": "author", "type": "string", "required": true, "isList": false }
    ]
  },
  {
    "name": "comments",
    "permissions": { "publicRead": true, "publicWrite": true },
    "fields": [
      { "name": "postId", "type": "string", "required": true, "isList": false },
      { "name": "content", "type": "text", "required": true, "isList": false },
      { "name": "author", "type": "string", "required": true, "isList": false }
    ]
  }
]

4. 模板源码

源码方式

模板开发规范

必须知道的规则

  1. 模板必须是完整 HTML 页面,建议包含 <!doctype html><html lang="zh-CN"><meta charset="utf-8">
  2. 参数占位符格式固定是 {{参数名}},参数名要和参数声明里的 name 完全一致。
  3. 颜色参数请使用 type: "color",默认值必须是 #RRGGBB,例如 #2563eb
  4. 需要互动数据时,勾选“需要互动功能”,并声明要创建的数据表。
  5. 投稿不会执行任何后端代码;模板里的 JS 只会在用户作品页面的浏览器中运行。

系统占位符

预览机制

互动请求最小写法

const API_BASE = "{{PLAYPAGE_API_BASE}}";
const PROJECT_KEY = "{{PLAYPAGE_PUBLIC_KEY}}";
const PREVIEW = "{{PLAYPAGE_PREVIEW}}" === "true";

async function api(path, options = {}) {
  if (PREVIEW) {
    return previewApi(path, options);
  }

  const response = await fetch(API_BASE + path, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      "X-Project-Key": PROJECT_KEY,
      ...(options.headers || {})
    }
  });
  const data = await response.json().catch(() => ({}));
  if (!response.ok) throw new Error(data.error || "请求失败");
  return data;
}

const previewMessages = [
  { id: "preview-1", data: { nickname: "预览用户", content: "这是预览数据,不会保存。" }, createdAt: new Date().toISOString() }
];

async function previewApi(path, options = {}) {
  if (path === "/collections/messages/records" && (!options.method || options.method === "GET")) {
    return { items: previewMessages };
  }
  if (path === "/collections/messages/records" && options.method === "POST") {
    const body = JSON.parse(options.body || "{}");
    const item = { id: "preview-" + (previewMessages.length + 1), data: body.data || {}, createdAt: new Date().toISOString() };
    previewMessages.push(item);
    return item;
  }
  throw new Error("预览模式不支持这个操作");
}