fix: 修复更新头像接口参数错误及页面刷新问题
主要修改: - 修复 updateProfileAPI 参数:使用 avatar_meta_id 替代 avatar_url - 头像上传后保存 meta_id 字段用于后续更新 - 添加 meta_id 验证,避免传递空值 - 修复"我的"页面:添加 useDidShow 实现返回时自动刷新 - 显示工号(employee_no)替代用户 ID - 更新项目文档:记录生命周期钩子使用陷阱 技术细节: - src/pages/avatar/index.vue: 添加 tempAvatarMetaId 存储 - src/pages/mine/index.vue: 使用 useDidShow 而非 useShow - docs/lessons-learned.md: 新增"生命周期钩子使用陷阱"章节 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
7 changed files
with
143 additions
and
14 deletions
| ... | @@ -91,18 +91,25 @@ | ... | @@ -91,18 +91,25 @@ |
| 91 | 91 | ||
| 92 | | 日期 | 版本 | 变更内容 | 变更原因 | 文档链接 | | 92 | | 日期 | 版本 | 变更内容 | 变更原因 | 文档链接 | |
| 93 | |------|------|---------|---------|---------| | 93 | |------|------|---------|---------|---------| |
| 94 | +| 2026-02-03 | v1.1 | 新增 `employee_no` 字段(工号) | 业务需求变更 | [查看](#) | | ||
| 94 | | 2026-02-03 | v1.0 | 初始版本 | - | [查看](#) | | 95 | | 2026-02-03 | v1.0 | 初始版本 | - | [查看](#) | |
| 95 | 96 | ||
| 96 | **页面调试情况** | 97 | **页面调试情况** |
| 97 | 98 | ||
| 98 | | 日期 | 调试页面 | 问题记录 | 解决方案 | 状态 | | 99 | | 日期 | 调试页面 | 问题记录 | 解决方案 | 状态 | |
| 99 | |------|---------|---------|---------|------| | 100 | |------|---------|---------|---------|------| |
| 100 | -| 2026-02-03 | `src/pages/mine/index.vue` | 无 | - | ✅ 已完成 | | 101 | +| 2026-02-03 | `src/pages/mine/index.vue` | 页面显示 ID 字段,应显示工号 | 将 `userInfo?.id` 改为 `userInfo?.employee_no` | ✅ 已完成 | |
| 101 | 102 | ||
| 102 | **接口状态**: ✅ 已完成 | 103 | **接口状态**: ✅ 已完成 |
| 103 | 104 | ||
| 104 | **备注**: | 105 | **备注**: |
| 105 | -- 返回用户信息:id、avatar_url、name | 106 | +- **返回用户信息**: |
| 107 | + - `id` - 用户ID(内部标识) | ||
| 108 | + - `employee_no` - 工号(对外展示)⭐ 新增 | ||
| 109 | + - `avatar_url` - 头像URL | ||
| 110 | + - `name` - 姓名 | ||
| 111 | + - `avatar_meta_id` - 头像附件ID | ||
| 112 | +- **重要变更**:页面展示工号使用 `employee_no` 字段,而非 `id` 字段 | ||
| 106 | - 在"我的"页面加载时调用 | 113 | - 在"我的"页面加载时调用 |
| 107 | - 401 自动跳转登录页(由 request.js 拦截器处理) | 114 | - 401 自动跳转登录页(由 request.js 拦截器处理) |
| 108 | - 实现位置:`src/pages/mine/index.vue:fetchUserProfile()` | 115 | - 实现位置:`src/pages/mine/index.vue:fetchUserProfile()` | ... | ... |
| ... | @@ -64,15 +64,25 @@ paths: | ... | @@ -64,15 +64,25 @@ paths: |
| 64 | name: | 64 | name: |
| 65 | type: string | 65 | type: string |
| 66 | title: 姓名 | 66 | title: 姓名 |
| 67 | + avatar_meta_id: | ||
| 68 | + type: integer | ||
| 69 | + title: 头像的附件ID | ||
| 70 | + employee_no: | ||
| 71 | + type: string | ||
| 72 | + title: 工号 | ||
| 67 | x-apifox-orders: | 73 | x-apifox-orders: |
| 68 | - id | 74 | - id |
| 69 | - name | 75 | - name |
| 70 | - avatar_url | 76 | - avatar_url |
| 77 | + - avatar_meta_id | ||
| 78 | + - employee_no | ||
| 71 | title: 用户信息 | 79 | title: 用户信息 |
| 72 | required: | 80 | required: |
| 73 | - id | 81 | - id |
| 74 | - avatar_url | 82 | - avatar_url |
| 75 | - name | 83 | - name |
| 84 | + - avatar_meta_id | ||
| 85 | + - employee_no | ||
| 76 | x-apifox-orders: | 86 | x-apifox-orders: |
| 77 | - user | 87 | - user |
| 78 | required: | 88 | required: | ... | ... |
| ... | @@ -43,14 +43,15 @@ paths: | ... | @@ -43,14 +43,15 @@ paths: |
| 43 | schema: | 43 | schema: |
| 44 | type: object | 44 | type: object |
| 45 | properties: | 45 | properties: |
| 46 | - avatar_url: | 46 | + avatar_meta_id: |
| 47 | - type: string | 47 | + type: integer |
| 48 | + title: 头像的附件ID | ||
| 48 | required: | 49 | required: |
| 49 | - - avatar_url | 50 | + - avatar_meta_id |
| 50 | x-apifox-orders: | 51 | x-apifox-orders: |
| 51 | - - avatar_url | 52 | + - avatar_meta_id |
| 52 | example: | 53 | example: |
| 53 | - avatar_url: http://.../new_avatar.jpg | 54 | + avatar_meta_id: 834933 |
| 54 | responses: | 55 | responses: |
| 55 | '200': | 56 | '200': |
| 56 | description: '' | 57 | description: '' |
| ... | @@ -75,7 +76,7 @@ paths: | ... | @@ -75,7 +76,7 @@ paths: |
| 75 | x-apifox-ordering: 0 | 76 | x-apifox-ordering: 0 |
| 76 | security: [] | 77 | security: [] |
| 77 | x-apifox-folder: 用户 | 78 | x-apifox-folder: 用户 |
| 78 | - x-apifox-status: developing | 79 | + x-apifox-status: testing |
| 79 | x-run-in-apifox: https://app.apifox.com/web/project/7792797/apis/api-413906669-run | 80 | x-run-in-apifox: https://app.apifox.com/web/project/7792797/apis/api-413906669-run |
| 80 | components: | 81 | components: |
| 81 | schemas: {} | 82 | schemas: {} | ... | ... |
| ... | @@ -6,6 +6,7 @@ | ... | @@ -6,6 +6,7 @@ |
| 6 | 6 | ||
| 7 | - [组件抽取与复用](#组件抽取与复用) | 7 | - [组件抽取与复用](#组件抽取与复用) |
| 8 | - [NutUI 组件使用陷阱](#nutui-组件使用陷阱) | 8 | - [NutUI 组件使用陷阱](#nutui-组件使用陷阱) |
| 9 | +- [生命周期钩子使用陷阱](#生命周期钩子使用陷阱) | ||
| 9 | - [静态资源加载问题](#静态资源加载问题) | 10 | - [静态资源加载问题](#静态资源加载问题) |
| 10 | - [样式处理策略](#样式处理策略) | 11 | - [样式处理策略](#样式处理策略) |
| 11 | - [性能优化](#性能优化) | 12 | - [性能优化](#性能优化) |
| ... | @@ -237,6 +238,92 @@ setTimeout(() => { | ... | @@ -237,6 +238,92 @@ setTimeout(() => { |
| 237 | 238 | ||
| 238 | --- | 239 | --- |
| 239 | 240 | ||
| 241 | +## 生命周期钩子使用陷阱 | ||
| 242 | + | ||
| 243 | +### ❌ 坑: 误用 `useShow` 而非 `useDidShow`(重复 2 次) | ||
| 244 | + | ||
| 245 | +**问题描述**: | ||
| 246 | +```javascript | ||
| 247 | +// ❌ 错误:使用了 Vue 3 的 useShow(在 Taro 中不可用) | ||
| 248 | +import { useShow } from '@tarojs/taro' | ||
| 249 | + | ||
| 250 | +useShow(() => { | ||
| 251 | + fetchUserProfile() | ||
| 252 | +}) | ||
| 253 | +``` | ||
| 254 | + | ||
| 255 | +**错误表现**: | ||
| 256 | +- IDE 提示 "useShow 未使用"(因为 Taro 中没有这个钩子) | ||
| 257 | +- 页面返回时不会触发刷新 | ||
| 258 | + | ||
| 259 | +**正确做法**: | ||
| 260 | +```javascript | ||
| 261 | +// ✅ 正确:使用 Taro 的 useDidShow | ||
| 262 | +import Taro, { useLoad, useDidShow } from '@tarojs/taro' | ||
| 263 | + | ||
| 264 | +useLoad(() => { | ||
| 265 | + // 页面首次加载时触发 | ||
| 266 | + fetchUserProfile() | ||
| 267 | +}) | ||
| 268 | + | ||
| 269 | +useDidShow(() => { | ||
| 270 | + // 每次页面显示时触发(包括从其他页面返回) | ||
| 271 | + fetchUserProfile() | ||
| 272 | +}) | ||
| 273 | +``` | ||
| 274 | + | ||
| 275 | +**Taro 生命周期钩子对照表**: | ||
| 276 | + | ||
| 277 | +| 生命周期 | Taro 钩子 | 用途 | 备注 | | ||
| 278 | +|---------|-----------|------|------| | ||
| 279 | +| 页面加载 | `useLoad` | 首次进入页面时触发 | 只触发一次 | | ||
| 280 | +| 页面显示 | `useDidShow` | 每次页面显示时触发 | 包括从其他页面返回 | | ||
| 281 | +| 页面渲染完成 | `useReady` | 首次渲染完成后触发 | 可操作 DOM | | ||
| 282 | +| 页面隐藏 | `useDidHide` | 页面隐藏时触发 | 清理定时器等 | | ||
| 283 | +| 页面卸载 | `useUnload` | 页面卸载时触发 | 清理资源 | | ||
| 284 | + | ||
| 285 | +**⚠️ 重要检查清单(写代码前必须执行)**: | ||
| 286 | + | ||
| 287 | +1. **搜索现有用法**: 在项目中搜索关键字,确认其他页面是如何使用的 | ||
| 288 | + ```bash | ||
| 289 | + # 在终端执行 | ||
| 290 | + grep -r "useDidShow\|useShow" src/pages/ | ||
| 291 | + ``` | ||
| 292 | + | ||
| 293 | +2. **参考现有页面**: 查看项目中已有的页面实现 | ||
| 294 | + ```bash | ||
| 295 | + # 例如查看 feedback-list 页面 | ||
| 296 | + cat src/pages/feedback-list/index.vue | grep "import.*@tarojs/taro" | ||
| 297 | + ``` | ||
| 298 | + | ||
| 299 | +3. **统一命名规范**: 本项目统一使用 Taro 的钩子 | ||
| 300 | + - ✅ `useDidShow`(Taro 官方) | ||
| 301 | + - ❌ `useShow`(Vue 3 Composition API,在小程序中不适用) | ||
| 302 | + | ||
| 303 | +**历史记录**: | ||
| 304 | +- 第 1 次错误:在 `src/pages/mine/index.vue` 中使用 `useShow` | ||
| 305 | +- 第 2 次错误:在同一位置再次使用 `useShow`(未检查项目现有用法) | ||
| 306 | +- **教训**: ⚠️ **写代码前必须先搜索项目中是否已有类似实现** | ||
| 307 | + | ||
| 308 | +**最佳实践**: | ||
| 309 | +```javascript | ||
| 310 | +// ✅ 推荐的导入方式(一行导入所有需要的钩子) | ||
| 311 | +import Taro, { useLoad, useDidShow, useReady } from '@tarojs/taro' | ||
| 312 | + | ||
| 313 | +// ✅ 页面加载时获取数据 | ||
| 314 | +useLoad((options) => { | ||
| 315 | + console.log('页面参数:', options) | ||
| 316 | + fetchData() | ||
| 317 | +}) | ||
| 318 | + | ||
| 319 | +// ✅ 页面显示时刷新数据(从其他页面返回时) | ||
| 320 | +useDidShow(() => { | ||
| 321 | + refreshData() | ||
| 322 | +}) | ||
| 323 | +``` | ||
| 324 | + | ||
| 325 | +--- | ||
| 326 | + | ||
| 240 | ## 静态资源加载问题 | 327 | ## 静态资源加载问题 |
| 241 | 328 | ||
| 242 | ### ❌ 坑: SVG 图标加载失败(500 错误) | 329 | ### ❌ 坑: SVG 图标加载失败(500 错误) | ... | ... |
| ... | @@ -20,6 +20,8 @@ const Api = { | ... | @@ -20,6 +20,8 @@ const Api = { |
| 20 | * id: integer; // 用户ID | 20 | * id: integer; // 用户ID |
| 21 | * avatar_url: string; // 头像 | 21 | * avatar_url: string; // 头像 |
| 22 | * name: string; // 姓名 | 22 | * name: string; // 姓名 |
| 23 | + * avatar_meta_id: integer; // 头像的附件ID | ||
| 24 | + * employee_no: string; // 工号 | ||
| 23 | * }; | 25 | * }; |
| 24 | * }; | 26 | * }; |
| 25 | * }>} | 27 | * }>} |
| ... | @@ -71,7 +73,7 @@ export const logoutAPI = (params) => fn(fetch.post(Api.Logout, params)); | ... | @@ -71,7 +73,7 @@ export const logoutAPI = (params) => fn(fetch.post(Api.Logout, params)); |
| 71 | * @description 更新个人资料 | 73 | * @description 更新个人资料 |
| 72 | * @remark | 74 | * @remark |
| 73 | * @param {Object} params 请求参数 | 75 | * @param {Object} params 请求参数 |
| 74 | - * @param {string} params.avatar_url | 76 | + * @param {integer} params.avatar_meta_id |
| 75 | * @returns {Promise<{ | 77 | * @returns {Promise<{ |
| 76 | * code: number; // 状态码 | 78 | * code: number; // 状态码 |
| 77 | * msg: string; // 消息 | 79 | * msg: string; // 消息 | ... | ... |
| ... | @@ -46,6 +46,7 @@ import BASE_URL from '@/utils/config' | ... | @@ -46,6 +46,7 @@ import BASE_URL from '@/utils/config' |
| 46 | const go = useGo() | 46 | const go = useGo() |
| 47 | const avatarUrl = ref(defaultAvatar) | 47 | const avatarUrl = ref(defaultAvatar) |
| 48 | const tempAvatarUrl = ref('') // 临时存储上传后的头像URL | 48 | const tempAvatarUrl = ref('') // 临时存储上传后的头像URL |
| 49 | +const tempAvatarMetaId = ref(null) // 临时存储上传后的头像 meta_id | ||
| 49 | 50 | ||
| 50 | /** | 51 | /** |
| 51 | * @description 更换头像(参考老来赛项目,直接上传到服务器) | 52 | * @description 更换头像(参考老来赛项目,直接上传到服务器) |
| ... | @@ -93,6 +94,9 @@ const onChangeAvatar = () => { | ... | @@ -93,6 +94,9 @@ const onChangeAvatar = () => { |
| 93 | if (data.code === 0) { // 注意:老来赛后端 code=0 表示成功 | 94 | if (data.code === 0) { // 注意:老来赛后端 code=0 表示成功 |
| 94 | avatarUrl.value = data.data.src | 95 | avatarUrl.value = data.data.src |
| 95 | tempAvatarUrl.value = data.data.src | 96 | tempAvatarUrl.value = data.data.src |
| 97 | + // 保存 meta_id,用于后续更新头像 | ||
| 98 | + tempAvatarMetaId.value = data.data.meta_id || data.data.id || null | ||
| 99 | + console.log('上传成功,meta_id:', tempAvatarMetaId.value) | ||
| 96 | Taro.showToast({ | 100 | Taro.showToast({ |
| 97 | title: '上传成功', | 101 | title: '上传成功', |
| 98 | icon: 'success' | 102 | icon: 'success' |
| ... | @@ -136,12 +140,23 @@ const onSave = async () => { | ... | @@ -136,12 +140,23 @@ const onSave = async () => { |
| 136 | return | 140 | return |
| 137 | } | 141 | } |
| 138 | 142 | ||
| 143 | + // 检查是否有 meta_id | ||
| 144 | + if (!tempAvatarMetaId.value) { | ||
| 145 | + console.error('缺少 avatar_meta_id,上传响应可能不包含 meta_id 字段') | ||
| 146 | + Taro.showToast({ | ||
| 147 | + title: '上传数据异常,请重试', | ||
| 148 | + icon: 'none' | ||
| 149 | + }) | ||
| 150 | + return | ||
| 151 | + } | ||
| 152 | + | ||
| 139 | // 保存到服务器 | 153 | // 保存到服务器 |
| 140 | Taro.showLoading({ title: '保存中...', mask: true }) | 154 | Taro.showLoading({ title: '保存中...', mask: true }) |
| 141 | 155 | ||
| 142 | try { | 156 | try { |
| 157 | + // ✅ 修复:传递正确的参数 avatar_meta_id | ||
| 143 | const res = await updateProfileAPI({ | 158 | const res = await updateProfileAPI({ |
| 144 | - avatar_url: tempAvatarUrl.value | 159 | + avatar_meta_id: tempAvatarMetaId.value |
| 145 | }) | 160 | }) |
| 146 | 161 | ||
| 147 | Taro.hideLoading() | 162 | Taro.hideLoading() | ... | ... |
| ... | @@ -18,7 +18,7 @@ | ... | @@ -18,7 +18,7 @@ |
| 18 | <!-- Info --> | 18 | <!-- Info --> |
| 19 | <view class="ml-[32rpx] flex-1 flex flex-col justify-center"> | 19 | <view class="ml-[32rpx] flex-1 flex flex-col justify-center"> |
| 20 | <text class="text-[36rpx] font-bold text-gray-800 mb-[8rpx]">{{ userInfo?.name || '加载中...' }}</text> | 20 | <text class="text-[36rpx] font-bold text-gray-800 mb-[8rpx]">{{ userInfo?.name || '加载中...' }}</text> |
| 21 | - <text class="text-[28rpx] text-gray-500 mb-[4rpx]">ID: {{ userInfo?.id || '--' }}</text> | 21 | + <text class="text-[28rpx] text-gray-500 mb-[4rpx]">工号: {{ userInfo?.employee_no || '--' }}</text> |
| 22 | <text class="text-[24rpx] text-gray-400">点击修改头像</text> | 22 | <text class="text-[24rpx] text-gray-400">点击修改头像</text> |
| 23 | </view> | 23 | </view> |
| 24 | 24 | ||
| ... | @@ -75,8 +75,7 @@ import { useUserStore } from '@/stores/user' | ... | @@ -75,8 +75,7 @@ import { useUserStore } from '@/stores/user' |
| 75 | import IconFont from '@/components/IconFont.vue' | 75 | import IconFont from '@/components/IconFont.vue' |
| 76 | import TabBar from '@/components/TabBar.vue' | 76 | import TabBar from '@/components/TabBar.vue' |
| 77 | import NavHeader from '@/components/NavHeader.vue' | 77 | import NavHeader from '@/components/NavHeader.vue' |
| 78 | -import Taro from '@tarojs/taro' | 78 | +import Taro, { useLoad, useDidShow } from '@tarojs/taro' |
| 79 | -import { useLoad } from '@tarojs/taro' | ||
| 80 | import { getProfileAPI } from '@/api/user' | 79 | import { getProfileAPI } from '@/api/user' |
| 81 | import defaultAvatar from '@/assets/images/icon/avatar.svg' | 80 | import defaultAvatar from '@/assets/images/icon/avatar.svg' |
| 82 | 81 | ||
| ... | @@ -113,12 +112,20 @@ const fetchUserProfile = async () => { | ... | @@ -113,12 +112,20 @@ const fetchUserProfile = async () => { |
| 113 | } | 112 | } |
| 114 | 113 | ||
| 115 | /** | 114 | /** |
| 116 | - * @description 页面加载时获取用户信息 | 115 | + * @description 页面加载时获取用户信息(首次进入) |
| 117 | */ | 116 | */ |
| 118 | useLoad(() => { | 117 | useLoad(() => { |
| 119 | fetchUserProfile() | 118 | fetchUserProfile() |
| 120 | }) | 119 | }) |
| 121 | 120 | ||
| 121 | +/** | ||
| 122 | + * @description 页面显示时刷新用户信息(从其他页面返回时) | ||
| 123 | + * @description 例如:从头像设置页面保存后返回,需要刷新显示新头像 | ||
| 124 | + */ | ||
| 125 | +useDidShow(() => { | ||
| 126 | + fetchUserProfile() | ||
| 127 | +}) | ||
| 128 | + | ||
| 122 | const menuItems = [ | 129 | const menuItems = [ |
| 123 | { title: '我的计划书', icon: 'order', path: '/pages/plan/index' }, | 130 | { title: '我的计划书', icon: 'order', path: '/pages/plan/index' }, |
| 124 | { title: '我的收藏', icon: 'star', path: '/pages/favorites/index' }, | 131 | { title: '我的收藏', icon: 'star', path: '/pages/favorites/index' }, | ... | ... |
-
Please register or login to post a comment