# 页面模板与示例

仓库的 `examples/apps/templates/` 保存可以重新打包的多文件应用。它们不是固定页面，也不是产品能力上限；使用前先阅读 [UI 设计原则](/doc/styles/design-principles) 和 [应用设计清单](/doc/styles/checklist)，再根据数据结构和用户任务修改。

## 列表与详情

列表承担浏览和选择，详情承担完整阅读与编辑。不要把所有字段、所有操作和编辑表单都放进列表行。

```js preview height=360
export default function render(root, platform) {
  const ui = platform.ui;
  const detail = ui.hint("选择一条记录后，这里显示详情和返回路径。");
  const list = ui.list(["早餐", "散步", "阅读"].map((name) => {
    const row = ui.row(name, ui.tag("待处理"));
    row.onclick = () => { detail.textContent = `正在查看：${name} · 可以继续编辑或返回列表`; };
    return row;
  }));
  root.replaceChildren(ui.page([
    ui.header("今日事项", "3 项"),
    ui.card(ui.stack([ui.section("列表", [list]), ui.divider(), detail])),
  ]));
}
```

仓库模板的多文件预览结构如下；完整源码保存在 `examples/apps/templates/reading/`，可以用同样的结构替换为自己的数据。

```js preview bundle height=300
{
  "entry": "index.js",
  "files": {
    "app.json": "{\"name\":\"阅读记录\",\"entry\":\"index.js\"}",
    "index.js": "import { show } from \"./pages/home.js\"; export default function render(root, platform) { show(root, platform.ui); }",
    "pages/home.js": "export function show(root, ui) { root.replaceChildren(ui.page([ui.header(\"阅读记录\", \"最近打开\"), ui.card([ui.section(\"正在阅读\", [ui.list([ui.row(\"设计的日常\", \"阅读中\"), ui.row(\"时间简史\", \"已读 42%\")])])])])); }"
  }
}
```

## 统计面板

统计只回答一个范围内的重要问题。数字需要说明口径，不要把不完整分页的结果说成全量统计。

```js preview height=300
export default function render(root, platform) {
  const ui = platform.ui;
  root.replaceChildren(ui.page([
    ui.header("本周概览", "2026 年第 42 周"),
    ui.card(ui.stack([
      ui.section("完成情况", [ui.stat("已经完成", "18"), ui.stat("待处理", "4")]),
      ui.hint("统计范围：已经读取的本周记录。"),
    ]),
  ]));
}
```

仓库中的 `examples/apps/templates/weight/` 是同一类统计面板的多文件版本，趋势图只使用已经读取的记录，并明确展示统计范围。

```js preview bundle height=300
{
  "entry": "index.js",
  "files": {
    "app.json": "{\"name\":\"体重记录\",\"entry\":\"index.js\"}",
    "index.js": "import { show } from \"./pages/home.js\"; export default function render(root, platform) { show(root, platform.ui); }",
    "pages/home.js": "export function show(root, ui) { root.replaceChildren(ui.page([ui.header(\"体重记录\", \"最近 30 次\"), ui.card(ui.stack([ui.stat(\"最近一次\", \"63.5 kg\"), ui.chart({ type: \"line\", data: [64.1, 63.8, 63.5], label: \"体重变化趋势\" })]))])); }"
  }
}
```

## 记账录入

录入表单把名称、金额和分类放在一起，成功后保留当前范围并明确说明发生了什么。金额需要在写入前转换为 number 并处理无效值。

```js preview height=380
export default function render(root, platform) {
  const ui = platform.ui;
  const title = ui.input({ placeholder: "例如 午餐" });
  const amount = ui.input({ placeholder: "金额", type: "number", step: "0.01" });
  title.setAttribute("aria-label", "账目名称");
  amount.setAttribute("aria-label", "金额");
  const status = ui.hint("还没有提交这笔账目。");
  const save = ui.button("添加账目", () => {
    if (!title.value.trim() || !Number.isFinite(Number(amount.value))) {
      status.textContent = "请填写名称和有效金额。";
      return;
    }
    status.textContent = `已经添加：${title.value.trim()}，¥${Number(amount.value).toFixed(2)}`;
    ui.toast("账目已经保存");
  });
  root.replaceChildren(ui.page([
    ui.header("添加账目", "日常支出"),
    ui.card(ui.stack([ui.field(title), ui.field(amount), save, status])),
  ]));
}
```

仓库中的 `examples/apps/templates/ledger/` 是可重新打包的记账多文件包，页面把录入和最近账目分成两个逻辑区域。

