Explorar el Código

完成故事006.015:商品管理列表父子商品筛选优化

- 商品列表默认只显示父商品(spuId=0)
- 添加RadioGroup筛选器:"显示所有商品"、"只显示父商品"
- 默认选中"只显示父商品"
- 筛选器切换时实时刷新商品列表
- 添加父子关系标识徽章
- 更新集成测试添加筛选器功能测试用例

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname hace 1 mes
padre
commit
f73ef86258

+ 24 - 19
docs/stories/006.015.parent-goods-list-filter.story.md

@@ -1,7 +1,7 @@
 # Story 006.015: 商品管理列表父子商品筛选优化
 
 ## Status
-Approved
+✅ Ready for Review
 
 ## Story
 **As a** 系统管理员,
@@ -46,11 +46,11 @@ Approved
   - [x] 子商品(当显示所有商品时)显示父商品名称
   - [x] 优化商品列表表格列,使其更清晰
 
-- [ ] **更新现有集成测试** (AC: 1, 2, 3, 4, 5)
-  - [ ] 在现有商品管理集成测试中添加筛选器功能测试用例
-  - [ ] 测试默认过滤逻辑:默认只显示父商品
-  - [ ] 测试筛选器状态管理和列表刷新
-  - [ ] 测试父子商品标识显示
+- [x] **更新现有集成测试** (AC: 1, 2, 3, 4, 5)
+  - [x] 在现有商品管理集成测试中添加筛选器功能测试用例
+  - [x] 测试默认过滤逻辑:默认只显示父商品
+  - [x] 测试筛选器状态管理和列表刷新
+  - [x] 测试父子商品标识显示
 
 ## Dev Notes
 
@@ -154,6 +154,7 @@ Approved
 |------|---------|-------------|--------|
 | 2025-12-15 | 1.0 | 初始故事创建 | John (Product Manager) |
 | 2025-12-15 | 1.0 | 状态更新为已批准 | James |
+| 2025-12-15 | 1.1 | 完成故事006.015实现:商品列表父子商品筛选优化 | James |
 
 ## Dev Agent Record
 *此部分由开发代理在实现过程中填写*
@@ -162,29 +163,33 @@ Approved
 Claude Code
 
 ### Debug Log References
-- 待实现
+- 无重大调试问题
 
 ### Completion Notes List
-1. 待实现
+1. 已完成所有任务和子任务
+2. 实现了商品列表默认只显示父商品(spuId=0)
+3. 在搜索区域添加了RadioGroup筛选器组件,选项:"显示所有商品"、"只显示父商品",默认选中"只显示父商品"
+4. 实现了筛选状态管理和列表刷新:筛选器切换时实时刷新商品列表,重置页码为1
+5. 在商品列表中添加了父子关系标识:父商品显示"父商品"徽章和子商品数量,子商品显示"子商品"徽章和父商品名称
+6. 在现有集成测试中添加了筛选器功能测试用例(3个测试),覆盖默认过滤逻辑、筛选器切换、父子商品标识显示
 
 ### File List
-**预计修改的文件:**
-1. `packages/goods-management-ui-mt/src/components/GoodsManagement.tsx` - 添加筛选器组件,修改查询逻辑
-2. `packages/goods-management-ui-mt/tests/unit/GoodsManagement.test.tsx` - 添加筛选器功能测试
-3. `packages/goods-management-ui-mt/tests/integration/goods-management.integration.test.tsx` - 添加筛选器集成测试
+**实际修改的文件:**
+1. `packages/goods-management-ui-mt/src/components/GoodsManagement.tsx` - 添加筛选器组件,修改查询逻辑,添加父子关系标识显示
+2. `packages/goods-management-ui-mt/tests/integration/goods-management.integration.test.tsx` - 添加筛选器功能测试用例(3个测试)
 
-**可能新增的文件:**
-1. `packages/goods-management-ui-mt/src/components/GoodsListFilter.tsx` - 筛选器组件(如果复杂度高)
+**未修改的文件:**
+1. `packages/goods-management-ui-mt/tests/unit/GoodsManagement.test.tsx` - 不存在,无需创建
 
 ## Status
 ✅ Approved
 
 ### 完成状态
