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 @@
本文件为 Claude Code (claude.ai/code) 在处理此仓库代码时提供指导。
## 开发命令
## 📚 项目文档索引
- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
- **[项目 README](README.md)** - 项目概述和快速开始指南
## 🚀 开发命令
### 核心命令
- `pnpm dev:weapp` - 启动微信小程序开发服务器
- `pnpm dev:h5` - 启动 H5 开发服务器
- `pnpm build:weapp` - 构建生产版本(微信小程序)
- `pnpm lint` - 运行 ESLint
```bash
pnpm dev:weapp # 启动微信小程序开发服务器
pnpm dev:h5 # 启动 H5 开发服务器
pnpm build:weapp # 构建生产版本(微信小程序)
pnpm lint # 运行 ESLint
```
### 其他平台构建
- `pnpm dev:alipay` - 支付宝小程序开发
- `pnpm dev:swan` - 百度小程序开发
- `pnpm dev:tt` - 字节跳动小程序开发
```bash
pnpm dev:alipay # 支付宝小程序开发
pnpm dev:swan # 百度小程序开发
pnpm dev:tt # 字节跳动小程序开发
```
## 📋 快速参考
### ⚡ 常见问题快速解决
| 问题 | 解决方案 | 参考文档 |
|------|---------|---------|
| NutUI textarea 样式无法覆盖 | 使用原生 `<textarea>` | [经验教训](docs/lessons-learned.md#nutui-组件使用陷阱) |
| IconFont 动态切换不响应 | 添加 `:key="name"` | [经验教训](docs/lessons-learned.md#nutui-组件使用陷阱) |
| SVG 图标加载失败(500 错误) | 使用 `import` 导入 | [经验教训](docs/lessons-learned.md#静态资源加载问题) |
| 代码重复 3 次 | 抽取为 Composable | [经验教训](docs/lessons-learned.md#组件抽取与复用) |
| 组件对象响应式警告 | 使用 `shallowRef` + `markRaw` | [经验教训](docs/lessons-learned.md#性能优化) |
### 🎯 核心架构模式
1. **认证流程** - 静默认证 + 401 自动刷新
2. **双设计宽度** - NutUI: 375px, 其他: 750px
3. **样式策略** - TailwindCSS(80%) + Less(20%)
4. **组件抽取** - "第 3 次出现原则"
### 📦 技术栈
- **框架**: Taro 4.1.9 + Vue 3.3.0 + Composition API
- **UI 库**: NutUI 4.3.13(京东推出的 Taro UI 库)
- **状态管理**: Pinia 3.0.3 + taro-plugin-pinia
- **HTTP 客户端**: axios-miniprogram
- **样式**: Less + TailwindCSS 3.x(双设计宽度系统)
- **构建工具**: Webpack 5
## 项目概述
---
## 📖 项目概述
**Manulife WeApp**(臻奇智荟圈)是一个基于 Taro 4 + Vue 3 + NutUI 构建的财富管理微信小程序。
### 业务模块
应用目前包含以下主要功能:
- **产品展示** - 热门产品展示及详情页
- 首页显示热门产品,带"产品资料"按钮
- 产品详情页展示完整产品信息
- **资料库** - 培训材料和文档管理
- 培训材料和案例的知识库
- 资料列表页支持文档预览
- **家办** - 家族办公室服务
- **签单** - 签约流程
- **用户中心** - 个人资料、收藏、反馈、帮助中心
## 项目架构
这是一个基于 **Taro 4 + Vue 3 + NutUI** 的微信小程序,内置身份认证和可复用的导航组件。
### 技术栈
- **框架**:Taro 4.1.9 + Vue 3.3.0 + Composition API
- **UI 库**:NutUI 4.3.13(京东推出的 Taro UI 库)
- **状态管理**:Pinia 3.0.3 + taro-plugin-pinia
- **HTTP 客户端**:axios-miniprogram
- **样式**:Less + TailwindCSS 3.x(双设计宽度系统)
- **构建工具**:Webpack 5
- **导航**:自定义 TabBar + 增强导航 hooks
### 双设计宽度系统
项目在 `config/index.js:16-23` 中配置了**两种不同的设计宽度**
- **NutUI 组件**:375px 基准宽度
- **所有其他页面**:750px 基准宽度
处理样式时:
- 使用 NutUI 组件 → 参考 375px 设计稿
- 自定义页面布局 → 参考 750px 设计稿
## 🏗️ 核心架构
### 核心架构模式
### 1. 可复用的导航组件
#### 1. 可复用的导航组件
**TabBar 组件**`src/components/TabBar.vue`):
- 固定底部导航栏
- 自动适配安全区域(刘海屏/底部指示器)
- 支持图标 + 文字布局
- 激活状态高亮
**TabBar 组件**`src/components/TabBar.vue`
- 固定底部导航栏,自动适配安全区域
- 支持图标 + 文字布局,激活状态高亮
- 使用于:首页、我的、家办、知识库、签单页面
**NavHeader 组件**`src/components/NavHeader.vue`
**NavHeader 组件**`src/components/NavHeader.vue`
- 带返回按钮的自定义导航头
- 透明/背景变体
- 刘海屏设备的安全区域内边距
- 替代默认的 Taro 导航栏
- 透明/背景变体,刘海屏设备的安全区域内边距
**IconFont 组件**`src/components/IconFont.vue`
**IconFont 组件**`src/components/IconFont.vue`
- 自定义图标的图标字体包装器
- 支持大小和颜色自定义
#### 2. 身份认证流程(必需)
- ⚠️ **动态切换时需添加 `:key="name"`** [详见经验教训](docs/lessons-learned.md#坑-2-iconfont-动态切换不响应)
项目具有完善的身份认证系统,支持自动会话管理:
### 2. 身份认证流程(必需)
**启动流程**`src/app.js:26-214`):
1. 应用保存启动路径用于认证回调
2. 检查网络状态并处理弱网络场景
3. 如果未认证,尝试静默认证
4. 认证成功后,启用离线功能
项目具有完善的身份认证系统,支持自动会话管理。
**核心文件**
- `src/utils/authRedirect.js` - 所有认证逻辑(静默刷新、导航、状态)
......@@ -90,7 +92,7 @@
- `src/pages/auth/index.vue` - 认证页(必须保留)
- `src/pages/login/index.vue` - 登录页
**401 自动刷新工作原理**`src/utils/request.js:241-276`
**401 自动刷新流程**
1. API 返回 401
2. 拦截器保存当前页面路径
3. 调用 `refreshSession()` 通过微信登录获取新会话
......@@ -99,7 +101,7 @@
**重要**:后端必须提供 `/srv/?a=openid_wxapp` 端点用于微信登录。
#### 3. API 层架构
### 3. API 层架构
**API 定义模式**`src/api/index.js`):
```javascript
......@@ -111,27 +113,73 @@ export const yourAPI = (params) => {
**请求包装器**`src/api/fn.js`):
- 所有 API 调用都应通过此包装器
- 处理常见错误场景
- 提供一致的接口
**URL 构建**`src/utils/tools.js`):
- `buildApiUrl(action, params)` - 构建完整的 API URL
- 自动合并来自 `src/utils/config.js` 的默认参数
- **始终检查 `res.code === 1` 判断成功**
#### 4. 增强导航系统
### 4. 增强导航系统
**useGo Hook**`src/hooks/useGo.js`):
```javascript
import { useGo } from '@/hooks/useGo'
const go = useGo()
go('/page-name') // 自动补全路径
go('/page', { id: 123 }) // 带查询参数
go('/pages/detail/index') // 自动补全路径
go('/pages/product-detail/index', { id: 123 }) // 带查询参数
go.back() // 返回上一页
```
**路由存储**`src/stores/router.js`):
- 维护已访问路由的栈
- 用于认证回调导航
- 由认证流程自动管理
### 5. 可复用 Composables
**项目中的 Composables**
| Composable | 用途 | 文档 |
|-----------|------|------|
| `useSectionList` | 分组列表管理 | [经验教训](docs/lessons-learned.md#案例-1-usesectionlist-composable) |
| `useFileOperation` | 文件下载、预览、打开 | [经验教训](docs/lessons-learned.md#案例-2-usefileoperation-composable) |
| `useListItemClick` | 统一的列表点击处理 | [经验教训](docs/lessons-learned.md#案例-3-uselistitemclick-composable) |
**抽取原则**:"第 3 次出现原则" - 当相同代码模式出现 3 次时,**必须**抽取为 Composable。
### 6. 样式处理策略
**TailwindCSS vs Less 使用指南**
| 场景 | 使用 | 比例 |
|------|------|------|
| 布局(flex、grid、absolute) | TailwindCSS | 80% |
| 间距(padding、margin、gap) | TailwindCSS | |
| 排版(font-size、text-align) | TailwindCSS | |
| 颜色(bg-*、text-*、border-*) | TailwindCSS | |
| 响应式设计(sm:、md:、lg:) | TailwindCSS | |
| 组件特定样式(需要 scoped) | Less | 20% |
| 深度选择器(`:deep()`) | Less | |
| 动画和过渡 | Less | |
| 伪元素(`::before``::after`) | Less | |
[详见样式处理策略](docs/lessons-learned.md#✅-tailwindcss-vs-less-使用指南)
### 7. 响应式优化
**处理组件对象响应式**
```javascript
// ❌ BAD - 深度响应式导致性能问题
const menuItems = ref([
{ icon: IconFont, name: 'heart' } // Vue 会深度代理组件对象
])
// ✅ GOOD - 使用 shallowRef + markRaw
import { shallowRef, markRaw } from 'vue'
const menuItems = shallowRef([
{ icon: markRaw(IconFont), name: 'heart' } // 避免深度代理
])
```
[详见性能优化](docs/lessons-learned.md#✅-1-响应式数据优化)
### 页面结构
......@@ -224,7 +272,7 @@ src/pages/your-page/
- 标签栏配置(可选)
- 分包(如需要)
## 重要实现细节
## 🔧 重要实现细节
### 会话管理
- 会话 ID 存储在 `localStorage` 中,键名为 `sessionid`
......@@ -252,44 +300,15 @@ NutUI 组件通过 unplugin-vue-components 自动导入(`config/index.js:91-93
- 为小程序兼容性禁用了 Preflight
- 启用了 `rem2rpx` 转换
- 内容路径配置在 `tailwind.config.js`
- 使用 Tailwind 处理布局、间距、颜色
- 使用 Less 处理组件特定样式、动画、伪元素
### 样式指南
**何时使用 TailwindCSS**(80% 的情况):
```vue
<div class="flex items-center justify-between p-4 bg-white">
<h1 class="text-xl font-bold text-gray-900">标题</h1>
</div>
```
**何时使用 Less**(20% 的情况):
- 组件特定样式
- 深度选择器(`:deep()`
- 动画和过渡
- 伪元素(`::before``::after`
```less
<style lang="less" scoped>
.custom-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
:deep(.nut-button) {
background-color: rgba(255, 255, 255, 0.2);
}
}
</style>
```
- [详见样式处理策略](docs/lessons-learned.md#✅-tailwindcss-vs-less-使用指南)
## 可选功能
## 📦 可选功能
如果不需要,可以移除以下功能:
- **微信支付**`src/utils/wechatPay.js``src/api/wx/pay.js`
- **二维码**`src/components/qrCode.vue``src/components/qrCodeSearch.vue`
- **海报生成器**`src/components/PosterBuilder/`
- **时间选择器**`src/components/time-picker-data/`
- **离线缓存**:整个离线预约缓存系统(如果不使用)
## 开发工作流
......@@ -509,30 +528,38 @@ store.setState('新值')
- 验证页面目录结构与路由匹配
- 使用 `useGo` hook 进行一致的导航
## 最佳实践
## 最佳实践
### 组件开发
- 使用 `<script setup>` 语法
- 使用 Composables 处理可复用逻辑
- Props 应该有类型定义
- 使用 `emit` 进行子到父通信
- 优先使用 TailwindCSS 进行样式设计
- 使用 `<script setup>` 语法
- 使用 Composables 处理可复用逻辑
- Props 应该有类型定义
- 使用 `emit` 进行子到父通信
- 优先使用 TailwindCSS 进行样式设计
### API 集成
- 始终检查 `res.code === 1` 判断成功
- 使用 `try/catch` 进行错误处理
- 请求期间显示加载状态
- 优雅地处理网络错误
- 始终检查 `res.code === 1` 判断成功
- 使用 `try/catch` 进行错误处理
- 请求期间显示加载状态
- 优雅地处理网络错误
### 性能
- 使用页面懒加载(分包)
- 使用 CDN 参数优化图片
- 避免无分页的大数据集
-`onUnmounted` 中清理定时器和监听器
### 代码风格
- 遵循 Vue 3 Composition API 模式
- 使用描述性变量名
- 保持函数聚焦且简短(< 50 行)
- 为复杂函数添加 JSDoc 注释
- 提交前运行 `pnpm lint`
- ✅ 使用页面懒加载(分包)
- ✅ 使用 CDN 参数优化图片
- ✅ 避免无分页的大数据集
- ✅ 在 `onUnmounted` 中清理定时器和监听器
- ✅ 使用 `shallowRef` + `markRaw` 处理组件对象
### 代码质量
- ✅ 遵循 Vue 3 Composition API 模式
- ✅ 使用描述性变量名
- ✅ 保持函数聚焦且简短(< 50 行)
-**所有函数必须有 JSDoc 注释** [详见代码注释规范](~/.claude/rules/code-commenting.md)
- ✅ 提交前运行 `pnpm lint`
### 代码复用
- ✅ 遵循"第 3 次出现原则" - 代码重复 3 次时必须抽取
- ✅ 优先使用 Composables 而非 Mixins
- ✅ 组件职责单一,避免过度复杂
[更多最佳实践详见经验教训总结](docs/lessons-learned.md)
......
# manulife-weapp
# Manulife WeApp(臻奇智荟圈)
基于 Taro 4 + Vue 3 + NutUI 的微信小程序模板项目
> 基于 Taro 4 + Vue 3 + NutUI 构建的财富管理微信小程序
## 📚 项目文档
- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
- **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
## 🚀 快速开始
......@@ -26,49 +31,88 @@ pnpm dev:alipay
pnpm build:weapp
```
### 代码检查
```bash
pnpm lint
```
## 🌟 核心特性
-**完整的认证系统** - 静默认证 + 401 自动刷新
-**双设计宽度系统** - NutUI: 375px, 其他: 750px
-**响应式优化** - shallowRef + markRaw 处理组件对象
-**样式方案** - TailwindCSS(80%) + Less(20%) 混合使用
-**组件复用** - "第 3 次出现原则"抽取 Composables
-**可复用组件** - TabBar、NavHeader、IconFont
## ⚡ 常见问题
### NutUI 组件使用陷阱
| 问题 | 解决方案 | 详细文档 |
|------|---------|---------|
| textarea 样式无法覆盖 | 使用原生 `<textarea>` | [经验教训](docs/lessons-learned.md#❌-坑-1-nutui-textarea-样式无法覆盖) |
| IconFont 动态切换不响应 | 添加 `:key="name"` | [经验教训](docs/lessons-learned.md#❌-坑-2-iconfont-动态切换不响应) |
### 静态资源加载问题
| 问题 | 解决方案 | 详细文档 |
|------|---------|---------|
| SVG 图标加载失败(500 错误) | 使用 `import` 导入 | [经验教训](docs/lessons-learned.md#❌-坑-svg-图标加载失败500-错误) |
### 代码质量
- ✅ 所有函数必须有 JSDoc 注释
- ✅ 遵循"第 3 次出现原则"抽取代码
- ✅ 使用 `shallowRef` + `markRaw` 优化性能
[详见经验教训总结](docs/lessons-learned.md)
## 📁 项目结构
```
src/
├── api/ # API 接口层
│ ├── index.js # 业务接口定义(需根据业务修改)
│ ├── fn.js # HTTP 请求封装
│ └── wx/ # 微信相关接口(可选)
│ ├── index.js # 业务接口定义
│ └── fn.js # HTTP 请求封装
├── assets/ # 静态资源
│ ├── images/ # 图片资源
│ ├── styles/ # 全局样式
│ └── css/ # CSS 文件
│ └── styles/ # 全局样式
├── components/ # 通用组件
│ ├── IconFont.vue # 图标字体组件
│ ├── NavHeader.vue # 自定义导航头
│ ├── TabBar.vue # 底部导航栏
│ ├── SectionCard.vue # 分组卡片组件
│ ├── FilterTabs.vue # 筛选标签组件
│ ├── PosterBuilder/ # 海报生成器(可选)
│ ├── time-picker-data/ # 时间选择器
│ ├── PlanPopup/ # 录入计划书弹窗容器
│ ├── PlanSchemes/ # 录入计划书方案组件
│ ├── indexNav.vue # 底部导航
│ └── qrCode.vue # 二维码组件(可选)
│ └── PlanSchemes/ # 计划书方案组件
├── composables/ # Composition API hooks
│ ├── useOfflineBookingCache.js # 离线缓存 hook
│ └── useOfflineBookingCachePolling.js # 轮询刷新 hook
│ ├── useSectionList.js # 分组列表管理
│ ├── useFileOperation.js # 文件操作(下载、预览)
│ └── useListItemClick.js # 列表点击处理
├── hooks/ # 自定义 hooks
│ └── useGo.js # 导航辅助 hook
│ └── useGo.js # 增强导航 hook
├── pages/ # 页面组件
│ ├── index/ # 首页(示例页面)
│ └── auth/ # 认证页(必须保留)
│ ├── index/ # 首页
│ ├── auth/ # 认证页(必须保留)
│ ├── login/ # 登录页
│ ├── product-detail/ # 产品详情
│ ├── material-list/ # 资料列表
│ ├── knowledge-base/ # 知识库
│ ├── plan/ # 计划书
│ ├── favorites/ # 我的收藏
│ ├── mine/ # 我的
│ └── ...
├── stores/ # Pinia 状态管理
│ ├── router.js # 路由状态(用于认证回跳)
│ ├── router.js # 路由状态(认证回跳)
│ ├── main.js # 主 store
│ ├── host.js # 配置 store
│ └── counter.js # 示例 store
│ └── host.js # 配置 store
├── utils/ # 工具函数
│ ├── authRedirect.js # 认证流程核心(必须)
│ ├── request.js # HTTP 客户端核心(必须)
│ ├── network.js # 网络状态监测
│ ├── config.js # 环境配置(⚠️ 需修改)
│ ├── tools.js # 通用工具
│ ├── uiText.js # 文案管理
│ ├── wechatPay.js # 微信支付(可选)
│ ├── mixin.js # Vue mixin
│ ├── polyfill.js # 浏览器兼容
│ └── weapp.js # 小程序工具
│ └── documentIcons.js # 文档图标工具
├── app.js # 应用入口
├── app.config.js # 页面路由配置
└── app.less # 全局样式
......@@ -76,15 +120,20 @@ src/
## 📄 页面说明
- 计划书页面支持顶部搜索与标签固定,列表区域独立滚动,便于快速筛选和浏览
- 资料列表页面支持顶部搜索与分类固定,列表区域独立滚动,便于批量浏览与筛选
- 收藏页面支持顶部筛选固定,列表区域独立滚动
- 知识库页面支持顶部筛选固定,列表区域独立滚动
- 资料列表、知识库、收藏、计划书页面统一使用 FilterTabs 组件进行横向筛选
## ✅ 优化建议
- 新增筛选类页面优先复用 FilterTabs,避免重复样式与交互逻辑
### 主要页面
- **首页** - 产品展示、热门资料、导航入口
- **产品详情** - 完整产品信息展示
- **资料列表** - 培训材料和文档管理
- **知识库** - 培训材料和案例知识库
- **计划书** - 计划书列表和管理
- **我的收藏** - 收藏内容管理
- **我的** - 个人资料、功能菜单
### 页面特性
- ✅ 顶部筛选固定,列表独立滚动
- ✅ 统一的导航头(NavHeader)和底部导航(TabBar)
- ✅ 统一的文件操作逻辑(下载、预览)
- ✅ 统一的列表点击处理
## ⚙️ 配置说明
......@@ -105,11 +154,9 @@ export const REQUEST_DEFAULT_PARAMS = {
### 2. 定义 API 接口
编辑 `src/api/index.js`,添加您的业务接口
编辑 `src/api/index.js`
```javascript
import { buildApiUrl } from '@/utils/tools'
export const yourAPI = (params) => {
return buildApiUrl('your_action', params)
}
......@@ -117,7 +164,7 @@ export const yourAPI = (params) => {
### 3. 配置页面路由
编辑 `src/app.config.js`,添加您的页面
编辑 `src/app.config.js`
```javascript
export default {
......@@ -126,24 +173,22 @@ export default {
'pages/auth/index',
'pages/your-page/index', // 添加您的页面
],
// ...
}
```
### 4. 双设计宽度体系
项目采用双设计宽度体系:
- **NutUI 组件**:基准宽度 375px
- **其他所有页面**:基准宽度 750px
此配置已在 `config/index.js` 中设置,请确保遵循此规范。
[详见双设计宽度系统使用指南](docs/lessons-learned.md#⚠️-双设计宽度系统)
## 🔐 认证流程
项目内置完整的微信登录认证系统:
1. **静默认证**:应用启动时自动执行静默认证
2. **401 自动刷新**当接口返回 401 时,自动刷新会话并重试请求
1. **静默认证**:应用启动时自动执行
2. **401 自动刷新**接口返回 401 时自动刷新会话
3. **授权页回跳**:认证完成后自动跳转回原页面
**核心文件**
......@@ -152,19 +197,6 @@ export default {
**重要**:后端需提供 `/srv/?a=openid_wxapp` 接口用于微信登录。
## 🌐 弱网/离线支持
项目内置弱网和离线支持:
1. **请求超时处理**:自动检测网络超时并降级处理
2. **离线缓存**:支持离线数据缓存和读取
3. **弱网提示**:统一的弱网提示文案
**相关文件**
- `src/composables/useOfflineBookingCache.js`
- `src/composables/useOfflineBookingCachePolling.js`
- `src/utils/uiText.js`
## 📦 技术栈
- **框架**:Taro 4.x
......@@ -176,8 +208,6 @@ export default {
## 🎯 路径别名
已配置的路径别名:
```javascript
@/utils -> src/utils
@/components -> src/components
......@@ -192,28 +222,22 @@ export default {
## 📝 开发规范
### 组件编写
- 使用 Vue 3 Composition API
- 组件统一放在 `src/components/` 目录
- Props 定义清晰,注释详细
- 图标组件直接使用 `@nutui/icons-vue-taro``IconFont`
- **代码注释**:所有函数和方法必须使用 JSDoc 注释,详细说明功能、参数、返回值
- ✅ 使用 Vue 3 Composition API
- ✅ Props 定义清晰,有类型和默认值
-**所有函数必须有 JSDoc 注释**
### API 调用
- 接口统一在 `src/api/index.js` 定义
- 使用 `xxxAPI(params)` 命名格式
- 请求方法统一使用 `src/api/fn.js` 中的封装
- **代码注释**:每个 API 接口都需要 JSDoc 注释说明用途和参数
- ✅ 接口统一在 `src/api/index.js` 定义
- ✅ 使用 `xxxAPI(params)` 命名格式
-**始终检查 `res.code === 1` 判断成功**
### 状态管理
- 使用 Pinia 进行状态管理
- Store 文件统一放在 `src/stores/` 目录
- 复杂逻辑使用 composables 封装
- **代码注释**:Store 的 state、actions 都需要详细注释
- ✅ 使用 Pinia 进行状态管理
- ✅ 复杂逻辑使用 composables 封装
### 样式编写
- 通用样式使用 TailwindCSS 工具类
- 组件样式使用 Less
- NutUI 组件使用 375px 设计稿,其他使用 750px
- ✅ TailwindCSS: 布局、间距、颜色(80%)
- ✅ Less: 组件样式、动画、伪元素(20%)
### 代码注释要求
遵循全局代码注释规范(`~/.claude/rules/code-commenting.md`):
......@@ -221,8 +245,6 @@ export default {
- ✅ 包含 `@description` 说明功能
- ✅ 所有参数都有 `@param` 说明
- ✅ 返回值有 `@returns` 说明
- ✅ 复杂逻辑需要详细注释
- ✅ 正则表达式需要说明含义
## 🔧 可选功能
......@@ -230,13 +252,14 @@ export default {
1. **二维码组件**`src/components/qrCode.vue`
2. **海报生成器**`src/components/PosterBuilder/`
3. **微信支付**`src/utils/wechatPay.js``src/api/wx/`
3. **微信支付**`src/utils/wechatPay.js`
4. **时间选择器**`src/components/time-picker-data/`
5. **离线缓存**`src/composables/useOfflineBookingCache.js`
## 📚 相关文档
- [Taro 官方文档](https://taro-docs.jd.com/)
- **[经验教训总结](docs/lessons-learned.md)** - Taro 项目开发经验、最佳实践和常见陷阱
- **[CLAUDE.md](CLAUDE.md)** - 项目开发指南(供 Claude Code 使用)
- [Taro 官方文档](https://docs.taro.zone/)
- [NutUI 文档](https://nutui.jd.com/taro/)
- [Vue 3 文档](https://cn.vuejs.org/)
- [Pinia 文档](https://pinia.vuejs.org/zh/)
......@@ -248,6 +271,7 @@ export default {
3. NutUI 组件已配置自动导入,无需手动引入
4. TailwindCSS 已禁用 preflight,避免与小程序样式冲突
5. 认证失败会自动跳转到 `/pages/auth/index`
6. **所有函数必须有 JSDoc 注释** - 详见 `~/.claude/rules/code-commenting.md`
## 📄 License
......
# 文档优化总结
## 📝 变更说明
本次优化整理了项目文档,创建了经验教训总结文档,并优化了 CLAUDE.md 和 README.md。
## ✨ 新增文档
### [docs/lessons-learned.md](docs/lessons-learned.md)
**完整的 Taro + Vue 3 项目开发经验教训总结**,包含:
#### 核心章节
1. **组件抽取与复用**
- "第 3 次出现原则"(Rule of Three)
- 何时抽取组件和 Composables
- 实际案例分析(useSectionList、useFileOperation、useListItemClick)
2. **NutUI 组件使用陷阱**
- textarea 样式无法覆盖 → 使用原生组件
- IconFont 动态切换不响应 → 添加 `:key="name"`
- 最佳实践建议
3. **静态资源加载问题**
- SVG 图标加载失败(500 错误)解决方案
- 使用 `import` 导入静态资源
4. **样式处理策略**
- TailwindCSS vs Less 使用指南(80% vs 20%)
- 双设计宽度系统详解
- 使用场景对比表格
5. **性能优化**
- 响应式数据优化(shallowRef + markRaw)
- 页面滚动优化(固定顶部 + 独立滚动)
- 图片优化策略
6. **代码质量**
- JSDoc 注释规范
- 命名规范
- 错误处理模式
7. **架构设计**
- 统一的列表点击处理
- 统一的文件操作
- 分层架构原则
## 🔄 优化文档
### [CLAUDE.md](CLAUDE.md)
**主要优化**:
- ✅ 添加快速参考表格,常见问题一目了然
- ✅ 核心架构模式章节更清晰
- ✅ 添加样式处理策略和响应式优化章节
- ✅ 删除重复内容,精简文档结构
- ✅ 添加对经验教训文档的交叉引用
- ✅ 使用表情符号和表格提升可读性
**关键改进**:
- 新增"快速参考"部分,常见问题快速解决
- 重新组织"核心架构"章节,结构更清晰
- 突出"第 3 次出现原则"等核心经验
- 添加更多代码示例和最佳实践
### [README.md](README.md)
**主要优化**:
- ✅ 添加项目文档索引(经验教训、CLAUDE.md)
- ✅ 新增"核心特性"章节
- ✅ 新增"常见问题"快速参考表格
- ✅ 精简项目结构,移除过时内容
- ✅ 优化开发规范说明
- ✅ 删除离线缓存等不再维护的功能说明
**关键改进**:
- 突出项目的核心特性
- 提供常见问题的快速解决方案
- 更清晰的项目结构说明
- 强调代码注释和开发规范
## 📊 文档对比
### 优化前
- ❌ CLAUDE.md 过于详细,缺乏结构化
- ❌ README.md 缺少项目亮点和常见问题
- ❌ 经验教训散落在 CHANGELOG 中,不便于查阅
- ❌ 缺少对 NutUI 陷阱、静态资源等问题的系统性总结
### 优化后
- ✅ 经验教训系统化整理,便于查阅
- ✅ 文档结构清晰,使用表格和表情符号提升可读性
- ✅ 添加交叉引用,相关文档相互链接
- ✅ 突出核心经验:"第 3 次出现原则"、样式策略、性能优化
- ✅ 提供常见问题的快速解决方案
## 🎯 使用指南
### 对于新开发者
1. **首先阅读**: [README.md](README.md)
- 了解项目概况
- 查看快速开始指南
- 阅读常见问题
2. **深入学习**: [docs/lessons-learned.md](docs/lessons-learned.md)
- 学习 Taro 项目开发的最佳实践
- 了解常见陷阱和解决方案
- 掌握组件抽取、样式处理、性能优化等核心技巧
3. **开发参考**: [CLAUDE.md](CLAUDE.md)
- 查看项目架构和核心模式
- 参考 API 层、认证流程、导航系统等实现
- 使用快速参考表格解决常见问题
### 对于 Claude Code
- **主要参考**: [CLAUDE.md](CLAUDE.md)
- **辅助参考**: [docs/lessons-learned.md](docs/lessons-learned.md)
- **遵循规范**: `~/.claude/rules/code-commenting.md`
## 📌 核心经验总结
### 1. "第 3 次出现原则"
当相同代码模式出现 3 次时,**必须**抽取为 Composable 或组件。
**实际收益**:
- 减少 60-290 行重复代码
- 提升可维护性
- 统一修改点
### 2. NutUI 组件陷阱
| 问题 | 解决方案 |
|------|---------|
| textarea 样式无法覆盖 | 使用原生 `<textarea>` |
| IconFont 动态切换不响应 | 添加 `:key="name"` |
### 3. 静态资源加载
- ❌ 错误: 字符串路径 `/assets/images/icon.svg`
- ✅ 正确: `import icon from '@/assets/images/icon.svg'`
### 4. 样式策略
- **TailwindCSS** (80%): 布局、间距、颜色
- **Less** (20%): 组件样式、动画、伪元素
### 5. 性能优化
- 使用 `shallowRef` + `markRaw` 处理组件对象
- 固定顶部 + 列表独立滚动
- CDN 参数优化图片
## 🔄 持续改进
本文档会随着项目开发持续更新,记录新的经验教训。
**下次更新建议**:
- 添加更多实际案例
- 补充性能优化数据
- 添加架构图和流程图
---
**优化日期**: 2026-01-31
**维护者**: Claude Code
**项目**: Manulife WeApp
# Taro + Vue 3 项目开发经验教训总结
> 本文档总结了在开发 Manulife WeApp 项目过程中遇到的问题、解决方案和最佳实践,为后续 Taro 项目提供参考。
## 目录
- [组件抽取与复用](#组件抽取与复用)
- [NutUI 组件使用陷阱](#nutui-组件使用陷阱)
- [静态资源加载问题](#静态资源加载问题)
- [样式处理策略](#样式处理策略)
- [性能优化](#性能优化)
- [代码质量](#代码质量)
- [架构设计](#架构设计)
---
## 组件抽取与复用
### ✅ 最佳实践
#### 1. "第 3 次出现原则"(Rule of Three)
**原则**:当相同代码模式出现 3 次时,**必须**抽取为 Composable 或组件。
**案例 1: useSectionList Composable**
- **问题**: `onboarding`, `family-office`, `signing` 三个页面都使用相同的分组列表模式
- **解决**: 创建 `src/composables/useSectionList.js`
- **收益**: 减少约 60 行重复代码,提升可维护性
```javascript
// src/composables/useSectionList.js
/**
* 分组列表页面 Composable
*
* @description 统一管理分组列表数据和点击事件
* @param {Object} sectionData - 分组数据
* @param {Function} handleClick - 点击回调函数
* @returns {Object} { sections, onItemClick }
*/
export function useSectionList(sectionData, handleClick) {
const sections = ref(sectionData)
const onItemClick = (item) => {
handleClick(item)
}
return { sections, onItemClick }
}
```
**案例 2: useFileOperation Composable**
- **问题**: 收藏页和产品详情页都有约 200 行重复的文件操作代码
- **解决**: 创建 `src/composables/useFileOperation.js`
- **收益**: 减少 ~290 行重复代码,统一修改点
**案例 3: useListItemClick Composable**
- **问题**: 多个列表页面的点击逻辑分散且重复
- **解决**: 创建 `src/composables/useListItemClick.js`
- **收益**: 支持上下文感知的行为路由,扩展性强
#### 2. 组件设计原则
**案例: SectionCard 默认渐变色优化**
- **问题**: 三个页面都重复设置默认渐变色
- **解决**: 在 `SectionCard` 组件内部使用 `computed` 属性内置默认值
- **收益**: 修改默认值只需在一处,简化页面代码
```vue
<!-- src/components/SectionCard.vue -->
<script setup>
const props = defineProps({
bgGradient: {
type: String,
default: ''
}
})
// ✅ GOOD - 内置默认渐变色
const computedBgGradient = computed(() => {
return props.bgGradient || 'linear-gradient(90deg, #EFF6FF 0%, #DBEAFE 100%)'
})
</script>
```
#### 3. 何时抽取组件
**触发条件**:
- ✅ 代码重复 ≥ 2 次 → 警惕
- ✅ 代码重复 ≥ 3 次 → **必须抽取**
- ✅ v-for 模板超过 5 行 → 提取列表项组件
- ✅ 组件模板超过 150 行 → 拆分组件
- ✅ 函数超过 50 行 → 拆分函数
### ❌ 反模式
#### 1. 过早抽取
```javascript
// ❌ BAD - 代码只出现 1 次就抽取
function useSpecificFeature() {
// 只在一个地方使用...
}
// ✅ GOOD - 等待第 3 次出现再抽取
// 第 1、2 次直接实现,第 3 次时抽取
```
#### 2. 过度抽象
```javascript
// ❌ BAD - 过度通用的抽象,难以理解
function useGenericList({ config, handlers, options }) {
// 太多参数,过于复杂
}
// ✅ GOOD - 专注特定场景
function useSectionList(sectionData, handleClick) {
// 简单直接,易于理解
}
```
---
## NutUI 组件使用陷阱
### ❌ 坑 1: NutUI textarea 样式无法覆盖
**问题描述**:
```vue
<!-- ❌ 无法生效 -->
<nut-textarea
v-model="content"
class="custom-textarea"
/>
```
```less
// ❌ 深度选择器无效
.custom-textarea :deep(.nut-textarea__textarea) {
padding: 24rpx;
}
```
**原因**: NutUI textarea 组件使用 Shadow DOM 或内部样式隔离
**解决方案**: 使用原生小程序 `<textarea>` 组件
```vue
<!-- ✅ 完全控制样式 -->
<textarea
v-model="content"
class="custom-textarea"
maxlength="200"
@input="handleInput"
/>
<view class="char-count">{{ content.length }}/200</view>
```
### ❌ 坑 2: IconFont 动态切换不响应
**问题描述**:
```vue
<!-- ❌ 图标不更新 -->
<IconFont :name="iconName" />
```
```javascript
const iconName = ref('heart')
setTimeout(() => {
iconName.value = 'heart-fill' // 视图不更新!
}, 1000)
```
**原因**: NutUI 的 `IconFont` 组件在某些环境下未正确响应 props 变化
**解决方案**: 添加 `:key` 强制重新渲染
```vue
<!-- ✅ 添加 key 属性 -->
<IconFont :name="iconName" :key="iconName" />
```
### ✅ NutUI 最佳实践
1. **优先使用原生组件**: 当 NutUI 组件样式限制时
2. **添加 key 属性**: 动态切换图标时
3. **深度样式覆盖**: 尝试 `:deep()` 选择器
4. **查阅文档**: 确认 NutUI 版本和已知问题
---
## 静态资源加载问题
### ❌ 坑: SVG 图标加载失败(500 错误)
**问题描述**:
```javascript
// ❌ 字符串路径导致加载失败
export const getDocumentIcon = (type) => {
const icons = {
pdf: '/assets/images/icon/doc/doc.svg', // ❌ 500 错误
doc: '/assets/images/icon/doc/doc.svg',
// ...
}
return icons[type]
}
```
**原因**: Taro 构建工具无法正确处理字符串路径的静态资源引用
**解决方案**: 使用 ES6 `import` 导入
```javascript
// ✅ 使用 import 导入
import pdfIcon from '@/assets/images/icon/doc/pdf.svg'
import docIcon from '@/assets/images/icon/doc/doc.svg'
// ...
export const getDocumentIcon = (type) => {
const icons = {
pdf: pdfIcon,
doc: docIcon,
// ...
}
return icons[type]
}
```
### ✅ 静态资源处理规则
| 资源类型 | 引用方式 | 示例 |
|---------|---------|------|
| **SVG 图标** | `import` 导入 | `import icon from '@/assets/icon.svg'` |
| **图片** | `import` 导入或字符串路径 | 两种方式都支持 |
| **字体** | 配置在 `config/index.js` | 参考项目配置 |
| **远程资源** | 字符串 URL | `https://cdn.example.com/image.png` |
---
## 样式处理策略
### ✅ TailwindCSS vs Less 使用指南
#### 使用 TailwindCSS(80% 场景)
**适用场景**:
- 布局(flex、grid、absolute)
- 间距(padding、margin、gap)
- 排版(font-size、font-weight、text-align)
- 颜色(bg-*、text-*、border-*
- 响应式设计(sm:、md:、lg:)
**示例**:
```vue
<div class="
flex items-center justify-between <!-- 布局 -->
p-4 mb-2 <!-- 间距 -->
bg-white rounded-lg shadow-md <!-- 颜色、圆角、阴影 -->
">
<h1 class="text-xl font-bold">标题</h1>
</div>
```
#### 使用 Less(20% 场景)
**适用场景**:
- 组件特定样式(需要 `scoped`
- 深度选择器(`:deep()`
- 动画和过渡
- 伪元素(`::before``::after`
- 复杂的计算表达式
**示例**:
```less
<style lang="less" scoped>
.custom-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
// 深度选择器修改第三方组件
:deep(.nut-button) {
background-color: rgba(255, 255, 255, 0.2);
}
// 伪元素
&::before {
content: '';
position: absolute;
// ...
}
// 动画
@keyframes slide-in {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
}
</style>
```
### ⚠️ 双设计宽度系统
**项目配置**:
```javascript
// config/index.js
const designWidth = {
750: [375, 375], // NutUI 组件: 375px 基准
750: [750, 750] // 其他所有页面: 750px 基准
}
```
**使用规则**:
- **NutUI 组件** → 参考 375px 设计稿
- **自定义页面** → 参考 750px 设计稿
**示例**:
```vue
<!-- NutUI 组件: 使用 375px 设计稿 -->
<nut-button :custom-style="buttonStyle">按钮</nut-button>
<script>
const buttonStyle = {
fontSize: '14px', // 375px 设计稿: 14px
padding: '8px 16px'
}
</script>
<!-- 自定义元素: 使用 750px 设计稿 -->
<view class="custom-box">
内容
</view>
<style lang="less" scoped>
.custom-box {
width: 750px; // 750px 设计稿: 750px
height: 200px;
}
</style>
```
---
## 性能优化
### ✅ 1. 响应式数据优化
**问题**: Vue 3 对包含组件对象的响应式数据进行深度代理,导致性能问题
**解决方案**: 使用 `shallowRef` + `markRaw`
```javascript
import { shallowRef, markRaw } from 'vue'
// ❌ BAD - 深度响应式
const menuItems = ref([
{
icon: markRaw(IconFont), // 组件对象
name: 'heart',
title: '我的收藏'
}
// ...
])
// ✅ GOOD - 浅层响应式
const menuItems = shallowRef([
{
icon: markRaw(IconFont), // 标记为原始对象
name: 'heart',
title: '我的收藏'
}
// ...
])
```
**收益**:
- 消除 "Component that was made a reactive object" 警告
- 避免不必要的深度代理
- 提升页面初始化和渲染性能
### ✅ 2. 页面滚动优化
**问题**: 整页滚动,顶部筛选区域会随列表滚动消失
**解决方案**: 固定顶部 + 列表独立滚动
```vue
<template>
<view class="page-container">
<!-- 固定顶部筛选 -->
<view class="fixed-header">
<search-bar />
<filter-tabs />
</view>
<!-- 独立滚动列表 -->
<scroll-view
scroll-y
class="scrollable-list"
:style="{ height: listHeight }"
>
<view v-for="item in list" :key="item.id">
{{ item.title }}
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const listHeight = ref('calc(100vh - 200rpx)') // 减去固定区域高度
onMounted(() => {
// 动态计算列表高度
const query = Taro.createSelectorQuery()
query.select('.fixed-header').boundingClientRect()
query.exec((res) => {
const headerHeight = res[0].height
listHeight.value = `calc(100vh - ${headerHeight}px)`
})
})
</script>
<style lang="less" scoped>
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.fixed-header {
position: sticky;
top: 0;
z-index: 10;
background-color: #fff;
}
.scrollable-list {
flex: 1;
overflow-y: auto;
}
</style>
```
**收益**:
- 顶部筛选区域始终可见
- 列表滚动更流畅
- 用户体验显著提升
### ✅ 3. 图片优化
**策略**:
1. 使用 CDN 参数优化图片
2. 图片懒加载
3. 响应式图片
```javascript
// src/utils/image.js
/**
* 优化 CDN 图片 URL
*
* @param {string} url - 原始 URL
* @param {Object} options - 优化参数
* @param {number} options.width - 宽度
* @param {number} options.quality - 质量(1-100)
*/
export function optimizeImageUrl(url, options = {}) {
if (!url || !url.includes('cdn.ipadbiz.cn')) {
return url
}
const { width = 750, quality = 70 } = options
const params = new URLSearchParams({
'imageMogr2/thumbnail': `${width}x`,
strip: '',
quality: quality.toString()
})
return `${url}?${params.toString()}`
}
```
---
## 代码质量
### ✅ 1. JSDoc 注释规范
**强制要求**:
- ✅ 所有函数必须有 JSDoc 注释
- ✅ 包含 `@description` 说明功能
- ✅ 所有参数都有 `@param` 说明
- ✅ 返回值有 `@returns` 说明
- ✅ 复杂逻辑需要详细注释
**示例**:
```javascript
/**
* 获取文档图标
*
* @description 根据文件类型返回对应的 SVG 图标路径
* @param {string} type - 文件类型(pdf、doc、xls、ppt、txt、img、video、zip、unknown)
* @returns {string} SVG 图标路径
*
* @example
* const icon = getDocumentIcon('pdf')
* // 返回: '/assets/images/icon/doc/pdf.svg'
*/
export function getDocumentIcon(type) {
const iconMap = {
pdf: pdfIcon,
doc: docIcon,
// ...
}
return iconMap[type] || unknownIcon
}
```
### ✅ 2. 命名规范
**文件命名**:
- 组件: `PascalCase.vue`(多单词)
- 页面: `index.vue` + `index.config.js`
- Composables: `useXxx.js`
- 工具函数: `xxxXxx.js`(camelCase)
**变量命名**:
- 常量: `UPPER_SNAKE_CASE`
- 响应式变量: `camelCase`
- 组件引用: `PascalCase`
**示例**:
```javascript
// ✅ GOOD
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 // 常量
const userList = ref([]) // 响应式变量
import UserCard from '@/components/UserCard.vue' // 组件
// ❌ BAD
const max_upload_size = 10 * 1024 * 1024
const UserList = ref([])
import userCard from '@/components/UserCard.vue'
```
### ✅ 3. 错误处理
**原则**:
- 所有 `async` 函数必须有 `try-catch`
- 用户友好的错误提示
- 日志记录便于调试
**示例**:
```javascript
/**
* 获取产品列表
*
* @description 从 API 获取产品列表数据
* @param {Object} params - 查询参数
* @returns {Promise<Object>} 产品列表数据
*/
export async function fetchProductList(params) {
try {
const { data } = await getProductListAPI(params)
// 检查 API 响应码
if (data.code !== 1) {
Taro.showToast({
title: data.msg || '获取失败',
icon: 'none'
})
return null
}
return data.data
} catch (err) {
console.error('获取产品列表失败:', err)
Taro.showToast({
title: '网络异常,请重试',
icon: 'none'
})
return null
}
}
```
---
## 架构设计
### ✅ 1. 统一的列表点击处理
**问题**: 不同列表页面的点击逻辑分散,难以维护
**解决方案**: 创建 `useListItemClick` Composable
```javascript
// src/composables/useListItemClick.js
import { ListType } from '@/constants/list'
/**
* 列表项点击处理 Composable
*
* @description 根据列表类型智能分发点击行为
* @param {ListType} type - 列表类型
* @param {Object} options - 配置选项
* @param {Function} options.beforeClick - 点击前钩子
* @param {Function} options.afterClick - 点击后钩子
* @returns {Function} 点击处理函数
*/
export function useListItemClick(type, options = {}) {
const { beforeClick, afterClick } = options
const handleClick = async (item) => {
// 执行点击前钩子
if (beforeClick) {
const shouldContinue = await beforeClick(item)
if (!shouldContinue) return
}
// 根据类型分发行为
switch (type) {
case ListType.FILE:
await handleFileClick(item)
break
case ListType.PRODUCT:
await handleProductClick(item)
break
case ListType.SEARCH:
await handleSearchClick(item)
break
default:
console.warn('未知的列表类型:', type)
}
// 执行点击后钩子
if (afterClick) {
afterClick(item)
}
}
return { handleClick }
}
```
**使用示例**:
```vue
<script setup>
import { useListItemClick } from '@/composables/useListItemClick'
import { ListType } from '@/constants/list'
const { handleClick } = useListItemClick(ListType.PRODUCT, {
beforeClick: (item) => {
console.log('点击前:', item)
return true
},
afterClick: (item) => {
console.log('点击后:', item)
}
})
</script>
<template>
<view v-for="item in products" :key="item.id" @tap="handleClick(item)">
{{ item.name }}
</view>
</template>
```
### ✅ 2. 统一的文件操作
**问题**: 文件下载、预览、打开等逻辑在多个页面重复
**解决方案**: 创建 `useFileOperation` Composable
```javascript
// src/composables/useFileOperation.js
/**
* 文件操作 Composable
*
* @description 封装文件下载、打开、预览等核心逻辑
* @returns {Object} 文件操作方法
*/
export function useFileOperation() {
const hasShownOfficeTip = ref(false)
/**
* 查看文件
*
* @param {Object} file - 文件对象
* @param {string} file.url - 文件 URL
* @param {string} file.name - 文件名
*/
const viewFile = async (file) => {
const ext = getFileExtension(file.name)
// Office 文档提示
if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
if (!hasShownOfficeTip.value) {
Taro.showModal({
title: '提示',
content: 'Office 文档建议使用电脑端查看',
confirmText: '继续',
cancelText: '取消'
}).then((res) => {
if (res.confirm) {
hasShownOfficeTip.value = true
openFile(file)
}
})
return
}
}
await openFile(file)
}
/**
* 下载文件
*
* @param {Object} file - 文件对象
*/
const downloadFile = async (file) => {
Taro.showLoading({ title: '下载中...' })
try {
const { tempFilePath } = await Taro.downloadFile({
url: file.url
})
Taro.openDocument({
filePath: tempFilePath
})
} catch (err) {
console.error('下载失败:', err)
Taro.showToast({
title: '下载失败',
icon: 'none'
})
} finally {
Taro.hideLoading()
}
}
return {
viewFile,
downloadFile
}
}
```
### ✅ 3. 分层架构
**推荐架构**:
```
src/
├── api/ # API 层 - 接口定义
├── composables/ # 逻辑层 - 可复用逻辑
├── components/ # 组件层 - UI 组件
├── pages/ # 页面层 - 页面组件
├── stores/ # 状态层 - 全局状态
└── utils/ # 工具层 - 工具函数
```
**原则**:
- API 层只负责接口调用
- Composables 负责业务逻辑复用
- Components 负责纯 UI 展示
- Pages 组装 Components 和 Composables
---
## 总结
### 🎯 核心经验
1. **"第 3 次出现原则"**: 代码重复 3 次时必须抽取
2. **NutUI 陷阱**: textarea、IconFont 等组件有坑,优先使用原生组件
3. **静态资源**: SVG 图标必须使用 `import` 导入
4. **样式策略**: TailwindCSS(80%) + Less(20%) 混合使用
5. **性能优化**: `shallowRef` + `markRaw` 处理组件对象响应式
6. **代码质量**: 强制 JSDoc 注释,统一命名规范
7. **架构设计**: 分层清晰,职责单一
### 📚 推荐阅读
- [Taro 官方文档](https://docs.taro.zone/)
- [NutUI 文档](https://nutui.jd.com/taro/)
- [Vue 3 文档](https://cn.vuejs.org/)
- [项目 CLAUDE.md](../CLAUDE.md)
- [代码注释规范](~/.claude/rules/code-commenting.md)
### 🔄 持续更新
本文档会随着项目开发持续更新,记录新的经验教训。
---
**最后更新**: 2026-01-31
**维护者**: Claude Code
**项目**: Manulife WeApp