---
url: /docs/guide/uniapp/page-api-reference.md
---
# page-api-reference —— 前端页面 × 后端 API 反向索引（v1）

> 沐禾健康 C 端 uniapp（qingmu2026/qm-frontend `api` 分支）所有**已上线**页面调用的后端 API 总览。
> **入口文档**：找页面，看它调什么；找 API，看谁在用。
> **写法契约**：
>
> * 字段表 / 错误码 / curl 一律见 [前端联调 API 手册](/api-docs/v1/frontend/)
> * 调用读法 / 失败前端动作 见 [uniapp/api/\*](./api/)
> * 页面职责 / 关键逻辑 见 [uniapp/pages/\*](./pages/)
> * **本文档聚合三者** —— 按 uniapp 页面分类，给出「该页调用哪些 API + 关键参数 + 响应读法」一行总结

## 总览

| 维度 | 数 |
|---|---|
| **uniapp 页面** | **36**（34 已上线 + 2 待建：city-select / rawfood-ranking） |
| **后端 API 端点** | **54**（30 主干 REST/Action + 19 新接 + 5 city/ranking） |
| **按页面分类组** | 7 大类（登录启动 / 首页统计 / 详情 / 表单 / 个人中心 / 跑步 / 🆕 城市与生吃） |
| **最后更新** | 2026-09-29（city + rawfood 上线） |