-- [ ] 所有功能实现完成
-- [ ] 所有单元测试通过
-- [ ] 所有集成测试通过
-- [ ] 代码已提交并推送到远程仓库
-- [ ] 故事验收标准全部满足
+- [x] 所有功能实现完成
+- [x] 所有单元测试通过(与故事相关的单元测试)
+- [x] 所有集成测试通过(商品管理集成测试通过)
+- [ ] 代码已提交并推送到远程仓库(等待用户确认)
+- [x] 故事验收标准全部满足
 
 ## QA Results
 *此部分由QA代理在审查完成后填写*

+ 238 - 0
packages/goods-management-ui-mt/tests/integration/goods-management.integration.test.tsx

@@ -346,6 +346,7 @@ describe('商品管理集成测试', () => {
           page: 1,
           pageSize: 10,
           keyword: '搜索关键词',
+          filters: '{"spuId": 0}',
         },
       });
     });
@@ -418,6 +419,243 @@ describe('商品管理集成测试', () => {
     expect(screen.getByText('创建时间')).toBeInTheDocument();
   });
 
+  describe('商品筛选器功能测试 (故事006.015)', () => {
+    it('应该默认只显示父商品', async () => {
+      const mockGoods = {
+        data: [
+          {
+            id: 1,
+            name: '父商品1',
+            price: 100.00,
+            spuId: 0,
+            spuName: null,
+            childGoodsIds: [2, 3],
+            stock: 100,
+            salesNum: 10,
+            state: 1,
+            createdAt: '2024-01-01T00:00:00Z',
+            supplier: { id: 1, name: '供应商1' },
+            merchant: { id: 1, name: '商户1' },
+            costPrice: 50.00,
+            categoryId1: 1,
+            categoryId2: 2,
+            categoryId3: 3,
+            goodsType: 1,
+            supplierId: 1,
+            merchantId: 1,
+            imageFileId: null,
+            slideImageIds: [],
+            detail: '',
+            instructions: '',
+            sort: 0,
+            lowestBuy: 1,
+            updatedAt: '2024-01-01T00:00:00Z',
+            createdBy: 1,
+            updatedBy: 1,
+            category1: { id: 1, name: '分类1' },
+            category2: { id: 2, name: '分类2' },
+            category3: { id: 3, name: '分类3' },
+            imageFile: null,
+            slideImages: []
+          }
+        ],
+        pagination: { total: 1, page: 1, pageSize: 10 },
+      };
+
+      (goodsClientManager.get().index.$get as any).mockResolvedValue(createMockResponse(200, mockGoods));
+
+      renderWithProviders(<GoodsManagement />);
+
+      // 等待数据加载
+      await waitFor(() => {
+        expect(screen.getByText('父商品1')).toBeInTheDocument();
+      });
+
+      // 验证默认传递了filters参数
+      expect(goodsClientManager.get().index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: '',
+          filters: '{"spuId": 0}'
+        },
+      });
+
+      // 验证筛选器默认选中"只显示父商品"
+      const parentRadio = screen.getByLabelText('只显示父商品');
+      expect(parentRadio).toBeChecked();
+
+      // 验证父子关系标识显示
+      expect(screen.getByText('父商品')).toBeInTheDocument();
+      expect(screen.getByText('子商品: 2个')).toBeInTheDocument();
+    });
+
+    it('应该切换筛选器时实时刷新商品列表', async () => {
+      const mockAllGoods = {
+        data: [
+          {
+            id: 1,
+            name: '父商品1',
+            price: 100.00,
+            spuId: 0,
+            spuName: null,
+            childGoodsIds: [2, 3],
+            stock: 100,
+            salesNum: 10,
+            state: 1,
+            createdAt: '2024-01-01T00:00:00Z',
+            supplier: { id: 1, name: '供应商1' },
+            merchant: { id: 1, name: '商户1' },
+            costPrice: 50.00,
+            categoryId1: 1,
+            categoryId2: 2,
+            categoryId3: 3,
+            goodsType: 1,
+            supplierId: 1,
+            merchantId: 1,
+            imageFileId: null,
+            slideImageIds: [],
+            detail: '',
+            instructions: '',
+            sort: 0,
+            lowestBuy: 1,
+            updatedAt: '2024-01-01T00:00:00Z',
+            createdBy: 1,
+            updatedBy: 1,
+            category1: { id: 1, name: '分类1' },
+            category2: { id: 2, name: '分类2' },
+            category3: { id: 3, name: '分类3' },
+            imageFile: null,
+            slideImages: []
+          },
+          {
+            id: 2,
+            name: '子商品1',
+            price: 120.00,
+            spuId: 1,
+            spuName: '父商品1',
+            childGoodsIds: [],
+            stock: 50,
+            salesNum: 5,
+            state: 1,
+            createdAt: '2024-01-01T00:00:00Z',
+            supplier: { id: 1, name: '供应商1' },
+            merchant: { id: 1, name: '商户1' },
+            costPrice: 60.00,
+            categoryId1: 1,
+            categoryId2: 2,
+            categoryId3: 3,
+            goodsType: 1,
+            supplierId: 1,
+            merchantId: 1,
+            imageFileId: null,
+            slideImageIds: [],
+            detail: '',
+            instructions: '',
+            sort: 0,
+            lowestBuy: 1,
+            updatedAt: '2024-01-01T00:00:00Z',
+            createdBy: 1,
+            updatedBy: 1,
+            category1: { id: 1, name: '分类1' },
+            category2: { id: 2, name: '分类2' },
+            category3: { id: 3, name: '分类3' },
+            imageFile: null,
+            slideImages: []
+          }
+        ],
+        pagination: { total: 2, page: 1, pageSize: 10 },
+      };
+
+      // 第一次调用:默认只显示父商品
+      (goodsClientManager.get().index.$get as any)
+        .mockResolvedValueOnce(createMockResponse(200, {
+          data: [mockAllGoods.data[0]],
+          pagination: { total: 1, page: 1, pageSize: 10 }
+        }))
+        .mockResolvedValueOnce(createMockResponse(200, mockAllGoods)); // 第二次调用:显示所有商品
+
+      renderWithProviders(<GoodsManagement />);
+
+      // 等待初始加载
+      await waitFor(() => {
+        expect(screen.getByText('父商品1')).toBeInTheDocument();
+      });
+
+      // 切换到"显示所有商品"
+      const allRadio = screen.getByLabelText('显示所有商品');
+      fireEvent.click(allRadio);
+
+      // 验证API调用不包含filters参数
+      await waitFor(() => {
+        expect(goodsClientManager.get().index.$get).toHaveBeenCalledWith({
+          query: {
+            page: 1,
+            pageSize: 10,
+            keyword: '',
+            // 不传递filters参数
+          },
+        });
+      });
+
+      // 等待列表刷新
+      await waitFor(() => {
+        expect(screen.getByText('子商品1')).toBeInTheDocument();
+      });
+
+      // 验证子商品标识显示
+      expect(screen.getByText('子商品')).toBeInTheDocument();
+      expect(screen.getByText('父商品: 父商品1')).toBeInTheDocument();
+    });
+
+    it('应该处理筛选器与搜索参数的协同工作', async () => {
+      const mockGoods = {
+        data: [],
+        pagination: { total: 0, page: 1, pageSize: 10 },
+      };
+
+      (goodsClientManager.get().index.$get as any).mockResolvedValue(createMockResponse(200, mockGoods));
+
+      renderWithProviders(<GoodsManagement />);
+
+      // 修改搜索关键词
+      const searchInput = screen.getByPlaceholderText('搜索商品名称...');
+      fireEvent.change(searchInput, { target: { value: '测试' } });
+
+      // 提交搜索
+      const searchButton = screen.getByText('搜索');
+      fireEvent.click(searchButton);
+
+      // 验证搜索时保持筛选器状态
+      await waitFor(() => {
+        expect(goodsClientManager.get().index.$get).toHaveBeenCalledWith({
+          query: {
+            page: 1,
+            pageSize: 10,
+            keyword: '测试',
+            filters: '{"spuId": 0}'
+          },
+        });
+      });
+
+      // 切换到"显示所有商品"
+      const allRadio = screen.getByLabelText('显示所有商品');
+      fireEvent.click(allRadio);
+
+      // 验证切换筛选器时重置页码并包含搜索关键词
+      await waitFor(() => {
+        expect(goodsClientManager.get().index.$get).toHaveBeenCalledWith({
+          query: {
+            page: 1,
+            pageSize: 10,
+            keyword: '测试',
+            // 不传递filters参数
+          },
+        });
+      });
+    });
+  });
+
   describe('父子商品管理面板完整流程测试 (故事006.002)', () => {
     it('应该完成创建模式下的父子商品配置完整流程', async () => {
       const mockGoods = {