模板本质上是一个 HTML 文件,加上一份参数声明和可选的数据表声明。用户从模板创建作品时,PlayPage 会把 HTML 里的占位符替换为用户填写的参数。
参数会出现在创建作品页面。比如声明了 themeColor,HTML 里写 {{themeColor}},创建时就会被替换成用户选择的颜色。
只有留言板、论坛、排行榜、云存档这类需要在线保存数据的模板才需要声明数据表。静态页面保持空数组 [] 即可。
整体必须是数组。数组里的每一项代表一个数据集合。集合创建后,模板 HTML 里的 JS 可以通过互动 API 读写它。
name:集合名,只建议用英文、数字、下划线,例如 messages、posts、scores。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 }
]
}
]<!doctype html>、<html lang="zh-CN">、<meta charset="utf-8">。{{参数名}},参数名要和参数声明里的 name 完全一致。type: "color",默认值必须是 #RRGGBB,例如 #2563eb。{{PLAYPAGE_API_BASE}}:当前作品互动 API 基础地址,例如 /api/v1/public/projects/作品ID。{{PLAYPAGE_PUBLIC_KEY}}:当前作品公开互动密钥,用在 X-Project-Key 请求头。{{PLAYPAGE_PREVIEW}}:预览时是 true,正式发布时是 false。{{PROJECT_ID}}、{{PROJECT_NAME}}:当前作品 ID 和名称。{{PLAYPAGE_PREVIEW}} 会被替换成 true;用户真正创建作品后会替换成 false。{{PLAYPAGE_API_BASE}} 和 {{PLAYPAGE_PUBLIC_KEY}} 会替换成演示值,不能当成真实数据接口使用。api 和 previewApi。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("预览模式不支持这个操作");
}