hookehuyr

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>
...@@ -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,12 +135,15 @@ ...@@ -135,12 +135,15 @@
135 135
136 <!-- 计划书表单容器 --> 136 <!-- 计划书表单容器 -->
137 <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 --> 137 <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 -->
138 - <PlanFormContainer 138 + <!-- 使用 v-if 条件渲染,避免 selectedProduct 为 null 时的 prop 类型检查错误 -->
139 - v-model:visible="showPlanPopup" 139 + <view v-if="showPlanPopup && selectedProduct">
140 - :product="selectedProduct" 140 + <PlanFormContainer
141 - @close="showPlanPopup = false" 141 + v-model:visible="showPlanPopup"
142 - @submit="handlePlanSubmit" 142 + :product="selectedProduct"
143 - /> 143 + @close="showPlanPopup = false"
144 + @submit="handlePlanSubmit"
145 + />
146 + </view>
144 </view> 147 </view>
145 </template> 148 </template>
146 149
......