```js preview bundle height=340
{
  "entry": "index.js",
  "files": {
    "app.json": "{\"name\":\"日常记账\",\"entry\":\"index.js\"}",
    "index.js": "import { show } from \"./pages/home.js\"; export default function render(root, platform) { show(root, platform.ui); }",
    "pages/home.js": "export function show(root, ui) { const name = ui.input({ placeholder: \"例如 午餐\" }); name.setAttribute(\"aria-label\", \"账目名称\"); const amount = ui.input({ type: \"number\", placeholder: \"金额\" }); amount.setAttribute(\"aria-label\", \"金额\"); root.replaceChildren(ui.page([ui.header(\"日常记账\", \"最近账目\"), ui.card(ui.stack([ui.section(\"本次查看\", [ui.stat(\"合计\", \"¥128.00\")]), ui.section(\"添加一笔\", [ui.field(name), ui.field(amount), ui.datePicker({}), ui.button(\"添加账目\", () => ui.toast(\"已经保存\"))])]))])); }"
  }
}
```

## 习惯与日程

习惯适合表达今天的完成状态，日程适合按照时间或日期分组。两者都应提供空状态和正在读取状态，并且让用户知道当前查看的日期范围。

```js preview bundle height=360
{
  "entry": "index.js",
  "files": {
    "app.json": "{\"name\":\"习惯与日程预览\",\"entry\":\"index.js\"}",
    "index.js": "import { items } from \"./pages/today.js\";\nexport default function render(root, platform) { const ui = platform.ui; root.replaceChildren(ui.page([ui.header(\"今天\", \"10 月 14 日\"), ui.card([ui.section(\"习惯\", [ui.list([ui.row(\"喝水\", \"已完成\"), ui.row(\"拉伸\", \"还没有完成\")])]), ui.divider(), ui.section(\"安排\", [items(ui)])])])); }",
    "pages/today.js": "export function items(ui) { return ui.list([ui.row(\"09:30 例会\", \"工作\"), ui.row(\"18:00 散步\", \"生活\")]); }"
  }
}
```

仓库中的 `examples/apps/templates/habits/` 是每日状态的多文件版本；状态保存日期，而不是永久修改一个布尔值。

```js preview bundle height=340
{
  "entry": "index.js",
  "files": {
    "app.json": "{\"name\":\"习惯打卡\",\"entry\":\"index.js\"}",
    "index.js": "import { show } from \"./pages/home.js\"; export default function render(root, platform) { show(root, platform.ui); }",
    "pages/home.js": "export function show(root, ui) { root.replaceChildren(ui.page([ui.header(\"习惯打卡\", \"今天\"), ui.card(ui.stack([ui.stat(\"今日完成\", \"2/3\"), ui.hint(\"点击一项切换今天的完成状态。\")])), ui.card([ui.list([ui.row(\"喝水\", \"今天已完成\"), ui.row(\"拉伸\", \"今天还没有完成\")])])])); }"
  }
}
```

## 分组表单

较长表单按真实主题分组；保存按钮只承担一个主要动作，失败时不清空用户已经填写的内容。

```js preview height=390
export default function render(root, platform) {
  const ui = platform.ui;
  const name = ui.input({ placeholder: "姓名" });
  const note = ui.textarea({ placeholder: "补充说明", rows: 4 });
  name.setAttribute("aria-label", "姓名");
  note.setAttribute("aria-label", "补充说明");
  const result = ui.hint("尚未保存修改。");
  root.replaceChildren(ui.page([
    ui.header("我的资料", "只填写愿意记录的内容"),
    ui.card(ui.stack([
      ui.section("基本信息", [ui.field(name)]),
      ui.section("补充说明", [ui.field(note)]),
      ui.button("保存修改", () => { result.textContent = "已经保存修改。"; }),
      result,
    ])),
  ]));
}
```

仓库中的 `examples/apps/templates/profile/` 是分组表单的多文件版本；表单失败时保留输入内容，成功后用轻提示说明结果。

```js preview bundle height=340
{
  "entry": "index.js",
  "files": {
    "app.json": "{\"name\":\"我的资料\",\"entry\":\"index.js\"}",
    "index.js": "import { show } from \"./pages/home.js\"; export default function render(root, platform) { show(root, platform.ui); }",
    "pages/home.js": "export function show(root, ui) { const name = ui.input({ placeholder: \"姓名\" }); name.setAttribute(\"aria-label\", \"姓名\"); const note = ui.textarea({ placeholder: \"补充说明\" }); note.setAttribute(\"aria-label\", \"补充说明\"); root.replaceChildren(ui.page([ui.header(\"我的资料\", \"只填写愿意记录的内容\"), ui.card(ui.stack([ui.section(\"基本信息\", [ui.field(name)]), ui.section(\"补充说明\", [ui.field(note)]), ui.button(\"保存修改\", () => ui.toast(\"已经保存修改\"))]))])); }"
  }
}
```

仓库中的模板可以使用下面的命令重新打包：

```sh
node examples/apps/scripts/package.mjs ledger
node examples/apps/scripts/package.mjs habits
node examples/apps/scripts/package.mjs planner
node examples/apps/scripts/package.mjs profile
```
