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>
Showing
4 changed files
with
1216 additions
and
201 deletions
| ... | @@ -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 | ... | ... |
docs/changes-summary.md
0 → 100644
| 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 |
docs/lessons-learned.md
0 → 100644
| 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 |
-
Please register or login to post a comment