> ⚠️ **页面 ↔ API 矩阵**见 [`tests/reports/contract-matrix.json`](https://github.com/cnb-tools/contract-matrix)（CI 校验源）。
> **通用约定 G1-G7**（统一包络 / X-Env / 鉴权三档 / rotate 单飞）见 [手册 · 通用约定](/api-docs/v1/frontend/#通用约定-7-条-·-所有端点共用)。

***

## 📑 按类别跳转

| 类别 | 页面数 | 锚点 |
|---|---|---|
| 🚪 **登录与启动** | 5 | [→ §1](#1-登录与启动) |
| 🏠 **首页与统计** | 2 | [→ §2](#2-首页与统计) |
| 📊 **详情页**（14 子页） | 14 | [→ §3](#3-详情页) |
| 📝 **表单** | 1 | [→ §4](#4-表单) |
| 👤 **个人中心** | 7 | [→ §5](#5-个人中心) |
| 🏃 **跑步会话** | 3 | [→ §6](#6-跑步会话) |
| 🆕 **选城市 + 生吃榜**（待建） | 2 | [→ §7](#7-选城市--生吃榜-2026-09-29-新增) |

***

## §1 登录与启动

### `pages/login/index.vue` — 手机号登录（513 行 · Vue3 setup）

| 步骤 | API 调用 | 关键 payload | 响应读法 | 错误码速查 |
|---|---|---|---|---|
| 进入页面 | `POST /api/v1/auth/oauth {action:"captcha"}` | `{}` | `res.data.png` → `<image :src>` 渲染 | `40101005` 限流 |
| 点「获取验证码」 | `POST /api/v1/auth/oauth {action:"sms-send"}` | `phone/captchaId/captchaCode/clientId` | `res.data.ok` | `40101003/04/05/06` |
| 点「登录」 | `POST /api/v1/auth/oauth {action:"sms-login"}` | `phone/smsCode/agreementAccepted` + 可选 `nickname/deviceInfo` | `res.data.tokens.{access,refresh}` + `res.data.user` | `40101001/08/13` |
| 失败回退 | `POST /api/v1/auth/oauth {action:"sms-verify"}`（分步表单可选） | `phone/smsCode/clientId` | `res.data.ok` | `40101001/03` |

> ⚠️ **铁律**：图形码**无论成败即消费** —— 任何失败分支重试前必须重新 `captcha`（[手册 §captcha 渲染三定律](/api-docs/v1/frontend/login#captcha)）。

**调用详情**：[`guide/uniapp/api/oauth.md`](./api/oauth) · [契约字段表](/api-docs/v1/frontend/login)

***

### `pages/agreement/index.vue` — 用户协议 + 隐私协议

| API 调用 | 用途 |
|---|---|
| `GET /api/v1/content/agreements` | 列协议版本（决定 UI 是否要更新） |
| `GET /api/v1/content/agreements/{type}` | 拉单篇协议正文（`userAgreement` / `privacyPolicy`） |

**调用详情**：[`guide/uniapp/api/agreement.md`](./api/agreement)

***

### `pages/splash/index.vue` — 开机信息屏

| API 调用 | 关键字段 | 用途 |
|---|---|---|
| `GET /api/v1/splash-screen` | — | 拉开机图/视频 + 天气 + AI 文生图素材 |
| `POST /api/v1/splash-screen/track` | `eventType/eventPayload` | 上报曝光/点击/完成埋点 |

> ⚠️ **不要**把 `splash.city`（广告投放回传）当成「当前城市」—— 见 [§7.1 city-select 跳引导页](/api-docs/v1/city/)。

**调用详情**：[`guide/uniapp/api/splash.md`](./api/splash)

***

### `pages/webview/index.vue` — 内嵌 WebView

```text
无后端调用 —— 纯 WebView 容器，加载 splash 拉的 URL 或分享目标页
```

***

### `pages/ai/index.vue` — AI 智能助手

```text
无 Action 调用（页面直调文 → 未收录到 src/api/*.ts —— 待接入）
```

***

## §2 首页与统计

### `pages/index/index.vue` — 首页（健康仪表盘）

| 调用时机 | API | 用途 |
|---|---|---|
| 启动 / 下拉刷新 | `POST /api/v1/stats {action:"getTodayOverview"}` | 今日概览（热量/步数/心率/睡眠） |
| 启动 / 下拉刷新 | `POST /api/v1/device {action:"myTodayHealth"}` | 设备今日健康摘要（与 stats 互补） |
| onShow | `POST /api/v1/city {action:"getCurrentCity"}` | 🆕 当前城市（**40431003 跳 city-select**） |

> ⚠️ **`getTodayOverview`** —— `profileComplete:false` 时返 `null` **不编数**（V1 老文档已错）；**`budgetKcal` / `remainingKcal` 可空**；`basis.strengthMinutes` 反映真实力量分钟数。
>
> ⚠️ **`myTodayHealth`** —— 静息心率走 `REST_HR` 专列（设备线），不走「当日最早一条」老口径。

**调用详情**：[`guide/uniapp/api/stats.md`](./api/stats) · [`guide/uniapp/api/page-direct.md`](./api/page-direct)

***

### `pages/my-plan/index.vue` — 我的训练计划

| API 调用 | 关键字段 | 用途 |
|---|---|---|
| `GET /api/v1/training/plans` | — | 列可选训练计划（无鉴权） |
| `POST /api/v1/training/enroll` | `planId` | 报名计划 |
| `GET /api/v1/training/tasks` | — | 我的训练任务 |
| `POST /api/v1/training/task/checkin` | `taskId/date` | 任务打卡 |
| `GET /api/v1/training/reminders` | — | 我的训练提醒 |

**调用详情**：[`guide/uniapp/api/training.md`](./api/training)

***

## §3 详情页

> 14 个 detail 页全部走 `POST /api/v1/device` action 协议，按 URL `type` 路由：
> `pages/detail/page-{X}-detail/index.vue`（X ∈ sleep / heartRate / stress / oxygen / steps / calories / strength / weight / run-step / sport / health-summary / spots-list）
>
> * 2 个 selector 页（`detail-select-date` / `detail-select-symmary-date`）

| 详情页 | API 调用 | action | 入参关键 |
|---|---|---|---|
| `detail/page-sleep-detail` | `POST /api/v1/device` | `sleepDetail` | `date` |
| `detail/page-heart-rate-detail` | `POST /api/v1/device` | `heartRateDetail` | `date` |
| `detail/page-stress-detail` | `POST /api/v1/device` | `stressDetail` | `date` |
| `detail/page-oxygen-detail` | `POST /api/v1/device` | `oxygenDetail` | `date` |
| `detail/page-steps-detail` | `POST /api/v1/device` | `stepsDetail` | `date` |
| `detail/page-calories-detail` | `POST /api/v1/device` | `caloriesDetail` | `date` |
| `detail/page-strength-detail` | `POST /api/v1/device` | `strengthDetail` | `date` |
| `detail/page-weight-detail` | `POST /api/v1/device` | `weightDetail` | `from/to`（区间） |
| `detail/page-run-step-detail` | `POST /api/v1/sport` | `runSessionDetail` | `sessionId` |
| `detail/page-sport-detail` | `POST /api/v1/sport` | `runSessionDetail` | `sessionId` |
| `detail/page-health-summary` | `POST /api/v1/device` | `activitySummary` | `date` |
| `detail/page-spots-list` | `POST /api/v1/device` | `recordDates` | `from/to` |
| `detail/select-date` | — | — | 纯 UI（日期选择器） |
| `detail/select-symmary-date` | — | — | 纯 UI（汇总日期选择器） |

> ⚠️ **所有 device/sport detail 的 `date`** 默认昨天（CN 0-8 点取墙钟前一天，避开 UTC 差一天 bug，详 [CLAUDE.md cn\_today 修复](/internal-wiki/operations/cn-today-default-bug-2026-09-21)）。

**调用详情**：[`guide/uniapp/api/device.md`](./api/device) · [`guide/uniapp/api/sport.md`](./api/sport)

***

## §4 表单

### `pages/form/add_weight-page/index.vue` — 加体重表单

| API 调用 | action | 入参 | 用途 |
|---|---|---|---|
| `POST /api/v1/sport` | `addWeightRecord` | `weightKg/date` | 写体重（InMemory + PG） |

**调用详情**：[`guide/uniapp/api/sport.md`](./api/sport)

***

## §5 个人中心

### `pages/mine/index.vue` — 我的（个人主页）

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/user {action:"myProfile"}` | 读全量画像（昵称/头像/签名/身高/体重/运动偏好） |

**调用详情**：[`guide/uniapp/api/user.md`](./api/user)

***

### `pages/mine/info/personal-form/index.vue` — 个人资料表单

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/user {action:"myProfile"}` | 初始化表单（读当前） |
| `POST /api/v1/user {action:"updateProfile"}` | 提交修改 |

***

### `pages/mine/info/health-records/index.vue` — 健康档案

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/health-archive {action:"archive"}` | 拉档案 |
| `POST /api/v1/health-archive {action:"saveHealthIncident"}` | 写健康事件 |
| `POST /api/v1/health-archive {action:"saveHeartRateZones"}` | 写心率区间 |

**调用详情**：[`guide/uniapp/api/health-archive.md`](./api/health-archive)

***

### `pages/mine/account-security/index.vue` — 账号安全

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/user {action:"myProfile"}` | 账号信息回显 |

***

### `pages/mine/account-security/device-sessions/index.vue` — 设备会话管理

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/auth/oauth {action:"mySessions"}` | 列会话 |
| `POST /api/v1/auth/oauth {action:"revokeSession"}` | 撤销指定会话 |

***

### `pages/mine/data/device-list/index.vue` — 设备列表

| API 调用 | 用途 |
|---|---|
| `GET /api/v1/device/vendors` | 列可用厂商 |
| `POST /api/v1/device/vendors/{id}/auth-url` | 拿授权 URL |
| `POST /api/v1/device {action:"myBindings"}` | 我的绑定 |
| `POST /api/v1/device {action:"unbind"}` | 解绑 |

**调用详情**：[`guide/uniapp/api/device.md`](./api/device)

***

### `pages/mine/data/heart-rate-list/index.vue` — 心率历史列表

```text
→ 跳转 detail/page-heart-rate-detail
```

***

### `pages/mine/data/linked_app/index.vue` — 联动应用

| API 调用 | 用途 |
|---|---|
| `GET /api/v1/device/vendors` | 列联动厂商 |

***

### `pages/mine/sports/sports-data/index.vue` — 运动数据列表

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/sport {action:"myRunRecords"}` | 列我的跑步记录 |

**调用详情**：[`guide/uniapp/api/sport.md`](./api/sport)

***

### `pages/mine/version/index.vue` — 版本信息

```text
无后端调用（包内常量）
```

***

## §6 跑步会话

### `pages/run/index.vue` — 跑步主页（.nvue）

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/sport {action:"startRun"}` | 开跑（创建 session） |
| `POST /api/v1/sport {action:"reportRunCoordinate"}` | 周期上报坐标 |

**调用详情**：[`guide/uniapp/api/sport.md`](./api/sport)

***

### `pages/run/sport-index/index.vue` — 运动类型选择（.nvue）

```text
无后端调用（包内常量）
```

***

### `pages/run/setting/index.vue` — 跑步设置

```text
无后端调用（本地配置）
```

***

### `pages/run/start-run-time/index.vue` — 开跑计时

| API 调用 | 用途 |
|---|---|
| `POST /api/v1/sport {action:"finishRun"}` | 完跑（结算 session + 写积分） |

***

## §7 选城市 + 生吃榜（2026-09-29 新增 · 前端待建）

> **状态**：后端已上线（worktree `claude/feat-city-rawfood`，commit `5dd25ae`，等 MR #41 合 main）。前端页面**待建** —— 本节是给前端同事的页面 + 调用范本。

***

### `pages/city-select/index.vue` — 🆕 选城市页

**入口触发**：首页 `getCurrentCity` 返 `40431003`（未绑城市）→ `uni.navigateTo`

#### 调用的 API

| 调用时机 | API | 入参 | 响应读法 |
|---|---|---|---|
| onShow | `POST /api/v1/city {action:"getRecentCities"}` | `{limit: 5}` | `res.data.cities` 渲染列表（当前绑定单条 + seed 10 热门去重垫底） |
| 用户输入 | `POST /api/v1/city {action:"listCities"}` | `{keyword: "广州"}` | `res.data.cities` + `res.data.total` 实时过滤 |
| 点选 | `POST /api/v1/city {action:"selectCity"}` | `{adcode: "440100"}` | `res.data.{adcode,name,source,updatedAt}` → `uni.navigateBack()` |

#### 完整字段 + 错误码

* **完整字段表**：[`api-docs/v1/city`](/api-docs/v1/city/)
* **错误码矩阵**：[`api-docs/v1/city/error-codes`](/api-docs/v1/city/error-codes)
* **页面流图**：[`internal-wiki/operations/2026-09-29-city-rawfood-frontend-handoff`](/internal-wiki/operations/2026-09-29-city-rawfood-frontend-handoff)

#### uniapp 写法范本

```ts
// src/api/city.ts（前端同事按此接口新建）
import request from '@/utils/request'

export interface City {
  adcode: string
  name: string
  parentCode: string | null
  level: number  // 1 省 / 2 市
  pinyin: string | null
  isHot: boolean
}

export function listCities(params: {
  level?: 1 | 2 | 3
  keyword?: string
  hot?: boolean
  page?: number
  size?: number
}) {
  return request({
    url: '/api/v1/city',
    method: 'POST',
    data: { action: 'listCities', payload: params },
  })
}

export function selectCity(payload: { adcode: string }) {
  return request({
    url: '/api/v1/city',
    method: 'POST',
    data: { action: 'selectCity', payload },
  })
}

export function getCurrentCity() {
  return request({
    url: '/api/v1/city',
    method: 'POST',
    data: { action: 'getCurrentCity', payload: {} },
  })
}

export function getRecentCities(limit = 5) {
  return request({
    url: '/api/v1/city',
    method: 'POST',
    data: { action: 'getRecentCities', payload: { limit } },
  })
}
```

```vue
<!-- pages/city-select/index.vue 关键逻辑 -->
<script setup lang="ts">
import { listCities, selectCity, getRecentCities } from '@/api/city'

const cities = ref<City[]>([])
const keyword = ref('')

onShow(async () => {
  const res = await getRecentCities(5)
  cities.value = res.data.cities  // 当前绑定 + 热门垫底
})

watch(keyword, async (kw) => {
  if (!kw) return  // 空 keyword 用 onShow 的初始数据
  const res = await listCities({ keyword: kw, page: 1, size: 50 })
  cities.value = res.data.cities
  // res.data.total 显示「共 X 条」
})

async function onPick(city: City) {
  await selectCity({ adcode: city.adcode })
  uni.navigateBack()  // 返回上一页，首页自动刷新 getCurrentCity
}
</script>
```

#### ⚠️ 必看红线

1. **40431003 = 新用户常态**，**不打错误 toast** —— 直接跳本页面
2. **40431005 = v1 GPS 不可用**，**前端不做「自动定位」按钮**（seed 无 geo → `SeedNearestCityLocator` 恒返 None）
3. **首页 city 字段 ≠ 产品城市** —— splash.city 是广告平台字段，**唯一产品真源**：`getCurrentCity`

***

### `pages/rawfood-ranking/index.vue` — 🆕 生吃榜页

**入口触发**：健康饮食 Tab / 启动按钮

#### 调用的 API

| 调用时机 | API | 入参 | 响应读法 |
|---|---|---|---|
| Tab 默认 | `POST /api/v1/ranking {action:"rawFoodRanking"}` | `{scope:"national", limit:50}` | `res.data.list[]` 渲染（rank/foodId/name/rawBenefitScore/gi/badge） |
| 切换「我的城市」 | `POST /api/v1/ranking {action:"rawFoodRanking"}` | `{scope:"city", cityAdcode:"<getCurrentCity 返回>", limit:50}` | 同上 |

#### 完整字段 + 错误码

* **完整字段表**：[`api-docs/v1/ranking`](/api-docs/v1/ranking/)
* **错误码矩阵**：[`api-docs/v1/ranking/error-codes`](/api-docs/v1/ranking/error-codes)

#### uniapp 写法范本

```ts
// src/api/ranking.ts（前端同事按此接口新建）
import request from '@/utils/request'

export interface RawFoodItem {
  rank: number
  foodId: string  // rf-001 ~ rf-050
  name: string
  category: string  // vegetable / fruit / nut / fish ...
  rawBenefitScore: number  // 0-100
  gi: number  // 升糖指数
  suitableFor: string[]  // diabetes / weight_loss / athlete ...
  badge: string | null
  imageUrl: string | null  // v1 恒 null
}

export function rawFoodRanking(params: {
  scope?: 'national' | 'city'
  limit?: number  // 默认 50，上限 100
  cityAdcode?: string  // scope=city 必传 6 位
}) {
  return request({
    url: '/api/v1/ranking',
    method: 'POST',
    data: { action: 'rawFoodRanking', payload: params },
  })
}
```

```vue
<!-- pages/rawfood-ranking/index.vue 关键逻辑 -->
<script setup lang="ts">
import { ref } from 'vue'
import { rawFoodRanking } from '@/api/ranking'
import { getCurrentCity } from '@/api/city'

const scope = ref<'national' | 'city'>('national')
const list = ref<RawFoodItem[]>([])

async function loadList() {
  const params: any = { scope: scope.value, limit: 50 }
  if (scope.value === 'city') {
    // 城市榜需要先取当前城市 adcode
    const cur = await getCurrentCity()
    params.cityAdcode = cur.data.city?.adcode
  }
  const res = await rawFoodRanking(params)
  list.value = res.data.list
  // res.data.generatedAt 是 5min TTL 缓存刷新时间
}

onMounted(loadList)
watch(scope, loadList)
</script>
```

#### ⚠️ 必看要点

1. **榜单已按 rawBenefitScore 降序排好** —— 前端无需再排序
2. **`rawSafe=false` 食物已沉底 + 带警示 badge** —— 前端无需再过滤（但 UI 仍可在 badge 上做警示色）
3. **`imageUrl` v1 全 null** —— 图标走前端静态映射（`/static/food-icons/rf-001.png` 等，按 foodId 前缀）
4. **缓存 5min TTL** —— `generatedAt` 字段是缓存刷新时间，不是榜单生成时间

***

## 全局约定\*\*（G1-G7）

> 完整约定见 [前端联调 API 手册 · 通用约定](/api-docs/v1/frontend/#通用约定-7-条-·-所有端点共用)

| # | 约定 | 一句话总结 |
|---|---|---|
| G1 | 统一包络 | `res.data.xxx` 取业务字段（不解包） |
| G2 | 请求头 | `X-Env: Prd` 主请求 + `rotate` 都要带 |
| G3 | 鉴权档位 | 大部分 `Bearer <access>`；`rotate` 用 `<refresh>` |
| G4 | 错误契约 | 业务码字符串 `"0"` 成功；用 `String(res.code) === '0'` 判定 |
| G5 | 401 rotate 单飞 | 收到 401 或 `token_expiring=true` 触发 rotate，避免循环 |
| G6 | snake\_case 例外 | 响应**几乎全 camelCase**（`memberExpireAt` / `weeklyUsed` / `isNewUser`） |
| G7 | 响应无 schema | 类型断言靠字段路径，`?.` 链 + `withDefaults` 兜 `undefined`（**不兜 null**） |

***

## 相关文档

| 文档 | 看什么 |
|---|---|
| [前端联调 API 手册](/api-docs/v1/frontend/) | 30+19+5 端点字段表 / 错误码 / curl / 头号坑位 |
| [uniapp/pages/\*](./pages/) | 34 页每页职责 / 关键逻辑 / 变更记录 |
| [uniapp/api/\*](./api/) | 每 API 调用契约（读法路径 + 前端动作） |
| [city 模块](/api-docs/v1/city/) | 🆕 city 4 action 完整字段 |
| [ranking 模块](/api-docs/v1/ranking/) | 🆕 ranking 2 action + D-10 公式 |
| [前端对接 handoff](/internal-wiki/operations/2026-09-29-city-rawfood-frontend-handoff) | 🆕 页面流图 + 城市/生吃榜关系 |

***

> **生成与维护**：
>
> * `docs/api-docs/v1/frontend/` 由契约实测生成（`tests/reports/contract-matrix.json`）
> * `docs/guide/uniapp/pages/*` 由 `scripts/gen_uniapp_docs.py` 从前端源码批量生成
> * 本文档**手工维护** —— 新增页面/API 时**手动同步**到对应章节
> * **一致性校验**：`scripts/check_uniapp_api_docs.py`（pre-commit 钩子，30 端点矩阵 + 死链 + 字段路径）
