hookehuyr

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>
...@@ -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' },
......