Skip to content

page-api-reference —— 前端页面 × 后端 API 反向索引(v1) ​

沐禾健康 C 端 uniapp(qingmu2026/qm-frontend api 分支)所有已上线页面调用的后端 API 总览。 入口文档:找页面,看它调什么;找 API,看谁在用。 写法契约:

  • 字段表 / 错误码 / curl 一律见 前端联调 API 手册
  • 调用读法 / 失败前端动作 见 uniapp/api/*
  • 页面职责 / 关键逻辑 见 uniapp/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(CI 校验源)。 通用约定 G1-G7(统一包络 / X-Env / 鉴权三档 / rotate 单飞)见 手册 · 通用约定。


📑 按类别跳转 ​

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

§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/clientIdres.data.ok40101003/04/05/06
点「登录」POST /api/v1/auth/oauth {action:"sms-login"}phone/smsCode/agreementAccepted + 可选 nickname/deviceInfores.data.tokens.{access,refresh} + res.data.user40101001/08/13
失败回退POST /api/v1/auth/oauth {action:"sms-verify"}(分步表单可选)phone/smsCode/clientIdres.data.ok40101001/03

⚠️ 铁律:图形码无论成败即消费 —— 任何失败分支重试前必须重新 captcha(手册 §captcha 渲染三定律)。

调用详情:guide/uniapp/api/oauth.md · 契约字段表


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

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

调用详情:guide/uniapp/api/agreement.md


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

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

⚠️ 不要把 splash.city(广告投放回传)当成「当前城市」—— 见 §7.1 city-select 跳引导页。

调用详情:guide/uniapp/api/splash.md


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 互补)
onShowPOST /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 · guide/uniapp/api/page-direct.md


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

API 调用关键字段用途
GET /api/v1/training/plans—列可选训练计划(无鉴权)
POST /api/v1/training/enrollplanId报名计划
GET /api/v1/training/tasks—我的训练任务
POST /api/v1/training/task/checkintaskId/date任务打卡
GET /api/v1/training/reminders—我的训练提醒

调用详情:guide/uniapp/api/training.md


§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-detailPOST /api/v1/devicesleepDetaildate
detail/page-heart-rate-detailPOST /api/v1/deviceheartRateDetaildate
detail/page-stress-detailPOST /api/v1/devicestressDetaildate
detail/page-oxygen-detailPOST /api/v1/deviceoxygenDetaildate
detail/page-steps-detailPOST /api/v1/devicestepsDetaildate
detail/page-calories-detailPOST /api/v1/devicecaloriesDetaildate
detail/page-strength-detailPOST /api/v1/devicestrengthDetaildate
detail/page-weight-detailPOST /api/v1/deviceweightDetailfrom/to(区间)
detail/page-run-step-detailPOST /api/v1/sportrunSessionDetailsessionId
detail/page-sport-detailPOST /api/v1/sportrunSessionDetailsessionId
detail/page-health-summaryPOST /api/v1/deviceactivitySummarydate
detail/page-spots-listPOST /api/v1/devicerecordDatesfrom/to
detail/select-date——纯 UI(日期选择器)
detail/select-symmary-date——纯 UI(汇总日期选择器)

⚠️ 所有 device/sport detail 的 date 默认昨天(CN 0-8 点取墙钟前一天,避开 UTC 差一天 bug,详 CLAUDE.md cn_today 修复)。

调用详情:guide/uniapp/api/device.md · guide/uniapp/api/sport.md


§4 表单 ​

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

API 调用action入参用途
POST /api/v1/sportaddWeightRecordweightKg/date写体重(InMemory + PG)

调用详情:guide/uniapp/api/sport.md


§5 个人中心 ​

pages/mine/index.vue — 我的(个人主页) ​

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

调用详情:guide/uniapp/api/user.md


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


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


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


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


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入参响应读法
onShowPOST /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()

完整字段 + 错误码 ​

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}同上

完整字段 + 错误码 ​

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 手册 · 通用约定

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

相关文档 ​

文档看什么
前端联调 API 手册30+19+5 端点字段表 / 错误码 / curl / 头号坑位
uniapp/pages/*34 页每页职责 / 关键逻辑 / 变更记录
uniapp/api/*每 API 调用契约(读法路径 + 前端动作)
city 模块🆕 city 4 action 完整字段
ranking 模块🆕 ranking 2 action + D-10 公式
前端对接 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 端点矩阵 + 死链 + 字段路径)