hookehuyr

docs: 添加经验教训总结并优化项目文档

新增文档:
- docs/lessons-learned.md - Taro 项目开发经验、最佳实践和常见陷阱总结
  - 组件抽取与复用("第 3 次出现原则")
  - NutUI 组件使用陷阱(textarea、IconFont)
  - 静态资源加载问题(SVG 图标)
  - 样式处理策略(TailwindCSS vs Less)
  - 性能优化(shallowRef + markRaw)
  - 代码质量规范(JSDoc、命名规范)
  - 架构设计(统一的列表点击、文件操作)

- docs/changes-summary.md - 文档优化变更说明

优化文档:
- CLAUDE.md
  - 添加快速参考表格,常见问题一目了然
  - 重新组织核心架构章节,结构更清晰
  - 添加样式处理策略和响应式优化章节
  - 添加对经验教训文档的交叉引用

- README.md
  - 添加项目文档索引和核心特性说明
  - 新增常见问题快速参考表格
  - 精简项目结构,移除过时内容
  - 优化开发规范说明

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -2,87 +2,89 @@ ...@@ -2,87 +2,89 @@
2 2
3 本文件为 Claude Code (claude.ai/code) 在处理此仓库代码时提供指导。 3 本文件为 Claude Code (claude.ai/code) 在处理此仓库代码时提供指导。
4 4
5 -## 开发命令 5 +## 📚 项目文档索引
6 +
7 +- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
8 +- **[项目 README](README.md)** - 项目概述和快速开始指南
9 +
10 +## 🚀 开发命令
6 11
7 ### 核心命令 12 ### 核心命令
8 -- `pnpm dev:weapp` - 启动微信小程序开发服务器 13 +```bash
9 -- `pnpm dev:h5` - 启动 H5 开发服务器 14 +pnpm dev:weapp # 启动微信小程序开发服务器
10 -- `pnpm build:weapp` - 构建生产版本(微信小程序) 15 +pnpm dev:h5 # 启动 H5 开发服务器
11 -- `pnpm lint` - 运行 ESLint 16 +pnpm build:weapp # 构建生产版本(微信小程序)
17 +pnpm lint # 运行 ESLint
18 +```
12 19
13 ### 其他平台构建 20 ### 其他平台构建
14 -- `pnpm dev:alipay` - 支付宝小程序开发 21 +```bash
15 -- `pnpm dev:swan` - 百度小程序开发 22 +pnpm dev:alipay # 支付宝小程序开发
16 -- `pnpm dev:tt` - 字节跳动小程序开发 23 +pnpm dev:swan # 百度小程序开发
24 +pnpm dev:tt # 字节跳动小程序开发
25 +```
26 +
27 +## 📋 快速参考
28 +
29 +### ⚡ 常见问题快速解决
30 +
31 +| 问题 | 解决方案 | 参考文档 |
32 +|------|---------|---------|
33 +| NutUI textarea 样式无法覆盖 | 使用原生 `<textarea>` | [经验教训](docs/lessons-learned.md#nutui-组件使用陷阱) |
34 +| IconFont 动态切换不响应 | 添加 `:key="name"` | [经验教训](docs/lessons-learned.md#nutui-组件使用陷阱) |
35 +| SVG 图标加载失败(500 错误) | 使用 `import` 导入 | [经验教训](docs/lessons-learned.md#静态资源加载问题) |
36 +| 代码重复 3 次 | 抽取为 Composable | [经验教训](docs/lessons-learned.md#组件抽取与复用) |
37 +| 组件对象响应式警告 | 使用 `shallowRef` + `markRaw` | [经验教训](docs/lessons-learned.md#性能优化) |
38 +
39 +### 🎯 核心架构模式
40 +
41 +1. **认证流程** - 静默认证 + 401 自动刷新
42 +2. **双设计宽度** - NutUI: 375px, 其他: 750px
43 +3. **样式策略** - TailwindCSS(80%) + Less(20%)
44 +4. **组件抽取** - "第 3 次出现原则"
45 +
46 +### 📦 技术栈
47 +
48 +- **框架**: Taro 4.1.9 + Vue 3.3.0 + Composition API
49 +- **UI 库**: NutUI 4.3.13(京东推出的 Taro UI 库)
50 +- **状态管理**: Pinia 3.0.3 + taro-plugin-pinia
51 +- **HTTP 客户端**: axios-miniprogram
52 +- **样式**: Less + TailwindCSS 3.x(双设计宽度系统)
53 +- **构建工具**: Webpack 5
17 54
18 -## 项目概述 55 +---
56 +
57 +## 📖 项目概述
19 58
20 **Manulife WeApp**(臻奇智荟圈)是一个基于 Taro 4 + Vue 3 + NutUI 构建的财富管理微信小程序。 59 **Manulife WeApp**(臻奇智荟圈)是一个基于 Taro 4 + Vue 3 + NutUI 构建的财富管理微信小程序。
21 60
22 ### 业务模块 61 ### 业务模块
23 -应用目前包含以下主要功能:
24 - **产品展示** - 热门产品展示及详情页 62 - **产品展示** - 热门产品展示及详情页
25 - - 首页显示热门产品,带"产品资料"按钮
26 - - 产品详情页展示完整产品信息
27 - **资料库** - 培训材料和文档管理 63 - **资料库** - 培训材料和文档管理
28 - - 培训材料和案例的知识库
29 - - 资料列表页支持文档预览
30 - **家办** - 家族办公室服务 64 - **家办** - 家族办公室服务
31 - **签单** - 签约流程 65 - **签单** - 签约流程
32 - **用户中心** - 个人资料、收藏、反馈、帮助中心 66 - **用户中心** - 个人资料、收藏、反馈、帮助中心
33 67
34 -## 项目架构 68 +## 🏗️ 核心架构
35 -
36 -这是一个基于 **Taro 4 + Vue 3 + NutUI** 的微信小程序,内置身份认证和可复用的导航组件。
37 -
38 -### 技术栈
39 -- **框架**:Taro 4.1.9 + Vue 3.3.0 + Composition API
40 -- **UI 库**:NutUI 4.3.13(京东推出的 Taro UI 库)
41 -- **状态管理**:Pinia 3.0.3 + taro-plugin-pinia
42 -- **HTTP 客户端**:axios-miniprogram
43 -- **样式**:Less + TailwindCSS 3.x(双设计宽度系统)
44 -- **构建工具**:Webpack 5
45 -- **导航**:自定义 TabBar + 增强导航 hooks
46 -
47 -### 双设计宽度系统
48 -项目在 `config/index.js:16-23` 中配置了**两种不同的设计宽度**
49 -- **NutUI 组件**:375px 基准宽度
50 -- **所有其他页面**:750px 基准宽度
51 -
52 -处理样式时:
53 -- 使用 NutUI 组件 → 参考 375px 设计稿
54 -- 自定义页面布局 → 参考 750px 设计稿
55 69
56 -### 核心架构模式 70 +### 1. 可复用的导航组件
57 71
58 -#### 1. 可复用的导航组件 72 +**TabBar 组件**`src/components/TabBar.vue`
59 - 73 +- 固定底部导航栏,自动适配安全区域
60 -**TabBar 组件**`src/components/TabBar.vue`): 74 +- 支持图标 + 文字布局,激活状态高亮
61 -- 固定底部导航栏
62 -- 自动适配安全区域(刘海屏/底部指示器)
63 -- 支持图标 + 文字布局
64 -- 激活状态高亮
65 - 使用于:首页、我的、家办、知识库、签单页面 75 - 使用于:首页、我的、家办、知识库、签单页面
66 76
67 -**NavHeader 组件**`src/components/NavHeader.vue` 77 +**NavHeader 组件**`src/components/NavHeader.vue`
68 - 带返回按钮的自定义导航头 78 - 带返回按钮的自定义导航头
69 -- 透明/背景变体 79 +- 透明/背景变体,刘海屏设备的安全区域内边距
70 -- 刘海屏设备的安全区域内边距
71 -- 替代默认的 Taro 导航栏
72 80
73 -**IconFont 组件**`src/components/IconFont.vue` 81 +**IconFont 组件**`src/components/IconFont.vue`
74 - 自定义图标的图标字体包装器 82 - 自定义图标的图标字体包装器
75 -- 支持大小和颜色自定义 83 +- ⚠️ **动态切换时需添加 `:key="name"`** [详见经验教训](docs/lessons-learned.md#坑-2-iconfont-动态切换不响应)
76 -
77 -#### 2. 身份认证流程(必需)
78 84
79 -项目具有完善的身份认证系统,支持自动会话管理: 85 +### 2. 身份认证流程(必需)
80 86
81 -**启动流程**`src/app.js:26-214`): 87 +项目具有完善的身份认证系统,支持自动会话管理。
82 -1. 应用保存启动路径用于认证回调
83 -2. 检查网络状态并处理弱网络场景
84 -3. 如果未认证,尝试静默认证
85 -4. 认证成功后,启用离线功能
86 88
87 **核心文件** 89 **核心文件**
88 - `src/utils/authRedirect.js` - 所有认证逻辑(静默刷新、导航、状态) 90 - `src/utils/authRedirect.js` - 所有认证逻辑(静默刷新、导航、状态)
...@@ -90,7 +92,7 @@ ...@@ -90,7 +92,7 @@
90 - `src/pages/auth/index.vue` - 认证页(必须保留) 92 - `src/pages/auth/index.vue` - 认证页(必须保留)
91 - `src/pages/login/index.vue` - 登录页 93 - `src/pages/login/index.vue` - 登录页
92 94
93 -**401 自动刷新工作原理**`src/utils/request.js:241-276` 95 +**401 自动刷新流程**
94 1. API 返回 401 96 1. API 返回 401
95 2. 拦截器保存当前页面路径 97 2. 拦截器保存当前页面路径
96 3. 调用 `refreshSession()` 通过微信登录获取新会话 98 3. 调用 `refreshSession()` 通过微信登录获取新会话
...@@ -99,7 +101,7 @@ ...@@ -99,7 +101,7 @@
99 101
100 **重要**:后端必须提供 `/srv/?a=openid_wxapp` 端点用于微信登录。 102 **重要**:后端必须提供 `/srv/?a=openid_wxapp` 端点用于微信登录。
101 103
102 -#### 3. API 层架构 104 +### 3. API 层架构
103 105
104 **API 定义模式**`src/api/index.js`): 106 **API 定义模式**`src/api/index.js`):
105 ```javascript 107 ```javascript
...@@ -111,27 +113,73 @@ export const yourAPI = (params) => { ...@@ -111,27 +113,73 @@ export const yourAPI = (params) => {
111 **请求包装器**`src/api/fn.js`): 113 **请求包装器**`src/api/fn.js`):
112 - 所有 API 调用都应通过此包装器 114 - 所有 API 调用都应通过此包装器
113 - 处理常见错误场景 115 - 处理常见错误场景
114 -- 提供一致的接口 116 +- **始终检查 `res.code === 1` 判断成功**
115 -
116 -**URL 构建**`src/utils/tools.js`):
117 -- `buildApiUrl(action, params)` - 构建完整的 API URL
118 -- 自动合并来自 `src/utils/config.js` 的默认参数
119 117
120 -#### 4. 增强导航系统 118 +### 4. 增强导航系统
121 119
122 **useGo Hook**`src/hooks/useGo.js`): 120 **useGo Hook**`src/hooks/useGo.js`):
123 ```javascript 121 ```javascript
124 import { useGo } from '@/hooks/useGo' 122 import { useGo } from '@/hooks/useGo'
125 const go = useGo() 123 const go = useGo()
126 124
127 -go('/page-name') // 自动补全路径 125 +go('/pages/detail/index') // 自动补全路径
128 -go('/page', { id: 123 }) // 带查询参数 126 +go('/pages/product-detail/index', { id: 123 }) // 带查询参数
127 +go.back() // 返回上一页
129 ``` 128 ```
130 129
131 **路由存储**`src/stores/router.js`): 130 **路由存储**`src/stores/router.js`):
132 - 维护已访问路由的栈 131 - 维护已访问路由的栈
133 - 用于认证回调导航 132 - 用于认证回调导航
134 -- 由认证流程自动管理 133 +
134 +### 5. 可复用 Composables
135 +
136 +**项目中的 Composables**
137 +
138 +| Composable | 用途 | 文档 |
139 +|-----------|------|------|
140 +| `useSectionList` | 分组列表管理 | [经验教训](docs/lessons-learned.md#案例-1-usesectionlist-composable) |
141 +| `useFileOperation` | 文件下载、预览、打开 | [经验教训](docs/lessons-learned.md#案例-2-usefileoperation-composable) |
142 +| `useListItemClick` | 统一的列表点击处理 | [经验教训](docs/lessons-learned.md#案例-3-uselistitemclick-composable) |
143 +
144 +**抽取原则**:"第 3 次出现原则" - 当相同代码模式出现 3 次时,**必须**抽取为 Composable。
145 +
146 +### 6. 样式处理策略
147 +
148 +**TailwindCSS vs Less 使用指南**
149 +
150 +| 场景 | 使用 | 比例 |
151 +|------|------|------|
152 +| 布局(flex、grid、absolute) | TailwindCSS | 80% |
153 +| 间距(padding、margin、gap) | TailwindCSS | |
154 +| 排版(font-size、text-align) | TailwindCSS | |
155 +| 颜色(bg-*、text-*、border-*) | TailwindCSS | |
156 +| 响应式设计(sm:、md:、lg:) | TailwindCSS | |
157 +| 组件特定样式(需要 scoped) | Less | 20% |
158 +| 深度选择器(`:deep()`) | Less | |
159 +| 动画和过渡 | Less | |
160 +| 伪元素(`::before``::after`) | Less | |
161 +
162 +[详见样式处理策略](docs/lessons-learned.md#✅-tailwindcss-vs-less-使用指南)
163 +
164 +### 7. 响应式优化
165 +
166 +**处理组件对象响应式**
167 +
168 +```javascript
169 +// ❌ BAD - 深度响应式导致性能问题
170 +const menuItems = ref([
171 + { icon: IconFont, name: 'heart' } // Vue 会深度代理组件对象
172 +])
173 +
174 +// ✅ GOOD - 使用 shallowRef + markRaw
175 +import { shallowRef, markRaw } from 'vue'
176 +
177 +const menuItems = shallowRef([
178 + { icon: markRaw(IconFont), name: 'heart' } // 避免深度代理
179 +])
180 +```
181 +
182 +[详见性能优化](docs/lessons-learned.md#✅-1-响应式数据优化)
135 183
136 ### 页面结构 184 ### 页面结构
137 185
...@@ -224,7 +272,7 @@ src/pages/your-page/ ...@@ -224,7 +272,7 @@ src/pages/your-page/
224 - 标签栏配置(可选) 272 - 标签栏配置(可选)
225 - 分包(如需要) 273 - 分包(如需要)
226 274
227 -## 重要实现细节 275 +## 🔧 重要实现细节
228 276
229 ### 会话管理 277 ### 会话管理
230 - 会话 ID 存储在 `localStorage` 中,键名为 `sessionid` 278 - 会话 ID 存储在 `localStorage` 中,键名为 `sessionid`
...@@ -252,44 +300,15 @@ NutUI 组件通过 unplugin-vue-components 自动导入(`config/index.js:91-93 ...@@ -252,44 +300,15 @@ NutUI 组件通过 unplugin-vue-components 自动导入(`config/index.js:91-93
252 - 为小程序兼容性禁用了 Preflight 300 - 为小程序兼容性禁用了 Preflight
253 - 启用了 `rem2rpx` 转换 301 - 启用了 `rem2rpx` 转换
254 - 内容路径配置在 `tailwind.config.js` 302 - 内容路径配置在 `tailwind.config.js`
255 -- 使用 Tailwind 处理布局、间距、颜色 303 +- [详见样式处理策略](docs/lessons-learned.md#✅-tailwindcss-vs-less-使用指南)
256 -- 使用 Less 处理组件特定样式、动画、伪元素
257 -
258 -### 样式指南
259 -
260 -**何时使用 TailwindCSS**(80% 的情况):
261 -```vue
262 -<div class="flex items-center justify-between p-4 bg-white">
263 - <h1 class="text-xl font-bold text-gray-900">标题</h1>
264 -</div>
265 -```
266 -
267 -**何时使用 Less**(20% 的情况):
268 -- 组件特定样式
269 -- 深度选择器(`:deep()`
270 -- 动画和过渡
271 -- 伪元素(`::before``::after`
272 -```less
273 -<style lang="less" scoped>
274 -.custom-card {
275 - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
276 - border-radius: 16px;
277 -
278 - :deep(.nut-button) {
279 - background-color: rgba(255, 255, 255, 0.2);
280 - }
281 -}
282 -</style>
283 -```
284 304
285 -## 可选功能 305 +## 📦 可选功能
286 306
287 如果不需要,可以移除以下功能: 307 如果不需要,可以移除以下功能:
288 - **微信支付**`src/utils/wechatPay.js``src/api/wx/pay.js` 308 - **微信支付**`src/utils/wechatPay.js``src/api/wx/pay.js`
289 - **二维码**`src/components/qrCode.vue``src/components/qrCodeSearch.vue` 309 - **二维码**`src/components/qrCode.vue``src/components/qrCodeSearch.vue`
290 - **海报生成器**`src/components/PosterBuilder/` 310 - **海报生成器**`src/components/PosterBuilder/`
291 - **时间选择器**`src/components/time-picker-data/` 311 - **时间选择器**`src/components/time-picker-data/`
292 -- **离线缓存**:整个离线预约缓存系统(如果不使用)
293 312
294 ## 开发工作流 313 ## 开发工作流
295 314
...@@ -509,30 +528,38 @@ store.setState('新值') ...@@ -509,30 +528,38 @@ store.setState('新值')
509 - 验证页面目录结构与路由匹配 528 - 验证页面目录结构与路由匹配
510 - 使用 `useGo` hook 进行一致的导航 529 - 使用 `useGo` hook 进行一致的导航
511 530
512 -## 最佳实践 531 +## 最佳实践
513 532
514 ### 组件开发 533 ### 组件开发
515 -- 使用 `<script setup>` 语法 534 +- 使用 `<script setup>` 语法
516 -- 使用 Composables 处理可复用逻辑 535 +- 使用 Composables 处理可复用逻辑
517 -- Props 应该有类型定义 536 +- Props 应该有类型定义
518 -- 使用 `emit` 进行子到父通信 537 +- 使用 `emit` 进行子到父通信
519 -- 优先使用 TailwindCSS 进行样式设计 538 +- 优先使用 TailwindCSS 进行样式设计
520 539
521 ### API 集成 540 ### API 集成
522 -- 始终检查 `res.code === 1` 判断成功 541 +- 始终检查 `res.code === 1` 判断成功
523 -- 使用 `try/catch` 进行错误处理 542 +- 使用 `try/catch` 进行错误处理
524 -- 请求期间显示加载状态 543 +- 请求期间显示加载状态
525 -- 优雅地处理网络错误 544 +- 优雅地处理网络错误
526 545
527 ### 性能 546 ### 性能
528 -- 使用页面懒加载(分包) 547 +- ✅ 使用页面懒加载(分包)
529 -- 使用 CDN 参数优化图片 548 +- ✅ 使用 CDN 参数优化图片
530 -- 避免无分页的大数据集 549 +- ✅ 避免无分页的大数据集
531 --`onUnmounted` 中清理定时器和监听器 550 +- ✅ 在 `onUnmounted` 中清理定时器和监听器
532 - 551 +- ✅ 使用 `shallowRef` + `markRaw` 处理组件对象
533 -### 代码风格 552 +
534 -- 遵循 Vue 3 Composition API 模式 553 +### 代码质量
535 -- 使用描述性变量名 554 +- ✅ 遵循 Vue 3 Composition API 模式
536 -- 保持函数聚焦且简短(< 50 行) 555 +- ✅ 使用描述性变量名
537 -- 为复杂函数添加 JSDoc 注释 556 +- ✅ 保持函数聚焦且简短(< 50 行)
538 -- 提交前运行 `pnpm lint` 557 +-**所有函数必须有 JSDoc 注释** [详见代码注释规范](~/.claude/rules/code-commenting.md)
558 +- ✅ 提交前运行 `pnpm lint`
559 +
560 +### 代码复用
561 +- ✅ 遵循"第 3 次出现原则" - 代码重复 3 次时必须抽取
562 +- ✅ 优先使用 Composables 而非 Mixins
563 +- ✅ 组件职责单一,避免过度复杂
564 +
565 +[更多最佳实践详见经验教训总结](docs/lessons-learned.md)
......
1 -# manulife-weapp 1 +# Manulife WeApp(臻奇智荟圈)
2 2
3 -基于 Taro 4 + Vue 3 + NutUI 的微信小程序模板项目 3 +> 基于 Taro 4 + Vue 3 + NutUI 构建的财富管理微信小程序
4 +
5 +## 📚 项目文档
6 +
7 +- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
8 +- **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
4 9
5 ## 🚀 快速开始 10 ## 🚀 快速开始
6 11
...@@ -26,49 +31,88 @@ pnpm dev:alipay ...@@ -26,49 +31,88 @@ pnpm dev:alipay
26 pnpm build:weapp 31 pnpm build:weapp
27 ``` 32 ```
28 33
34 +### 代码检查
35 +```bash
36 +pnpm lint
37 +```
38 +
39 +## 🌟 核心特性
40 +
41 +-**完整的认证系统** - 静默认证 + 401 自动刷新
42 +-**双设计宽度系统** - NutUI: 375px, 其他: 750px
43 +-**响应式优化** - shallowRef + markRaw 处理组件对象
44 +-**样式方案** - TailwindCSS(80%) + Less(20%) 混合使用
45 +-**组件复用** - "第 3 次出现原则"抽取 Composables
46 +-**可复用组件** - TabBar、NavHeader、IconFont
47 +
48 +## ⚡ 常见问题
49 +
50 +### NutUI 组件使用陷阱
51 +
52 +| 问题 | 解决方案 | 详细文档 |
53 +|------|---------|---------|
54 +| textarea 样式无法覆盖 | 使用原生 `<textarea>` | [经验教训](docs/lessons-learned.md#❌-坑-1-nutui-textarea-样式无法覆盖) |
55 +| IconFont 动态切换不响应 | 添加 `:key="name"` | [经验教训](docs/lessons-learned.md#❌-坑-2-iconfont-动态切换不响应) |
56 +
57 +### 静态资源加载问题
58 +
59 +| 问题 | 解决方案 | 详细文档 |
60 +|------|---------|---------|
61 +| SVG 图标加载失败(500 错误) | 使用 `import` 导入 | [经验教训](docs/lessons-learned.md#❌-坑-svg-图标加载失败500-错误) |
62 +
63 +### 代码质量
64 +
65 +- ✅ 所有函数必须有 JSDoc 注释
66 +- ✅ 遵循"第 3 次出现原则"抽取代码
67 +- ✅ 使用 `shallowRef` + `markRaw` 优化性能
68 +
69 +[详见经验教训总结](docs/lessons-learned.md)
70 +
29 ## 📁 项目结构 71 ## 📁 项目结构
30 72
31 ``` 73 ```
32 src/ 74 src/
33 ├── api/ # API 接口层 75 ├── api/ # API 接口层
34 -│ ├── index.js # 业务接口定义(需根据业务修改) 76 +│ ├── index.js # 业务接口定义
35 -│ ├── fn.js # HTTP 请求封装 77 +│ └── fn.js # HTTP 请求封装
36 -│ └── wx/ # 微信相关接口(可选)
37 ├── assets/ # 静态资源 78 ├── assets/ # 静态资源
38 │ ├── images/ # 图片资源 79 │ ├── images/ # 图片资源
39 -│ ├── styles/ # 全局样式 80 +│ └── styles/ # 全局样式
40 -│ └── css/ # CSS 文件
41 ├── components/ # 通用组件 81 ├── components/ # 通用组件
82 +│ ├── IconFont.vue # 图标字体组件
83 +│ ├── NavHeader.vue # 自定义导航头
84 +│ ├── TabBar.vue # 底部导航栏
85 +│ ├── SectionCard.vue # 分组卡片组件
86 +│ ├── FilterTabs.vue # 筛选标签组件
42 │ ├── PosterBuilder/ # 海报生成器(可选) 87 │ ├── PosterBuilder/ # 海报生成器(可选)
43 -│ ├── time-picker-data/ # 时间选择器 88 +│ └── PlanSchemes/ # 计划书方案组件
44 -│ ├── PlanPopup/ # 录入计划书弹窗容器
45 -│ ├── PlanSchemes/ # 录入计划书方案组件
46 -│ ├── indexNav.vue # 底部导航
47 -│ └── qrCode.vue # 二维码组件(可选)
48 ├── composables/ # Composition API hooks 89 ├── composables/ # Composition API hooks
49 -│ ├── useOfflineBookingCache.js # 离线缓存 hook 90 +│ ├── useSectionList.js # 分组列表管理
50 -│ └── useOfflineBookingCachePolling.js # 轮询刷新 hook 91 +│ ├── useFileOperation.js # 文件操作(下载、预览)
92 +│ └── useListItemClick.js # 列表点击处理
51 ├── hooks/ # 自定义 hooks 93 ├── hooks/ # 自定义 hooks
52 -│ └── useGo.js # 导航辅助 hook 94 +│ └── useGo.js # 增强导航 hook
53 ├── pages/ # 页面组件 95 ├── pages/ # 页面组件
54 -│ ├── index/ # 首页(示例页面) 96 +│ ├── index/ # 首页
55 -│ └── auth/ # 认证页(必须保留) 97 +│ ├── auth/ # 认证页(必须保留)
98 +│ ├── login/ # 登录页
99 +│ ├── product-detail/ # 产品详情
100 +│ ├── material-list/ # 资料列表
101 +│ ├── knowledge-base/ # 知识库
102 +│ ├── plan/ # 计划书
103 +│ ├── favorites/ # 我的收藏
104 +│ ├── mine/ # 我的
105 +│ └── ...
56 ├── stores/ # Pinia 状态管理 106 ├── stores/ # Pinia 状态管理
57 -│ ├── router.js # 路由状态(用于认证回跳) 107 +│ ├── router.js # 路由状态(认证回跳)
58 │ ├── main.js # 主 store 108 │ ├── main.js # 主 store
59 -│ ├── host.js # 配置 store 109 +│ └── host.js # 配置 store
60 -│ └── counter.js # 示例 store
61 ├── utils/ # 工具函数 110 ├── utils/ # 工具函数
62 │ ├── authRedirect.js # 认证流程核心(必须) 111 │ ├── authRedirect.js # 认证流程核心(必须)
63 │ ├── request.js # HTTP 客户端核心(必须) 112 │ ├── request.js # HTTP 客户端核心(必须)
64 -│ ├── network.js # 网络状态监测
65 │ ├── config.js # 环境配置(⚠️ 需修改) 113 │ ├── config.js # 环境配置(⚠️ 需修改)
66 │ ├── tools.js # 通用工具 114 │ ├── tools.js # 通用工具
67 -│ ├── uiText.js # 文案管理 115 +│ └── documentIcons.js # 文档图标工具
68 -│ ├── wechatPay.js # 微信支付(可选)
69 -│ ├── mixin.js # Vue mixin
70 -│ ├── polyfill.js # 浏览器兼容
71 -│ └── weapp.js # 小程序工具
72 ├── app.js # 应用入口 116 ├── app.js # 应用入口
73 ├── app.config.js # 页面路由配置 117 ├── app.config.js # 页面路由配置
74 └── app.less # 全局样式 118 └── app.less # 全局样式
...@@ -76,15 +120,20 @@ src/ ...@@ -76,15 +120,20 @@ src/
76 120
77 ## 📄 页面说明 121 ## 📄 页面说明
78 122
79 -- 计划书页面支持顶部搜索与标签固定,列表区域独立滚动,便于快速筛选和浏览 123 +### 主要页面
80 -- 资料列表页面支持顶部搜索与分类固定,列表区域独立滚动,便于批量浏览与筛选 124 +- **首页** - 产品展示、热门资料、导航入口
81 -- 收藏页面支持顶部筛选固定,列表区域独立滚动 125 +- **产品详情** - 完整产品信息展示
82 -- 知识库页面支持顶部筛选固定,列表区域独立滚动 126 +- **资料列表** - 培训材料和文档管理
83 -- 资料列表、知识库、收藏、计划书页面统一使用 FilterTabs 组件进行横向筛选 127 +- **知识库** - 培训材料和案例知识库
84 - 128 +- **计划书** - 计划书列表和管理
85 -## ✅ 优化建议 129 +- **我的收藏** - 收藏内容管理
86 - 130 +- **我的** - 个人资料、功能菜单
87 -- 新增筛选类页面优先复用 FilterTabs,避免重复样式与交互逻辑 131 +
132 +### 页面特性
133 +- ✅ 顶部筛选固定,列表独立滚动
134 +- ✅ 统一的导航头(NavHeader)和底部导航(TabBar)
135 +- ✅ 统一的文件操作逻辑(下载、预览)
136 +- ✅ 统一的列表点击处理
88 137
89 ## ⚙️ 配置说明 138 ## ⚙️ 配置说明
90 139
...@@ -105,11 +154,9 @@ export const REQUEST_DEFAULT_PARAMS = { ...@@ -105,11 +154,9 @@ export const REQUEST_DEFAULT_PARAMS = {
105 154
106 ### 2. 定义 API 接口 155 ### 2. 定义 API 接口
107 156
108 -编辑 `src/api/index.js`,添加您的业务接口 157 +编辑 `src/api/index.js`
109 158
110 ```javascript 159 ```javascript
111 -import { buildApiUrl } from '@/utils/tools'
112 -
113 export const yourAPI = (params) => { 160 export const yourAPI = (params) => {
114 return buildApiUrl('your_action', params) 161 return buildApiUrl('your_action', params)
115 } 162 }
...@@ -117,7 +164,7 @@ export const yourAPI = (params) => { ...@@ -117,7 +164,7 @@ export const yourAPI = (params) => {
117 164
118 ### 3. 配置页面路由 165 ### 3. 配置页面路由
119 166
120 -编辑 `src/app.config.js`,添加您的页面 167 +编辑 `src/app.config.js`
121 168
122 ```javascript 169 ```javascript
123 export default { 170 export default {
...@@ -126,24 +173,22 @@ export default { ...@@ -126,24 +173,22 @@ export default {
126 'pages/auth/index', 173 'pages/auth/index',
127 'pages/your-page/index', // 添加您的页面 174 'pages/your-page/index', // 添加您的页面
128 ], 175 ],
129 - // ...
130 } 176 }
131 ``` 177 ```
132 178
133 ### 4. 双设计宽度体系 179 ### 4. 双设计宽度体系
134 180
135 -项目采用双设计宽度体系:
136 - **NutUI 组件**:基准宽度 375px 181 - **NutUI 组件**:基准宽度 375px
137 - **其他所有页面**:基准宽度 750px 182 - **其他所有页面**:基准宽度 750px
138 183
139 -此配置已在 `config/index.js` 中设置,请确保遵循此规范。 184 +[详见双设计宽度系统使用指南](docs/lessons-learned.md#⚠️-双设计宽度系统)
140 185
141 ## 🔐 认证流程 186 ## 🔐 认证流程
142 187
143 项目内置完整的微信登录认证系统: 188 项目内置完整的微信登录认证系统:
144 189
145 -1. **静默认证**:应用启动时自动执行静默认证 190 +1. **静默认证**:应用启动时自动执行
146 -2. **401 自动刷新**当接口返回 401 时,自动刷新会话并重试请求 191 +2. **401 自动刷新**接口返回 401 时自动刷新会话
147 3. **授权页回跳**:认证完成后自动跳转回原页面 192 3. **授权页回跳**:认证完成后自动跳转回原页面
148 193
149 **核心文件** 194 **核心文件**
...@@ -152,19 +197,6 @@ export default { ...@@ -152,19 +197,6 @@ export default {
152 197
153 **重要**:后端需提供 `/srv/?a=openid_wxapp` 接口用于微信登录。 198 **重要**:后端需提供 `/srv/?a=openid_wxapp` 接口用于微信登录。
154 199
155 -## 🌐 弱网/离线支持
156 -
157 -项目内置弱网和离线支持:
158 -
159 -1. **请求超时处理**:自动检测网络超时并降级处理
160 -2. **离线缓存**:支持离线数据缓存和读取
161 -3. **弱网提示**:统一的弱网提示文案
162 -
163 -**相关文件**
164 -- `src/composables/useOfflineBookingCache.js`
165 -- `src/composables/useOfflineBookingCachePolling.js`
166 -- `src/utils/uiText.js`
167 -
168 ## 📦 技术栈 200 ## 📦 技术栈
169 201
170 - **框架**:Taro 4.x 202 - **框架**:Taro 4.x
...@@ -176,8 +208,6 @@ export default { ...@@ -176,8 +208,6 @@ export default {
176 208
177 ## 🎯 路径别名 209 ## 🎯 路径别名
178 210
179 -已配置的路径别名:
180 -
181 ```javascript 211 ```javascript
182 @/utils -> src/utils 212 @/utils -> src/utils
183 @/components -> src/components 213 @/components -> src/components
...@@ -192,28 +222,22 @@ export default { ...@@ -192,28 +222,22 @@ export default {
192 ## 📝 开发规范 222 ## 📝 开发规范
193 223
194 ### 组件编写 224 ### 组件编写
195 -- 使用 Vue 3 Composition API 225 +- ✅ 使用 Vue 3 Composition API
196 -- 组件统一放在 `src/components/` 目录 226 +- ✅ Props 定义清晰,有类型和默认值
197 -- Props 定义清晰,注释详细 227 +-**所有函数必须有 JSDoc 注释**
198 -- 图标组件直接使用 `@nutui/icons-vue-taro``IconFont`
199 -- **代码注释**:所有函数和方法必须使用 JSDoc 注释,详细说明功能、参数、返回值
200 228
201 ### API 调用 229 ### API 调用
202 -- 接口统一在 `src/api/index.js` 定义 230 +- ✅ 接口统一在 `src/api/index.js` 定义
203 -- 使用 `xxxAPI(params)` 命名格式 231 +- ✅ 使用 `xxxAPI(params)` 命名格式
204 -- 请求方法统一使用 `src/api/fn.js` 中的封装 232 +-**始终检查 `res.code === 1` 判断成功**
205 -- **代码注释**:每个 API 接口都需要 JSDoc 注释说明用途和参数
206 233
207 ### 状态管理 234 ### 状态管理
208 -- 使用 Pinia 进行状态管理 235 +- ✅ 使用 Pinia 进行状态管理
209 -- Store 文件统一放在 `src/stores/` 目录 236 +- ✅ 复杂逻辑使用 composables 封装
210 -- 复杂逻辑使用 composables 封装
211 -- **代码注释**:Store 的 state、actions 都需要详细注释
212 237
213 ### 样式编写 238 ### 样式编写
214 -- 通用样式使用 TailwindCSS 工具类 239 +- ✅ TailwindCSS: 布局、间距、颜色(80%)
215 -- 组件样式使用 Less 240 +- ✅ Less: 组件样式、动画、伪元素(20%)
216 -- NutUI 组件使用 375px 设计稿,其他使用 750px
217 241
218 ### 代码注释要求 242 ### 代码注释要求
219 遵循全局代码注释规范(`~/.claude/rules/code-commenting.md`): 243 遵循全局代码注释规范(`~/.claude/rules/code-commenting.md`):
...@@ -221,8 +245,6 @@ export default { ...@@ -221,8 +245,6 @@ export default {
221 - ✅ 包含 `@description` 说明功能 245 - ✅ 包含 `@description` 说明功能
222 - ✅ 所有参数都有 `@param` 说明 246 - ✅ 所有参数都有 `@param` 说明
223 - ✅ 返回值有 `@returns` 说明 247 - ✅ 返回值有 `@returns` 说明
224 -- ✅ 复杂逻辑需要详细注释
225 -- ✅ 正则表达式需要说明含义
226 248
227 ## 🔧 可选功能 249 ## 🔧 可选功能
228 250
...@@ -230,13 +252,14 @@ export default { ...@@ -230,13 +252,14 @@ export default {
230 252
231 1. **二维码组件**`src/components/qrCode.vue` 253 1. **二维码组件**`src/components/qrCode.vue`
232 2. **海报生成器**`src/components/PosterBuilder/` 254 2. **海报生成器**`src/components/PosterBuilder/`
233 -3. **微信支付**`src/utils/wechatPay.js``src/api/wx/` 255 +3. **微信支付**`src/utils/wechatPay.js`
234 4. **时间选择器**`src/components/time-picker-data/` 256 4. **时间选择器**`src/components/time-picker-data/`
235 -5. **离线缓存**`src/composables/useOfflineBookingCache.js`
236 257
237 ## 📚 相关文档 258 ## 📚 相关文档
238 259
239 -- [Taro 官方文档](https://taro-docs.jd.com/) 260 +- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
261 +- **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
262 +- [Taro 官方文档](https://docs.taro.zone/)
240 - [NutUI 文档](https://nutui.jd.com/taro/) 263 - [NutUI 文档](https://nutui.jd.com/taro/)
241 - [Vue 3 文档](https://cn.vuejs.org/) 264 - [Vue 3 文档](https://cn.vuejs.org/)
242 - [Pinia 文档](https://pinia.vuejs.org/zh/) 265 - [Pinia 文档](https://pinia.vuejs.org/zh/)
...@@ -248,6 +271,7 @@ export default { ...@@ -248,6 +271,7 @@ export default {
248 3. NutUI 组件已配置自动导入,无需手动引入 271 3. NutUI 组件已配置自动导入,无需手动引入
249 4. TailwindCSS 已禁用 preflight,避免与小程序样式冲突 272 4. TailwindCSS 已禁用 preflight,避免与小程序样式冲突
250 5. 认证失败会自动跳转到 `/pages/auth/index` 273 5. 认证失败会自动跳转到 `/pages/auth/index`
274 +6. **所有函数必须有 JSDoc 注释** - 详见 `~/.claude/rules/code-commenting.md`
251 275
252 ## 📄 License 276 ## 📄 License
253 277
......
1 +# 文档优化总结
2 +
3 +## 📝 变更说明
4 +
5 +本次优化整理了项目文档,创建了经验教训总结文档,并优化了 CLAUDE.md 和 README.md。
6 +
7 +## ✨ 新增文档
8 +
9 +### [docs/lessons-learned.md](docs/lessons-learned.md)
10 +
11 +**完整的 Taro + Vue 3 项目开发经验教训总结**,包含:
12 +
13 +#### 核心章节
14 +1. **组件抽取与复用**
15 + - "第 3 次出现原则"(Rule of Three)
16 + - 何时抽取组件和 Composables
17 + - 实际案例分析(useSectionList、useFileOperation、useListItemClick)
18 +
19 +2. **NutUI 组件使用陷阱**
20 + - textarea 样式无法覆盖 → 使用原生组件
21 + - IconFont 动态切换不响应 → 添加 `:key="name"`
22 + - 最佳实践建议
23 +
24 +3. **静态资源加载问题**
25 + - SVG 图标加载失败(500 错误)解决方案
26 + - 使用 `import` 导入静态资源
27 +
28 +4. **样式处理策略**
29 + - TailwindCSS vs Less 使用指南(80% vs 20%)
30 + - 双设计宽度系统详解
31 + - 使用场景对比表格
32 +
33 +5. **性能优化**
34 + - 响应式数据优化(shallowRef + markRaw)
35 + - 页面滚动优化(固定顶部 + 独立滚动)
36 + - 图片优化策略
37 +
38 +6. **代码质量**
39 + - JSDoc 注释规范
40 + - 命名规范
41 + - 错误处理模式
42 +
43 +7. **架构设计**
44 + - 统一的列表点击处理
45 + - 统一的文件操作
46 + - 分层架构原则
47 +
48 +## 🔄 优化文档
49 +
50 +### [CLAUDE.md](CLAUDE.md)
51 +
52 +**主要优化**:
53 +- ✅ 添加快速参考表格,常见问题一目了然
54 +- ✅ 核心架构模式章节更清晰
55 +- ✅ 添加样式处理策略和响应式优化章节
56 +- ✅ 删除重复内容,精简文档结构
57 +- ✅ 添加对经验教训文档的交叉引用
58 +- ✅ 使用表情符号和表格提升可读性
59 +
60 +**关键改进**:
61 +- 新增"快速参考"部分,常见问题快速解决
62 +- 重新组织"核心架构"章节,结构更清晰
63 +- 突出"第 3 次出现原则"等核心经验
64 +- 添加更多代码示例和最佳实践
65 +
66 +### [README.md](README.md)
67 +
68 +**主要优化**:
69 +- ✅ 添加项目文档索引(经验教训、CLAUDE.md)
70 +- ✅ 新增"核心特性"章节
71 +- ✅ 新增"常见问题"快速参考表格
72 +- ✅ 精简项目结构,移除过时内容
73 +- ✅ 优化开发规范说明
74 +- ✅ 删除离线缓存等不再维护的功能说明
75 +
76 +**关键改进**:
77 +- 突出项目的核心特性
78 +- 提供常见问题的快速解决方案
79 +- 更清晰的项目结构说明
80 +- 强调代码注释和开发规范
81 +
82 +## 📊 文档对比
83 +
84 +### 优化前
85 +- ❌ CLAUDE.md 过于详细,缺乏结构化
86 +- ❌ README.md 缺少项目亮点和常见问题
87 +- ❌ 经验教训散落在 CHANGELOG 中,不便于查阅
88 +- ❌ 缺少对 NutUI 陷阱、静态资源等问题的系统性总结
89 +
90 +### 优化后
91 +- ✅ 经验教训系统化整理,便于查阅
92 +- ✅ 文档结构清晰,使用表格和表情符号提升可读性
93 +- ✅ 添加交叉引用,相关文档相互链接
94 +- ✅ 突出核心经验:"第 3 次出现原则"、样式策略、性能优化
95 +- ✅ 提供常见问题的快速解决方案
96 +
97 +## 🎯 使用指南
98 +
99 +### 对于新开发者
100 +
101 +1. **首先阅读**: [README.md](README.md)
102 + - 了解项目概况
103 + - 查看快速开始指南
104 + - 阅读常见问题
105 +
106 +2. **深入学习**: [docs/lessons-learned.md](docs/lessons-learned.md)
107 + - 学习 Taro 项目开发的最佳实践
108 + - 了解常见陷阱和解决方案
109 + - 掌握组件抽取、样式处理、性能优化等核心技巧
110 +
111 +3. **开发参考**: [CLAUDE.md](CLAUDE.md)
112 + - 查看项目架构和核心模式
113 + - 参考 API 层、认证流程、导航系统等实现
114 + - 使用快速参考表格解决常见问题
115 +
116 +### 对于 Claude Code
117 +
118 +- **主要参考**: [CLAUDE.md](CLAUDE.md)
119 +- **辅助参考**: [docs/lessons-learned.md](docs/lessons-learned.md)
120 +- **遵循规范**: `~/.claude/rules/code-commenting.md`
121 +
122 +## 📌 核心经验总结
123 +
124 +### 1. "第 3 次出现原则"
125 +
126 +当相同代码模式出现 3 次时,**必须**抽取为 Composable 或组件。
127 +
128 +**实际收益**:
129 +- 减少 60-290 行重复代码
130 +- 提升可维护性
131 +- 统一修改点
132 +
133 +### 2. NutUI 组件陷阱
134 +
135 +| 问题 | 解决方案 |
136 +|------|---------|
137 +| textarea 样式无法覆盖 | 使用原生 `<textarea>` |
138 +| IconFont 动态切换不响应 | 添加 `:key="name"` |
139 +
140 +### 3. 静态资源加载
141 +
142 +- ❌ 错误: 字符串路径 `/assets/images/icon.svg`
143 +- ✅ 正确: `import icon from '@/assets/images/icon.svg'`
144 +
145 +### 4. 样式策略
146 +
147 +- **TailwindCSS** (80%): 布局、间距、颜色
148 +- **Less** (20%): 组件样式、动画、伪元素
149 +
150 +### 5. 性能优化
151 +
152 +- 使用 `shallowRef` + `markRaw` 处理组件对象
153 +- 固定顶部 + 列表独立滚动
154 +- CDN 参数优化图片
155 +
156 +## 🔄 持续改进
157 +
158 +本文档会随着项目开发持续更新,记录新的经验教训。
159 +
160 +**下次更新建议**:
161 +- 添加更多实际案例
162 +- 补充性能优化数据
163 +- 添加架构图和流程图
164 +
165 +---
166 +
167 +**优化日期**: 2026-01-31
168 +**维护者**: Claude Code
169 +**项目**: Manulife WeApp
1 +# Taro + Vue 3 项目开发经验教训总结
2 +
3 +> 本文档总结了在开发 Manulife WeApp 项目过程中遇到的问题、解决方案和最佳实践,为后续 Taro 项目提供参考。
4 +
5 +## 目录
6 +
7 +- [组件抽取与复用](#组件抽取与复用)
8 +- [NutUI 组件使用陷阱](#nutui-组件使用陷阱)
9 +- [静态资源加载问题](#静态资源加载问题)
10 +- [样式处理策略](#样式处理策略)
11 +- [性能优化](#性能优化)
12 +- [代码质量](#代码质量)
13 +- [架构设计](#架构设计)
14 +
15 +---
16 +
17 +## 组件抽取与复用
18 +
19 +### ✅ 最佳实践
20 +
21 +#### 1. "第 3 次出现原则"(Rule of Three)
22 +
23 +**原则**:当相同代码模式出现 3 次时,**必须**抽取为 Composable 或组件。
24 +
25 +**案例 1: useSectionList Composable**
26 +- **问题**: `onboarding`, `family-office`, `signing` 三个页面都使用相同的分组列表模式
27 +- **解决**: 创建 `src/composables/useSectionList.js`
28 +- **收益**: 减少约 60 行重复代码,提升可维护性
29 +
30 +```javascript
31 +// src/composables/useSectionList.js
32 +/**
33 + * 分组列表页面 Composable
34 + *
35 + * @description 统一管理分组列表数据和点击事件
36 + * @param {Object} sectionData - 分组数据
37 + * @param {Function} handleClick - 点击回调函数
38 + * @returns {Object} { sections, onItemClick }
39 + */
40 +export function useSectionList(sectionData, handleClick) {
41 + const sections = ref(sectionData)
42 +
43 + const onItemClick = (item) => {
44 + handleClick(item)
45 + }
46 +
47 + return { sections, onItemClick }
48 +}
49 +```
50 +
51 +**案例 2: useFileOperation Composable**
52 +- **问题**: 收藏页和产品详情页都有约 200 行重复的文件操作代码
53 +- **解决**: 创建 `src/composables/useFileOperation.js`
54 +- **收益**: 减少 ~290 行重复代码,统一修改点
55 +
56 +**案例 3: useListItemClick Composable**
57 +- **问题**: 多个列表页面的点击逻辑分散且重复
58 +- **解决**: 创建 `src/composables/useListItemClick.js`
59 +- **收益**: 支持上下文感知的行为路由,扩展性强
60 +
61 +#### 2. 组件设计原则
62 +
63 +**案例: SectionCard 默认渐变色优化**
64 +- **问题**: 三个页面都重复设置默认渐变色
65 +- **解决**: 在 `SectionCard` 组件内部使用 `computed` 属性内置默认值
66 +- **收益**: 修改默认值只需在一处,简化页面代码
67 +
68 +```vue
69 +<!-- src/components/SectionCard.vue -->
70 +<script setup>
71 +const props = defineProps({
72 + bgGradient: {
73 + type: String,
74 + default: ''
75 + }
76 +})
77 +
78 +// ✅ GOOD - 内置默认渐变色
79 +const computedBgGradient = computed(() => {
80 + return props.bgGradient || 'linear-gradient(90deg, #EFF6FF 0%, #DBEAFE 100%)'
81 +})
82 +</script>
83 +```
84 +
85 +#### 3. 何时抽取组件
86 +
87 +**触发条件**:
88 +- ✅ 代码重复 ≥ 2 次 → 警惕
89 +- ✅ 代码重复 ≥ 3 次 → **必须抽取**
90 +- ✅ v-for 模板超过 5 行 → 提取列表项组件
91 +- ✅ 组件模板超过 150 行 → 拆分组件
92 +- ✅ 函数超过 50 行 → 拆分函数
93 +
94 +### ❌ 反模式
95 +
96 +#### 1. 过早抽取
97 +
98 +```javascript
99 +// ❌ BAD - 代码只出现 1 次就抽取
100 +function useSpecificFeature() {
101 + // 只在一个地方使用...
102 +}
103 +
104 +// ✅ GOOD - 等待第 3 次出现再抽取
105 +// 第 1、2 次直接实现,第 3 次时抽取
106 +```
107 +
108 +#### 2. 过度抽象
109 +
110 +```javascript
111 +// ❌ BAD - 过度通用的抽象,难以理解
112 +function useGenericList({ config, handlers, options }) {
113 + // 太多参数,过于复杂
114 +}
115 +
116 +// ✅ GOOD - 专注特定场景
117 +function useSectionList(sectionData, handleClick) {
118 + // 简单直接,易于理解
119 +}
120 +```
121 +
122 +---
123 +
124 +## NutUI 组件使用陷阱
125 +
126 +### ❌ 坑 1: NutUI textarea 样式无法覆盖
127 +
128 +**问题描述**:
129 +```vue
130 +<!-- ❌ 无法生效 -->
131 +<nut-textarea
132 + v-model="content"
133 + class="custom-textarea"
134 +/>
135 +```
136 +
137 +```less
138 +// ❌ 深度选择器无效
139 +.custom-textarea :deep(.nut-textarea__textarea) {
140 + padding: 24rpx;
141 +}
142 +```
143 +
144 +**原因**: NutUI textarea 组件使用 Shadow DOM 或内部样式隔离
145 +
146 +**解决方案**: 使用原生小程序 `<textarea>` 组件
147 +```vue
148 +<!-- ✅ 完全控制样式 -->
149 +<textarea
150 + v-model="content"
151 + class="custom-textarea"
152 + maxlength="200"
153 + @input="handleInput"
154 +/>
155 +<view class="char-count">{{ content.length }}/200</view>
156 +```
157 +
158 +### ❌ 坑 2: IconFont 动态切换不响应
159 +
160 +**问题描述**:
161 +```vue
162 +<!-- ❌ 图标不更新 -->
163 +<IconFont :name="iconName" />
164 +```
165 +
166 +```javascript
167 +const iconName = ref('heart')
168 +setTimeout(() => {
169 + iconName.value = 'heart-fill' // 视图不更新!
170 +}, 1000)
171 +```
172 +
173 +**原因**: NutUI 的 `IconFont` 组件在某些环境下未正确响应 props 变化
174 +
175 +**解决方案**: 添加 `:key` 强制重新渲染
176 +```vue
177 +<!-- ✅ 添加 key 属性 -->
178 +<IconFont :name="iconName" :key="iconName" />
179 +```
180 +
181 +### ✅ NutUI 最佳实践
182 +
183 +1. **优先使用原生组件**: 当 NutUI 组件样式限制时
184 +2. **添加 key 属性**: 动态切换图标时
185 +3. **深度样式覆盖**: 尝试 `:deep()` 选择器
186 +4. **查阅文档**: 确认 NutUI 版本和已知问题
187 +
188 +---
189 +
190 +## 静态资源加载问题
191 +
192 +### ❌ 坑: SVG 图标加载失败(500 错误)
193 +
194 +**问题描述**:
195 +```javascript
196 +// ❌ 字符串路径导致加载失败
197 +export const getDocumentIcon = (type) => {
198 + const icons = {
199 + pdf: '/assets/images/icon/doc/doc.svg', // ❌ 500 错误
200 + doc: '/assets/images/icon/doc/doc.svg',
201 + // ...
202 + }
203 + return icons[type]
204 +}
205 +```
206 +
207 +**原因**: Taro 构建工具无法正确处理字符串路径的静态资源引用
208 +
209 +**解决方案**: 使用 ES6 `import` 导入
210 +```javascript
211 +// ✅ 使用 import 导入
212 +import pdfIcon from '@/assets/images/icon/doc/pdf.svg'
213 +import docIcon from '@/assets/images/icon/doc/doc.svg'
214 +// ...
215 +
216 +export const getDocumentIcon = (type) => {
217 + const icons = {
218 + pdf: pdfIcon,
219 + doc: docIcon,
220 + // ...
221 + }
222 + return icons[type]
223 +}
224 +```
225 +
226 +### ✅ 静态资源处理规则
227 +
228 +| 资源类型 | 引用方式 | 示例 |
229 +|---------|---------|------|
230 +| **SVG 图标** | `import` 导入 | `import icon from '@/assets/icon.svg'` |
231 +| **图片** | `import` 导入或字符串路径 | 两种方式都支持 |
232 +| **字体** | 配置在 `config/index.js` | 参考项目配置 |
233 +| **远程资源** | 字符串 URL | `https://cdn.example.com/image.png` |
234 +
235 +---
236 +
237 +## 样式处理策略
238 +
239 +### ✅ TailwindCSS vs Less 使用指南
240 +
241 +#### 使用 TailwindCSS(80% 场景)
242 +
243 +**适用场景**:
244 +- 布局(flex、grid、absolute)
245 +- 间距(padding、margin、gap)
246 +- 排版(font-size、font-weight、text-align)
247 +- 颜色(bg-*、text-*、border-*
248 +- 响应式设计(sm:、md:、lg:)
249 +
250 +**示例**:
251 +```vue
252 +<div class="
253 + flex items-center justify-between <!-- 布局 -->
254 + p-4 mb-2 <!-- 间距 -->
255 + bg-white rounded-lg shadow-md <!-- 颜色、圆角、阴影 -->
256 +">
257 + <h1 class="text-xl font-bold">标题</h1>
258 +</div>
259 +```
260 +
261 +#### 使用 Less(20% 场景)
262 +
263 +**适用场景**:
264 +- 组件特定样式(需要 `scoped`
265 +- 深度选择器(`:deep()`
266 +- 动画和过渡
267 +- 伪元素(`::before``::after`
268 +- 复杂的计算表达式
269 +
270 +**示例**:
271 +```less
272 +<style lang="less" scoped>
273 +.custom-card {
274 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
275 + border-radius: 16px;
276 +
277 + // 深度选择器修改第三方组件
278 + :deep(.nut-button) {
279 + background-color: rgba(255, 255, 255, 0.2);
280 + }
281 +
282 + // 伪元素
283 + &::before {
284 + content: '';
285 + position: absolute;
286 + // ...
287 + }
288 +
289 + // 动画
290 + @keyframes slide-in {
291 + from { transform: translateX(-100%); }
292 + to { transform: translateX(0); }
293 + }
294 +}
295 +</style>
296 +```
297 +
298 +### ⚠️ 双设计宽度系统
299 +
300 +**项目配置**:
301 +```javascript
302 +// config/index.js
303 +const designWidth = {
304 + 750: [375, 375], // NutUI 组件: 375px 基准
305 + 750: [750, 750] // 其他所有页面: 750px 基准
306 +}
307 +```
308 +
309 +**使用规则**:
310 +- **NutUI 组件** → 参考 375px 设计稿
311 +- **自定义页面** → 参考 750px 设计稿
312 +
313 +**示例**:
314 +```vue
315 +<!-- NutUI 组件: 使用 375px 设计稿 -->
316 +<nut-button :custom-style="buttonStyle">按钮</nut-button>
317 +
318 +<script>
319 +const buttonStyle = {
320 + fontSize: '14px', // 375px 设计稿: 14px
321 + padding: '8px 16px'
322 +}
323 +</script>
324 +
325 +<!-- 自定义元素: 使用 750px 设计稿 -->
326 +<view class="custom-box">
327 + 内容
328 +</view>
329 +
330 +<style lang="less" scoped>
331 +.custom-box {
332 + width: 750px; // 750px 设计稿: 750px
333 + height: 200px;
334 +}
335 +</style>
336 +```
337 +
338 +---
339 +
340 +## 性能优化
341 +
342 +### ✅ 1. 响应式数据优化
343 +
344 +**问题**: Vue 3 对包含组件对象的响应式数据进行深度代理,导致性能问题
345 +
346 +**解决方案**: 使用 `shallowRef` + `markRaw`
347 +
348 +```javascript
349 +import { shallowRef, markRaw } from 'vue'
350 +
351 +// ❌ BAD - 深度响应式
352 +const menuItems = ref([
353 + {
354 + icon: markRaw(IconFont), // 组件对象
355 + name: 'heart',
356 + title: '我的收藏'
357 + }
358 + // ...
359 +])
360 +
361 +// ✅ GOOD - 浅层响应式
362 +const menuItems = shallowRef([
363 + {
364 + icon: markRaw(IconFont), // 标记为原始对象
365 + name: 'heart',
366 + title: '我的收藏'
367 + }
368 + // ...
369 +])
370 +```
371 +
372 +**收益**:
373 +- 消除 "Component that was made a reactive object" 警告
374 +- 避免不必要的深度代理
375 +- 提升页面初始化和渲染性能
376 +
377 +### ✅ 2. 页面滚动优化
378 +
379 +**问题**: 整页滚动,顶部筛选区域会随列表滚动消失
380 +
381 +**解决方案**: 固定顶部 + 列表独立滚动
382 +
383 +```vue
384 +<template>
385 + <view class="page-container">
386 + <!-- 固定顶部筛选 -->
387 + <view class="fixed-header">
388 + <search-bar />
389 + <filter-tabs />
390 + </view>
391 +
392 + <!-- 独立滚动列表 -->
393 + <scroll-view
394 + scroll-y
395 + class="scrollable-list"
396 + :style="{ height: listHeight }"
397 + >
398 + <view v-for="item in list" :key="item.id">
399 + {{ item.title }}
400 + </view>
401 + </scroll-view>
402 + </view>
403 +</template>
404 +
405 +<script setup>
406 +import { ref, onMounted } from 'vue'
407 +
408 +const listHeight = ref('calc(100vh - 200rpx)') // 减去固定区域高度
409 +
410 +onMounted(() => {
411 + // 动态计算列表高度
412 + const query = Taro.createSelectorQuery()
413 + query.select('.fixed-header').boundingClientRect()
414 + query.exec((res) => {
415 + const headerHeight = res[0].height
416 + listHeight.value = `calc(100vh - ${headerHeight}px)`
417 + })
418 +})
419 +</script>
420 +
421 +<style lang="less" scoped>
422 +.page-container {
423 + height: 100vh;
424 + display: flex;
425 + flex-direction: column;
426 +}
427 +
428 +.fixed-header {
429 + position: sticky;
430 + top: 0;
431 + z-index: 10;
432 + background-color: #fff;
433 +}
434 +
435 +.scrollable-list {
436 + flex: 1;
437 + overflow-y: auto;
438 +}
439 +</style>
440 +```
441 +
442 +**收益**:
443 +- 顶部筛选区域始终可见
444 +- 列表滚动更流畅
445 +- 用户体验显著提升
446 +
447 +### ✅ 3. 图片优化
448 +
449 +**策略**:
450 +1. 使用 CDN 参数优化图片
451 +2. 图片懒加载
452 +3. 响应式图片
453 +
454 +```javascript
455 +// src/utils/image.js
456 +/**
457 + * 优化 CDN 图片 URL
458 + *
459 + * @param {string} url - 原始 URL
460 + * @param {Object} options - 优化参数
461 + * @param {number} options.width - 宽度
462 + * @param {number} options.quality - 质量(1-100)
463 + */
464 +export function optimizeImageUrl(url, options = {}) {
465 + if (!url || !url.includes('cdn.ipadbiz.cn')) {
466 + return url
467 + }
468 +
469 + const { width = 750, quality = 70 } = options
470 + const params = new URLSearchParams({
471 + 'imageMogr2/thumbnail': `${width}x`,
472 + strip: '',
473 + quality: quality.toString()
474 + })
475 +
476 + return `${url}?${params.toString()}`
477 +}
478 +```
479 +
480 +---
481 +
482 +## 代码质量
483 +
484 +### ✅ 1. JSDoc 注释规范
485 +
486 +**强制要求**:
487 +- ✅ 所有函数必须有 JSDoc 注释
488 +- ✅ 包含 `@description` 说明功能
489 +- ✅ 所有参数都有 `@param` 说明
490 +- ✅ 返回值有 `@returns` 说明
491 +- ✅ 复杂逻辑需要详细注释
492 +
493 +**示例**:
494 +```javascript
495 +/**
496 + * 获取文档图标
497 + *
498 + * @description 根据文件类型返回对应的 SVG 图标路径
499 + * @param {string} type - 文件类型(pdf、doc、xls、ppt、txt、img、video、zip、unknown)
500 + * @returns {string} SVG 图标路径
501 + *
502 + * @example
503 + * const icon = getDocumentIcon('pdf')
504 + * // 返回: '/assets/images/icon/doc/pdf.svg'
505 + */
506 +export function getDocumentIcon(type) {
507 + const iconMap = {
508 + pdf: pdfIcon,
509 + doc: docIcon,
510 + // ...
511 + }
512 + return iconMap[type] || unknownIcon
513 +}
514 +```
515 +
516 +### ✅ 2. 命名规范
517 +
518 +**文件命名**:
519 +- 组件: `PascalCase.vue`(多单词)
520 +- 页面: `index.vue` + `index.config.js`
521 +- Composables: `useXxx.js`
522 +- 工具函数: `xxxXxx.js`(camelCase)
523 +
524 +**变量命名**:
525 +- 常量: `UPPER_SNAKE_CASE`
526 +- 响应式变量: `camelCase`
527 +- 组件引用: `PascalCase`
528 +
529 +**示例**:
530 +```javascript
531 +// ✅ GOOD
532 +const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 // 常量
533 +const userList = ref([]) // 响应式变量
534 +import UserCard from '@/components/UserCard.vue' // 组件
535 +
536 +// ❌ BAD
537 +const max_upload_size = 10 * 1024 * 1024
538 +const UserList = ref([])
539 +import userCard from '@/components/UserCard.vue'
540 +```
541 +
542 +### ✅ 3. 错误处理
543 +
544 +**原则**:
545 +- 所有 `async` 函数必须有 `try-catch`
546 +- 用户友好的错误提示
547 +- 日志记录便于调试
548 +
549 +**示例**:
550 +```javascript
551 +/**
552 + * 获取产品列表
553 + *
554 + * @description 从 API 获取产品列表数据
555 + * @param {Object} params - 查询参数
556 + * @returns {Promise<Object>} 产品列表数据
557 + */
558 +export async function fetchProductList(params) {
559 + try {
560 + const { data } = await getProductListAPI(params)
561 +
562 + // 检查 API 响应码
563 + if (data.code !== 1) {
564 + Taro.showToast({
565 + title: data.msg || '获取失败',
566 + icon: 'none'
567 + })
568 + return null
569 + }
570 +
571 + return data.data
572 + } catch (err) {
573 + console.error('获取产品列表失败:', err)
574 + Taro.showToast({
575 + title: '网络异常,请重试',
576 + icon: 'none'
577 + })
578 + return null
579 + }
580 +}
581 +```
582 +
583 +---
584 +
585 +## 架构设计
586 +
587 +### ✅ 1. 统一的列表点击处理
588 +
589 +**问题**: 不同列表页面的点击逻辑分散,难以维护
590 +
591 +**解决方案**: 创建 `useListItemClick` Composable
592 +
593 +```javascript
594 +// src/composables/useListItemClick.js
595 +import { ListType } from '@/constants/list'
596 +
597 +/**
598 + * 列表项点击处理 Composable
599 + *
600 + * @description 根据列表类型智能分发点击行为
601 + * @param {ListType} type - 列表类型
602 + * @param {Object} options - 配置选项
603 + * @param {Function} options.beforeClick - 点击前钩子
604 + * @param {Function} options.afterClick - 点击后钩子
605 + * @returns {Function} 点击处理函数
606 + */
607 +export function useListItemClick(type, options = {}) {
608 + const { beforeClick, afterClick } = options
609 +
610 + const handleClick = async (item) => {
611 + // 执行点击前钩子
612 + if (beforeClick) {
613 + const shouldContinue = await beforeClick(item)
614 + if (!shouldContinue) return
615 + }
616 +
617 + // 根据类型分发行为
618 + switch (type) {
619 + case ListType.FILE:
620 + await handleFileClick(item)
621 + break
622 + case ListType.PRODUCT:
623 + await handleProductClick(item)
624 + break
625 + case ListType.SEARCH:
626 + await handleSearchClick(item)
627 + break
628 + default:
629 + console.warn('未知的列表类型:', type)
630 + }
631 +
632 + // 执行点击后钩子
633 + if (afterClick) {
634 + afterClick(item)
635 + }
636 + }
637 +
638 + return { handleClick }
639 +}
640 +```
641 +
642 +**使用示例**:
643 +```vue
644 +<script setup>
645 +import { useListItemClick } from '@/composables/useListItemClick'
646 +import { ListType } from '@/constants/list'
647 +
648 +const { handleClick } = useListItemClick(ListType.PRODUCT, {
649 + beforeClick: (item) => {
650 + console.log('点击前:', item)
651 + return true
652 + },
653 + afterClick: (item) => {
654 + console.log('点击后:', item)
655 + }
656 +})
657 +</script>
658 +
659 +<template>
660 + <view v-for="item in products" :key="item.id" @tap="handleClick(item)">
661 + {{ item.name }}
662 + </view>
663 +</template>
664 +```
665 +
666 +### ✅ 2. 统一的文件操作
667 +
668 +**问题**: 文件下载、预览、打开等逻辑在多个页面重复
669 +
670 +**解决方案**: 创建 `useFileOperation` Composable
671 +
672 +```javascript
673 +// src/composables/useFileOperation.js
674 +/**
675 + * 文件操作 Composable
676 + *
677 + * @description 封装文件下载、打开、预览等核心逻辑
678 + * @returns {Object} 文件操作方法
679 + */
680 +export function useFileOperation() {
681 + const hasShownOfficeTip = ref(false)
682 +
683 + /**
684 + * 查看文件
685 + *
686 + * @param {Object} file - 文件对象
687 + * @param {string} file.url - 文件 URL
688 + * @param {string} file.name - 文件名
689 + */
690 + const viewFile = async (file) => {
691 + const ext = getFileExtension(file.name)
692 +
693 + // Office 文档提示
694 + if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
695 + if (!hasShownOfficeTip.value) {
696 + Taro.showModal({
697 + title: '提示',
698 + content: 'Office 文档建议使用电脑端查看',
699 + confirmText: '继续',
700 + cancelText: '取消'
701 + }).then((res) => {
702 + if (res.confirm) {
703 + hasShownOfficeTip.value = true
704 + openFile(file)
705 + }
706 + })
707 + return
708 + }
709 + }
710 +
711 + await openFile(file)
712 + }
713 +
714 + /**
715 + * 下载文件
716 + *
717 + * @param {Object} file - 文件对象
718 + */
719 + const downloadFile = async (file) => {
720 + Taro.showLoading({ title: '下载中...' })
721 + try {
722 + const { tempFilePath } = await Taro.downloadFile({
723 + url: file.url
724 + })
725 + Taro.openDocument({
726 + filePath: tempFilePath
727 + })
728 + } catch (err) {
729 + console.error('下载失败:', err)
730 + Taro.showToast({
731 + title: '下载失败',
732 + icon: 'none'
733 + })
734 + } finally {
735 + Taro.hideLoading()
736 + }
737 + }
738 +
739 + return {
740 + viewFile,
741 + downloadFile
742 + }
743 +}
744 +```
745 +
746 +### ✅ 3. 分层架构
747 +
748 +**推荐架构**:
749 +```
750 +src/
751 +├── api/ # API 层 - 接口定义
752 +├── composables/ # 逻辑层 - 可复用逻辑
753 +├── components/ # 组件层 - UI 组件
754 +├── pages/ # 页面层 - 页面组件
755 +├── stores/ # 状态层 - 全局状态
756 +└── utils/ # 工具层 - 工具函数
757 +```
758 +
759 +**原则**:
760 +- API 层只负责接口调用
761 +- Composables 负责业务逻辑复用
762 +- Components 负责纯 UI 展示
763 +- Pages 组装 Components 和 Composables
764 +
765 +---
766 +
767 +## 总结
768 +
769 +### 🎯 核心经验
770 +
771 +1. **"第 3 次出现原则"**: 代码重复 3 次时必须抽取
772 +2. **NutUI 陷阱**: textarea、IconFont 等组件有坑,优先使用原生组件
773 +3. **静态资源**: SVG 图标必须使用 `import` 导入
774 +4. **样式策略**: TailwindCSS(80%) + Less(20%) 混合使用
775 +5. **性能优化**: `shallowRef` + `markRaw` 处理组件对象响应式
776 +6. **代码质量**: 强制 JSDoc 注释,统一命名规范
777 +7. **架构设计**: 分层清晰,职责单一
778 +
779 +### 📚 推荐阅读
780 +
781 +- [Taro 官方文档](https://docs.taro.zone/)
782 +- [NutUI 文档](https://nutui.jd.com/taro/)
783 +- [Vue 3 文档](https://cn.vuejs.org/)
784 +- [项目 CLAUDE.md](../CLAUDE.md)
785 +- [代码注释规范](~/.claude/rules/code-commenting.md)
786 +
787 +### 🔄 持续更新
788 +
789 +本文档会随着项目开发持续更新,记录新的经验教训。
790 +
791 +---
792 +
793 +**最后更新**: 2026-01-31
794 +**维护者**: Claude Code
795 +**项目**: Manulife WeApp