Appearance
页面直调端点(不在 src/api/*.ts 里的 3 条)
后端契约:手册 · page-direct(3 端点字段表 / curl / 错误码)。 为什么单列:这 3 条没有
src/api/*.ts封装(页面里内联request/uni.uploadFile)—— 自动化提取器的盲区(生成器只扫src/api/*.ts),也是「读法出错」的高危区。 本页只写正文 1 条(upload);另 2 条已在专页写过,本页只做索引与交叉引用(不重复,避免两处维护)。
一、端点归属索引
| 端点 | 谁在调 | 正文在哪 |
|---|---|---|
POST api/v1/device myTodayHealth | 首页健康卡 src/pages/index/index.vue:124-141 | device —— 块状结构读法 + todayActivity.totalDistanceKm 坑 |
POST api/v1/stats getTodayOverview | 首页热量 / 连续天数卡 src/pages/index/index.vue:120-122 | stats —— 嵌套两层读法 + 防回归块 |
POST api/v1/upload?type=avatar | 个人资料页换头像 src/pages/mine/info/personal-form.vue:281-350 | 本页正文(下) |
⚠️ 前两条共用同一个
Promise.all(loadToday())—— 它们的失败是耦合的, 见 stats · 失败时的前端动作「Promise.all一损俱损」。
upload(页面直调 · 头像上传)
一句话:个人资料页换头像 —— uni.chooseImage 选图 → multipart 直传拿 URL → 再调 updateProfile 落库(两步,别只做第一步)。
前端函数签名
| 项 | 值 |
|---|---|
| 封装 | 页面直调(非 src/api/*.ts)—— src/pages/mine/info/personal-form.vue:282-318 uploadAvatarFile();调用链 chooseAvatar(:320-330)→ handleAvatarUpload(:332-350) |
| 触发时机 | 用户点「头像」行 → uni.chooseImage({ count: 1, sizeType: ['compressed'] }) → 选中即传;uploading 期间入口自锁(重复点击直接 return) |
| 鉴权 | Bearer access + X-Env —— 页面手动拼 header(⚠️ 不走 request.ts 拦截器) |
| 由此失去的能力 | 无自动 rotate —— token 过期不会自动续期,直接失败(其余端点的 401 链在拦截器里,这条没有) |
必需 payload 键
| 键 | 从哪来 | 必填 |
|---|---|---|
type(query) | 字面量 'avatar',拼在 URL 上(${BASE_URL}api/v1/upload?type=avatar) | ✔ |
file(multipart) | uni.chooseImage 的 res.tempFilePaths[0];uni.uploadFile 的 name 参数必须写 file | ✔ |
Authorization / X-Env(header) | storage.token.get() + config.X_ENV —— 手动拼(不是拦截器注入) | ✔ |
响应读法
ts
// uni.uploadFile 的 res.data 是【字符串】(不是对象)—— 与 uni.request 的关键差异,必须先 parse
const body = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
if (res.statusCode === 200 && String(body?.code ?? '') === '0' && body?.data?.url) {
const url = body.data.url; // 中文注解:业务字段在 body.data.url(COS/CDN 或本地 /uploads/...)
const r = await updateProfile({ avatarUrl: url }); // 中文注解:上传 ≠ 落库 —— 落库要走 user 端点(这条经拦截器)
form.avatarUrl = r?.data?.user?.avatarUrl || url; // 中文注解:以后端回吐为准,取不到才用上传返回的 url
}✅ 正确读法:
body.data.url(body= 已JSON.parse的响应体) ❌ 错误读法:直接res.data.url——res.data是字符串,取属性恒undefined→ 上传其实成功了、前端却判失败(resolve分支进不去)。 与其余端点的读法同一原则:业务字段在data之下,只是这里的data要先 parse 出来。 字段表 / 约束(大小上限、MIME 白名单)/ 错误码见 手册 · page-direct#upload。
失败时的前端动作
| 情形 | 前端动作 |
|---|---|
上传层失败(HTTP 非 200 / code ≠ "0" / 无 url) | uploadAvatarFile 内 reject(new Error(body?.msg || '头像上传失败'));页面 catch 用 e instanceof Error 判定 → toast e.message |
网络失败(uni.uploadFile 的 fail) | reject new Error('网络异常,请检查网络连接') → 同上 toast |
落库失败(updateProfile 被拒) | 该层的失败已由拦截器统一 toast,e 是普通对象而非 Error → 页面 catch 不再重复提示。⚠️ 此时图片已上传成功,只是没落库 —— 重试只需重调 updateProfile,不必重传文件 |
| 未取到 token | 前置 reject(new Error('请先登录'))(本条不发起请求) |
| 401 | 无自动 rotate —— 需手动刷新 token 后重试(或引导重新登录)。别指望"等一下再试"能自愈 |
⚠️ 坑位
- 不是 action 协议:没有
{action, payload}信封 ——type在 query、file在 multipart body、header 要手动拼。 别的端点靠request.ts托管的三件事(鉴权头 /X-Env/ 401 rotate),这条全得自己来。 - 漏
X-Env会静默落错库:后端对缺头 / 非法值有 Dev 兜底 —— 上传会「成功」,但记录写进了测试库, 线上访问图片时才暴露(与 手册 G2 同一原则:X-Env 全程一致)。 res.data是字符串:uni.uploadFile不像uni.request那样给结构化响应 —— 忘了 parse 就是「后端成功、前端报错」。- 上传成功 ≠ 头像生效:必须走
updateProfile({ avatarUrl })落库(读法见 user);页面还顺手写了本地缓存 (persistLocalCache()→uni.setStorageSync,下次进入表单预填用),缓存与后端不一致时以res.data.user.avatarUrl为准。 - 上传前先自查 token 有效性:这条没有 rotate 兜底,过期即失败,且失败提示是通用文案(不指向"重新登录")。