# 多文件待办

这是可以直接运行的完整多文件应用。入口负责导航，`lib/todo.js` 负责数据访问，两个页面分别处理列表和详情。列表支持每页 10 条的分页。

## 运行预览

点击事项可以进入详情，修改状态后返回列表。预览使用相同的多文件加载器与 `platform.ui`，数据来自独立的模拟数据库。

代码标签中的 JSON 是完整应用包。安装时，将其中的 `files` 作为 `POST /api/apps` 的 `files`，另行提供 `name` 与实际 `table_ids`。

```json preview bundle height=590
{
  "entry": "index.js",
  "files": {
    "app.json": "{\n  \"name\": \"待办\",\n  \"entry\": \"index.js\"\n}",
    "index.js": "import { createStore } from \"./lib/todo.js\";\nimport { showHome } from \"./pages/home.js\";\nimport { showDetail } from \"./pages/detail.js\";\n\nexport default async function render(root, platform) {\n  const ui = platform.ui;\n  root.replaceChildren(ui.loading());\n  try {\n    const store = await createStore(platform);\n    const nav = {\n      home: () => showHome(root, ui, store, nav),\n      detail: (id) => showDetail(root, ui, store, nav, id),\n    };\n    await nav.home();\n  } catch (error) {\n    root.replaceChildren(ui.empty(error.message));\n  }\n}",
    "lib/todo.js": "export async function createStore(platform) {\n  const dataset = await platform.data.get(\"todo\");\n  const data = platform.data;\n  return {\n    list: (offset) => data.query(dataset.id, { limit: 10, offset }),\n    count: () => data.count(dataset.id),\n    get: (id) => data.getRecord(dataset.id, id),\n    add: (title) => data.insert(dataset.id, { title, done: false }),\n    toggle: (row) => data.update(dataset.id, row.id, { done: !row.done }),\n  };\n}",
    "pages/home.js": "export async function showHome(root, ui, store, nav, offset = 0) {\n  root.replaceChildren(ui.loading());\n  try {\n    const [rows, total] = await Promise.all([store.list(offset), store.count()]);\n    const input = ui.input({ placeholder: \"新增待办\" });\n    input.setAttribute(\"aria-label\", \"新增待办\");\n    const add = ui.button(\"添加\", async () => {\n      const title = input.value.trim();\n      if (!title) return ui.toast(\"请填写事项名称\");\n      add.disabled = true;\n      try {\n        await store.add(title);\n        await nav.home();\n      } catch (error) {\n        ui.toast(error.message);\n        add.disabled = false;\n      }\n    });\n    const items = rows.map((row) => {\n      const item = ui.row(row.title, row.done ? \"已完成\" : \"未完成\");\n      item.onclick = () => { void nav.detail(row.id); };\n      return item;\n    });\n    const previous = ui.button(\"上一页\", () => { void showHome(root, ui, store, nav, offset - 10); }, \"secondary\");\n    previous.disabled = offset === 0;\n    const next = ui.button(\"下一页\", () => { void showHome(root, ui, store, nav, offset + 10); }, \"secondary\");\n    next.disabled = offset + rows.length >= total;\n    root.replaceChildren(ui.page([\n      ui.header(\"待办\", \"共 \" + total + \" 项\"),\n      ui.card(ui.stack([ui.field(input), add])),\n      ui.card(ui.stack([\n        items.length ? ui.list(items) : ui.empty(\"还没有事项\"),\n        previous, next,\n      ])),\n    ]));\n  } catch (error) {\n    root.replaceChildren(ui.card([ui.hint(error.message), ui.button(\"重试\", () => { void nav.home(); })]));\n  }\n}",
    "pages/detail.js": "export async function showDetail(root, ui, store, nav, id) {\n  root.replaceChildren(ui.loading());\n  try {\n    const row = await store.get(id);\n    const toggle = ui.button(row.done ? \"改为未完成\" : \"标记完成\", async () => {\n      toggle.disabled = true;\n      try {\n        await store.toggle(row);\n        await nav.detail(id);\n      } catch (error) {\n        ui.toast(error.message);\n        toggle.disabled = false;\n      }\n    });\n    root.replaceChildren(ui.page([ui.card(ui.stack([\n      ui.title(row.title),\n      ui.tag(row.done ? \"已完成\" : \"未完成\"),\n      toggle,\n      ui.button(\"返回列表\", () => { void nav.home(); }, \"secondary\"),\n    ]))]));\n  } catch (error) {\n    root.replaceChildren(ui.card([\n      ui.hint(error.message),\n      ui.button(\"返回列表\", () => { void nav.home(); }, \"secondary\"),\n    ]));\n  }\n}"
  }
}
```

## 文件说明

### app.json

```json file="app.json"
{
  "name": "待办",
  "entry": "index.js"
}
```

### index.js

```js file="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

```js file="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

```js file="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

```js file="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。它说明了如何用多文件组织页面，不依赖尚未提供的页面路由器。
