fix(plan): 修复 PlanFormContainer prop 验证错误
修复了产品中心页面进入时的 Vue prop 类型验证警告: - 明确指定 product prop 的 type 为 Object - 将 required 设为 false 并允许 null 默认值 - 添加空值检查,防止访问 null.form_sn 报错 - 在 product-center 页面添加 v-if 条件渲染 影响范围: - src/components/PlanFormContainer.vue - src/pages/product-center/index.vue 相关文档: - 更新 API 联调日志(搜索模块完成) - 更新 CLAUDE.md(可复用组件说明) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
4 changed files
with
130 additions
and
15 deletions
| ... | @@ -155,6 +155,11 @@ go.back() // 返回上一页 | ... | @@ -155,6 +155,11 @@ go.back() // 返回上一页 |
| 155 | 155 | ||
| 156 | **抽取原则**:"第 3 次出现原则" - 当相同代码模式出现 3 次时,**必须**抽取为 Composable。 | 156 | **抽取原则**:"第 3 次出现原则" - 当相同代码模式出现 3 次时,**必须**抽取为 Composable。 |
| 157 | 157 | ||
| 158 | +**组件自包含原则**(新增): | ||
| 159 | +- 对于重复的UI结构,抽取为可复用组件(如 MaterialCard、ProductCard) | ||
| 160 | +- 组件应该自包含业务逻辑(查看、收藏等),通过事件与父组件通信 | ||
| 161 | +- 避免在父组件中重复编写相同的逻辑代码 | ||
| 162 | + | ||
| 158 | ### 6. 样式处理策略 | 163 | ### 6. 样式处理策略 |
| 159 | 164 | ||
| 160 | **TailwindCSS vs Less 使用指南**: | 165 | **TailwindCSS vs Less 使用指南**: |
| ... | @@ -212,7 +217,12 @@ src/pages/your-page/ | ... | @@ -212,7 +217,12 @@ src/pages/your-page/ |
| 212 | - 热门产品的"产品资料"按钮跳转到 `product-detail` 页面,带产品 ID | 217 | - 热门产品的"产品资料"按钮跳转到 `product-detail` 页面,带产品 ID |
| 213 | - 热门资料的"查看更多"跳转到 `material-list` 页面 | 218 | - 热门资料的"查看更多"跳转到 `material-list` 页面 |
| 214 | - 网格导航图标跳转到各个业务页面 | 219 | - 网格导航图标跳转到各个业务页面 |
| 220 | + - 使用可复用组件(MaterialCard、ProductCard) | ||
| 215 | 2. `pages/search/index` - 产品和资料搜索页 | 221 | 2. `pages/search/index` - 产品和资料搜索页 |
| 222 | + - 支持实时搜索(输入关键字自动调用 searchAPI) | ||
| 223 | + - 双Tab切换(产品、资料) | ||
| 224 | + - 支持分页加载和触底加载更多 | ||
| 225 | + - 使用可复用组件(MaterialCard、ProductCard) | ||
| 216 | 3. `pages/webview/index` - 外部 URL 的 WebView 包装器 | 226 | 3. `pages/webview/index` - 外部 URL 的 WebView 包装器 |
| 217 | 4. `pages/document-preview/index` - 文档预览页 | 227 | 4. `pages/document-preview/index` - 文档预览页 |
| 218 | 5. `pages/document-demo/index` - 文档预览演示页 | 228 | 5. `pages/document-demo/index` - 文档预览演示页 |
| ... | @@ -258,6 +268,8 @@ src/pages/your-page/ | ... | @@ -258,6 +268,8 @@ src/pages/your-page/ |
| 258 | - `SectionCard.vue` - 分组卡片组件 | 268 | - `SectionCard.vue` - 分组卡片组件 |
| 259 | - `SectionItem.vue` - 分组列表项组件 | 269 | - `SectionItem.vue` - 分组列表项组件 |
| 260 | - `ListItemActions/` - 列表项操作按钮 | 270 | - `ListItemActions/` - 列表项操作按钮 |
| 271 | +- `MaterialCard.vue` - 资料卡片组件(可复用) | ||
| 272 | +- `ProductCard.vue` - 产品卡片组件(可复用) | ||
| 261 | 273 | ||
| 262 | **表单与输入组件**: | 274 | **表单与输入组件**: |
| 263 | - `FilterTabs.vue` - 过滤标签组件 | 275 | - `FilterTabs.vue` - 过滤标签组件 |
| ... | @@ -536,6 +548,14 @@ store.setState('新值') | ... | @@ -536,6 +548,14 @@ store.setState('新值') |
| 536 | 2. **`src/components/NavHeader.vue`** - 自定义导航头 | 548 | 2. **`src/components/NavHeader.vue`** - 自定义导航头 |
| 537 | 3. **`src/components/SectionCard.vue`** - 分组卡片 | 549 | 3. **`src/components/SectionCard.vue`** - 分组卡片 |
| 538 | 4. **`src/components/DocumentPreview/`** - 文档预览 | 550 | 4. **`src/components/DocumentPreview/`** - 文档预览 |
| 551 | +5. **`src/components/MaterialCard.vue`** - 资料卡片(可复用) | ||
| 552 | + - 自包含业务逻辑:查看、收藏 | ||
| 553 | + - 支持动态标签、文件大小格式化、学习人数显示 | ||
| 554 | + - 使用页面:首页、搜索页、周热门资料页 | ||
| 555 | +6. **`src/components/ProductCard.vue`** - 产品卡片(可复用) | ||
| 556 | + - 自定义样式:动态标签、封面图 | ||
| 557 | + - 支持产品详情查看和计划书功能 | ||
| 558 | + - 使用页面:首页、搜索页 | ||
| 539 | 559 | ||
| 540 | ### Composables | 560 | ### Composables |
| 541 | 1. **`src/composables/useSectionList.js`** - 分组列表管理 | 561 | 1. **`src/composables/useSectionList.js`** - 分组列表管理 |
| ... | @@ -609,5 +629,10 @@ store.setState('新值') | ... | @@ -609,5 +629,10 @@ store.setState('新值') |
| 609 | - ✅ 遵循"第 3 次出现原则" - 代码重复 3 次时必须抽取 | 629 | - ✅ 遵循"第 3 次出现原则" - 代码重复 3 次时必须抽取 |
| 610 | - ✅ 优先使用 Composables 而非 Mixins | 630 | - ✅ 优先使用 Composables 而非 Mixins |
| 611 | - ✅ 组件职责单一,避免过度复杂 | 631 | - ✅ 组件职责单一,避免过度复杂 |
| 632 | +- ✅ **组件自包含业务逻辑** - 当UI结构重复出现时,抽取为可复用组件(如 MaterialCard、ProductCard) | ||
| 633 | + - 组件内部处理业务逻辑(查看、收藏等) | ||
| 634 | + - 通过事件与父组件通信 | ||
| 635 | + - 减少父组件的重复代码 | ||
| 636 | + - 示例:MaterialCard 组件在首页、搜索页、周热门资料页复用 | ||
| 612 | 637 | ||
| 613 | [更多最佳实践详见经验教训总结](docs/lessons-learned.md) | 638 | [更多最佳实践详见经验教训总结](docs/lessons-learned.md) | ... | ... |
| ... | @@ -4,16 +4,27 @@ | ... | @@ -4,16 +4,27 @@ |
| 4 | 4 | ||
| 5 | ## 📊 总体进度 | 5 | ## 📊 总体进度 |
| 6 | 6 | ||
| 7 | -- **总接口数**: 26 | 7 | +- **总接口数**: 27 |
| 8 | -- **已完成**: 14 (53.8%) | 8 | +- **已完成**: 15 (55.6%) |
| 9 | -- **联调中**: 1 (3.8%) | 9 | +- **联调中**: 0 (0%) |
| 10 | -- **已废弃**: 3 (11.5%) | 10 | +- **已废弃**: 3 (11.1%) |
| 11 | -- **待联调**: 8 (30.8%) | 11 | +- **待联调**: 9 (33.3%) |
| 12 | - **有阻塞**: 0 | 12 | - **有阻塞**: 0 |
| 13 | 13 | ||
| 14 | --- | 14 | --- |
| 15 | 15 | ||
| 16 | -**📝 最近更新** (2026-02-05): | 16 | +**📝 最近更新** (2026-02-06): |
| 17 | +- ✅ **搜索模块联调完成**:searchAPI 接口前端已完成集成 | ||
| 18 | + - 支持产品和资料的实时搜索 | ||
| 19 | + - 支持分页加载和触底加载更多 | ||
| 20 | + - 使用可复用卡片组件(MaterialCard、ProductCard) | ||
| 21 | +- 🆕 **新增可复用卡片组件**: | ||
| 22 | + - MaterialCard.vue - 资料卡片组件(272行) | ||
| 23 | + - ProductCard.vue - 产品卡片组件(106行) | ||
| 24 | + - 已应用到:搜索页、首页、周热门资料页 | ||
| 25 | +- 🔄 **页面重构**:搜索页、首页、周热门资料页使用新组件重构 | ||
| 26 | + - 减少代码重复:净减少238行 | ||
| 27 | + - 统一UI风格和交互逻辑 | ||
| 17 | - ✅ **收藏模块联调完成**:2个接口(delAPI、listAPI)前端已完成联调 | 28 | - ✅ **收藏模块联调完成**:2个接口(delAPI、listAPI)前端已完成联调 |
| 18 | - 收藏列表API:获取收藏数据,支持分页 | 29 | - 收藏列表API:获取收藏数据,支持分页 |
| 19 | - 取消收藏API:删除单个收藏项 | 30 | - 取消收藏API:删除单个收藏项 |
| ... | @@ -957,6 +968,73 @@ | ... | @@ -957,6 +968,73 @@ |
| 957 | 968 | ||
| 958 | --- | 969 | --- |
| 959 | 970 | ||
| 971 | +### 搜索模块 | ||
| 972 | + | ||
| 973 | +#### 接口 1: 搜索(产品、资料) | ||
| 974 | + | ||
| 975 | +**接口信息** | ||
| 976 | +- **接口名称**: `searchAPI` | ||
| 977 | +- **接口路径**: `/srv/?a=search` | ||
| 978 | +- **请求方法**: GET | ||
| 979 | +- **负责页面**: `src/pages/search/index.vue` | ||
| 980 | +- **负责人**: 后端团队 | ||
| 981 | + | ||
| 982 | +**接口文档更新记录** | ||
| 983 | + | ||
| 984 | +| 日期 | 版本 | 变更内容 | 变更原因 | 文档链接 | | ||
| 985 | +|------|------|---------|---------|---------| | ||
| 986 | +| 2026-02-06 | v1.0 | 初始版本 | 前端集成完成,联调成功 | [查看](docs/api-specs/search/search.md) | | ||
| 987 | + | ||
| 988 | +**页面调试情况** | ||
| 989 | + | ||
| 990 | +| 日期 | 调试页面 | 问题记录 | 解决方案 | 状态 | | ||
| 991 | +|------|---------|---------|---------|------| | ||
| 992 | +| 2026-02-06 | `src/pages/search/index.vue` | 无 | 实时搜索、分页加载均正常 | ✅ 已完成 | | ||
| 993 | + | ||
| 994 | +**接口状态**: ✅ 已完成 | ||
| 995 | + | ||
| 996 | +**备注**: | ||
| 997 | +- **参数**: | ||
| 998 | + - `k`: 搜索关键字(必需) | ||
| 999 | + - `type`: 搜索类型,product=产品,file=资料(必需) | ||
| 1000 | + - `page`: 页码,从 0 开始 | ||
| 1001 | + - `limit`: 每页数量(默认 10) | ||
| 1002 | +- **返回数据结构**: | ||
| 1003 | + ```javascript | ||
| 1004 | + { | ||
| 1005 | + code: 1, | ||
| 1006 | + data: { | ||
| 1007 | + list: [...], // 搜索结果列表 | ||
| 1008 | + total: 100 // 总数 | ||
| 1009 | + } | ||
| 1010 | + } | ||
| 1011 | + ``` | ||
| 1012 | +- **产品字段**: | ||
| 1013 | + - `id` - 产品ID | ||
| 1014 | + - `product_name` - 产品名称 | ||
| 1015 | + - `tags[]` - 产品标签(含 bg_color/text_color) | ||
| 1016 | + - `cover_image` - 封面图 | ||
| 1017 | + - `form_sn` - 计划书模板标识 | ||
| 1018 | +- **资料字段**: | ||
| 1019 | + - `id` - 文件ID | ||
| 1020 | + - `title` - 文件标题 | ||
| 1021 | + - `fileName` - 文件名 | ||
| 1022 | + - `fileSize` - 文件大小 | ||
| 1023 | + - `extension` - 文件扩展名 | ||
| 1024 | + - `downloadUrl` - 下载链接 | ||
| 1025 | + - `learners` - 学习人数 | ||
| 1026 | + - `readPeoplePercent` - 学习人数比例 | ||
| 1027 | + - `collected` - 是否已收藏 | ||
| 1028 | +- **实现位置**: `src/pages/search/index.vue:204-265` (`performSearch` 函数) | ||
| 1029 | +- **功能特性**: | ||
| 1030 | + - 实时搜索:输入关键字后自动调用API | ||
| 1031 | + - 双Tab切换:支持产品和资料切换 | ||
| 1032 | + - 分页加载:触底自动加载更多 | ||
| 1033 | + - 结果统计:显示找到的相关结果数量 | ||
| 1034 | + - 动画效果:列表项逐个淡入动画 | ||
| 1035 | + | ||
| 1036 | +--- | ||
| 1037 | + | ||
| 960 | ### 埋点模块 | 1038 | ### 埋点模块 |
| 961 | 1039 | ||
| 962 | #### 接口 1: 添加埋点 | 1040 | #### 接口 1: 添加埋点 |
| ... | @@ -1125,14 +1203,13 @@ | ... | @@ -1125,14 +1203,13 @@ |
| 1125 | 1203 | ||
| 1126 | --- | 1204 | --- |
| 1127 | 1205 | ||
| 1128 | -**最后更新时间**: 2026-02-04 18:00 | 1206 | +**最后更新时间**: 2026-02-06 17:20 |
| 1129 | -**文档版本**: v2.4 | 1207 | +**文档版本**: v2.5 |
| 1130 | **更新内容**: | 1208 | **更新内容**: |
| 1131 | -- 📝 **文档模块接口字段确认**: | 1209 | +- ✅ **搜索模块联调完成**:新增 searchAPI 接口,前端已完成集成 |
| 1132 | - - weekHotAPI(本周热门资料):字段已确认,更新接口文档 | 1210 | +- 🆕 **新增可复用卡片组件**:MaterialCard、ProductCard |
| 1133 | - - fileListAPI(文档列表):字段已确认,更新接口文档,修正接口路径 | 1211 | +- 🔄 **页面重构**:搜索页、首页、周热门资料页使用新组件重构,减少代码重复 |
| 1134 | -- 更新总体进度:26个接口(12个已完成,11个待联调,3个已废弃) | 1212 | +- 更新总体进度:27个接口(15个已完成,9个待联调,3个已废弃) |
| 1135 | -- 文档模块从"初稿"状态更新为"待联调"状态 | ||
| 1136 | 1213 | ||
| 1137 | **历史版本**: | 1214 | **历史版本**: |
| 1138 | - v2.1 (2026-02-03 21:00): 产品模块联调完成 | 1215 | - v2.1 (2026-02-03 21:00): 产品模块联调完成 | ... | ... |
| ... | @@ -70,7 +70,8 @@ const props = defineProps({ | ... | @@ -70,7 +70,8 @@ const props = defineProps({ |
| 70 | */ | 70 | */ |
| 71 | product: { | 71 | product: { |
| 72 | type: Object, | 72 | type: Object, |
| 73 | - required: true | 73 | + required: false, |
| 74 | + default: null | ||
| 74 | } | 75 | } |
| 75 | }) | 76 | }) |
| 76 | 77 | ||
| ... | @@ -112,7 +113,11 @@ const emit = defineEmits([ | ... | @@ -112,7 +113,11 @@ const emit = defineEmits([ |
| 112 | * // } | 113 | * // } |
| 113 | */ | 114 | */ |
| 114 | const templateConfig = computed(() => { | 115 | const templateConfig = computed(() => { |
| 115 | - if (!props.product?.form_sn) { | 116 | + if (!props.product) { |
| 117 | + return null | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + if (!props.product.form_sn) { | ||
| 116 | console.warn('[PlanFormContainer] 产品缺少 form_sn 字段', props.product) | 121 | console.warn('[PlanFormContainer] 产品缺少 form_sn 字段', props.product) |
| 117 | return null | 122 | return null |
| 118 | } | 123 | } |
| ... | @@ -197,6 +202,11 @@ const close = () => { | ... | @@ -197,6 +202,11 @@ const close = () => { |
| 197 | * @description 将表单数据和产品信息一起提交 | 202 | * @description 将表单数据和产品信息一起提交 |
| 198 | */ | 203 | */ |
| 199 | const submit = () => { | 204 | const submit = () => { |
| 205 | + if (!props.product) { | ||
| 206 | + console.error('[PlanFormContainer] 无法提交: 产品数据为空') | ||
| 207 | + return | ||
| 208 | + } | ||
| 209 | + | ||
| 200 | // 调用模版组件的校验方法 | 210 | // 调用模版组件的校验方法 |
| 201 | if (templateRef.value && templateRef.value.validate) { | 211 | if (templateRef.value && templateRef.value.validate) { |
| 202 | const isValid = templateRef.value.validate() | 212 | const isValid = templateRef.value.validate() | ... | ... |
| ... | @@ -135,6 +135,8 @@ | ... | @@ -135,6 +135,8 @@ |
| 135 | 135 | ||
| 136 | <!-- 计划书表单容器 --> | 136 | <!-- 计划书表单容器 --> |
| 137 | <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 --> | 137 | <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 --> |
| 138 | + <!-- 使用 v-if 条件渲染,避免 selectedProduct 为 null 时的 prop 类型检查错误 --> | ||
| 139 | + <view v-if="showPlanPopup && selectedProduct"> | ||
| 138 | <PlanFormContainer | 140 | <PlanFormContainer |
| 139 | v-model:visible="showPlanPopup" | 141 | v-model:visible="showPlanPopup" |
| 140 | :product="selectedProduct" | 142 | :product="selectedProduct" |
| ... | @@ -142,6 +144,7 @@ | ... | @@ -142,6 +144,7 @@ |
| 142 | @submit="handlePlanSubmit" | 144 | @submit="handlePlanSubmit" |
| 143 | /> | 145 | /> |
| 144 | </view> | 146 | </view> |
| 147 | + </view> | ||
| 145 | </template> | 148 | </template> |
| 146 | 149 | ||
| 147 | <script setup> | 150 | <script setup> | ... | ... |
-
Please register or login to post a comment