文档 / 多文件待办
多文件待办
这是可以直接运行的完整多文件应用。入口负责导航,lib/todo.js 负责数据访问,两个页面分别处理列表和详情。列表支持每页 10 条的分页。
运行预览
点击事项可以进入详情,修改状态后返回列表。预览使用相同的多文件加载器与 platform.ui,数据来自独立的模拟数据库。
代码标签中的 JSON 是完整应用包。安装时,将其中的 files 作为 POST /api/apps 的 files,另行提供 name 与实际 table_ids。
文件说明
app.json
{
"name": "待办",
"entry": "index.js"
}index.js
import { createStore } from "./lib/todo.js";
import { showHome } from "./pages/home.js";
import { showDetail } from "./pages/detail.js";
export default async function render(root, platform) {
const ui = platform.ui;
root.replaceChildren(ui.loading());
try {
const store = await createStore(platform);
const nav = {
home: () => showHome(root, ui, store, nav),
detail: (id) => showDetail(root, ui, store, nav, id),
};
await nav.home();
} catch (error) {
root.replaceChildren(ui.empty(error.message));
}
}lib/todo.js
export async function createStore(platform) {
const dataset = await platform.data.get("todo");
const data = platform.data;
return {
list: (offset) => data.query(dataset.id, { limit: 10, offset }),
count: () => data.count(dataset.id),
get: (id) => data.getRecord(dataset.id, id),
add: (title) => data.insert(dataset.id, { title, done: false }),
toggle: (row) => data.update(dataset.id, row.id, { done: !row.done }),
};
}pages/home.js
export async function showHome(root, ui, store, nav, offset = 0) {
root.replaceChildren(ui.loading());
try {
const [rows, total] = await Promise.all([store.list(offset), store.count()]);
const input = ui.input({ placeholder: "新增待办" });
input.setAttribute("aria-label", "新增待办");
const add = ui.button("添加", async () => {
const title = input.value.trim();
if (!title) return ui.toast("请填写事项名称");
add.disabled = true;
try {
await store.add(title);
await nav.home();
} catch (error) {
ui.toast(error.message);
add.disabled = false;
}
});
const items = rows.map((row) => {
const item = ui.row(row.title, row.done ? "已完成" : "未完成");
item.onclick = () => { void nav.detail(row.id); };
return item;
});
const previous = ui.button("上一页", () => { void showHome(root, ui, store, nav, offset - 10); }, "secondary");
previous.disabled = offset === 0;
const next = ui.button("下一页", () => { void showHome(root, ui, store, nav, offset + 10); }, "secondary");
next.disabled = offset + rows.length >= total;
root.replaceChildren(ui.page([
ui.header("待办", "共 " + total + " 项"),
ui.card(ui.stack([ui.field(input), add])),
ui.card(ui.stack([
items.length ? ui.list(items) : ui.empty("还没有事项"),
previous, next,
])),
]));
} catch (error) {
root.replaceChildren(ui.card([ui.hint(error.message), ui.button("重试", () => { void nav.home(); })]));
}
}pages/detail.js
export async function showDetail(root, ui, store, nav, id) {
root.replaceChildren(ui.loading());
try {
const row = await store.get(id);
const toggle = ui.button(row.done ? "改为未完成" : "标记完成", async () => {
toggle.disabled = true;
try {
await store.toggle(row);
await nav.detail(id);
} catch (error) {
ui.toast(error.message);
toggle.disabled = false;
}
});
root.replaceChildren(ui.page([ui.card(ui.stack([
ui.title(row.title),
ui.tag(row.done ? "已完成" : "未完成"),
toggle,
ui.button("返回列表", () => { void nav.home(); }, "secondary"),
]))]));
} catch (error) {
root.replaceChildren(ui.card([
ui.hint(error.message),
ui.button("返回列表", () => { void nav.home(); }, "secondary"),
]));
}
}使用自己的数据
安装前创建 title: string、done: boolean 两个字段的数据集,并确认 slug 为 todo。否则修改 lib/todo.js 中的 slug,再将数据集 ID 绑定到应用。
示例的 nav.home 和 nav.detail 是应用自己定义的函数,并非宿主 API。它说明了如何用多文件组织页面,不依赖尚未提供的页面路由器。