Browse Source

feat(story-008.002): 完成渠道选择器组件实现

- 创建ChannelSelector组件,参照PlatformSelector模式
- 添加渠道选择器集成测试(7个测试)
- 更新包导出配置(src/index.ts, src/components/index.ts)
- 更新故事文档状态为Ready for Review

🤖 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 6 ngày trước cách đây
mục cha
commit
addc7ad128

+ 78 - 0
allin-packages/channel-management-ui/src/components/ChannelSelector.tsx

@@ -0,0 +1,78 @@
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@d8d/shared-ui-components/components/ui/select';
+import { channelClientManager } from '../api/channelClient';
+
+interface ChannelSelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+  className?: string;
+  testId?: string;
+}
+
+export const ChannelSelector: React.FC<ChannelSelectorProps> = ({
+  value,
+  onChange,
+  placeholder = "请选择渠道",
+  disabled = false,
+  className,
+  testId,
+}) => {
+  const {
+    data: channels,
+    isLoading,
+    isError,
+  } = useQuery({
+    queryKey: ['channels'],
+    queryFn: async () => {
+      const client = channelClientManager.get()
+      const res = await client.getAllChannels.$get({
+        query: {
+          skip: 0,
+          take: 100
+        }
+      });
+      if (res.status !== 200) throw new Error('获取渠道列表失败');
+      return await res.json();
+    },
+  });
+
+  if (isError) {
+    return (
+      <div className="text-sm text-destructive">
+        加载渠道列表失败
+      </div>
+    );
+  }
+
+  const channelList = channels?.data || [];
+
+  return (
+    <Select
+      value={value?.toString()}
+      onValueChange={(val) => onChange?.(parseInt(val))}
+      disabled={disabled || isLoading || channelList.length === 0}
+    >
+      <SelectTrigger className={className} data-testid={testId}>
+        <SelectValue placeholder={isLoading ? '加载中...' : placeholder} />
+      </SelectTrigger>
+      <SelectContent>
+        {channelList.map((channel) => (
+          <SelectItem key={channel.id} value={channel.id.toString()}>
+            {channel.channelName}
+          </SelectItem>
+        ))}
+      </SelectContent>
+    </Select>
+  );
+};
+
+export default ChannelSelector;

+ 2 - 0
allin-packages/channel-management-ui/src/components/index.ts

@@ -0,0 +1,2 @@
+export { default as ChannelManagement } from './ChannelManagement';
+export { ChannelSelector } from './ChannelSelector';

+ 22 - 0
allin-packages/channel-management-ui/src/index.ts

@@ -0,0 +1,22 @@
+// 导出组件
+export { ChannelManagement, ChannelSelector } from './components';
+
+// 导出API
+export { channelClient, channelClientManager } from './api/channelClient';
+
+// 导出类型
+export type {
+  ChannelListItem,
+  ChannelSearchParams,
+  PaginatedResponse,
+  CreateChannelRequest,
+  UpdateChannelRequest,
+  DeleteChannelRequest,
+  SearchChannelsRequest,
+  GetChannelsQuery,
+  GetChannelDetailParam,
+  ChannelResponse,
+  ChannelListResponse,
+  ChannelDetailResponse,
+  SearchChannelsResponse,
+} from './types';

+ 214 - 0
allin-packages/channel-management-ui/tests/integration/channel-selector.integration.test.tsx

@@ -0,0 +1,214 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ChannelSelector } from '../../src/components/ChannelSelector'
+import { channelClient } from '../../src/api/channelClient'
+
+// Mock the channel client
+vi.mock('../../src/api/channelClient', () => {
+  const channelClient = {
+    getAllChannels: {
+      $get: vi.fn(),
+    },
+  }
+  return {
+    channelClient,
+    channelClientManager: {
+      get: vi.fn(() => channelClient),
+    },
+  }
+})
+
+const mockChannels = {
+  data: [
+    { id: 1, channelName: '微信小程序渠道', channelType: '小程序', contactPerson: '张三', contactPhone: '13800138000', description: '微信小程序渠道', status: 1, createTime: '2024-01-01T00:00:00Z', updateTime: '2024-01-01T00:00:00Z' },
+    { id: 2, channelName: '抖音广告渠道', channelType: '广告', contactPerson: '李四', contactPhone: '13900139000', description: '抖音广告渠道', status: 1, createTime: '2024-01-01T00:00:00Z', updateTime: '2024-01-01T00:00:00Z' },
+    { id: 3, channelName: '快手直播渠道', channelType: '直播', contactPerson: '王五', contactPhone: '13700137000', description: '快手直播渠道', status: 0, createTime: '2024-01-01T00:00:00Z', updateTime: '2024-01-01T00:00:00Z' },
+  ],
+  pagination: {
+    total: 3,
+    page: 1,
+    pageSize: 10,
+    totalPages: 1,
+  },
+}
+
+const TestWrapper = ({ children }: { children: React.ReactNode }) => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  })
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children as any}
+    </QueryClientProvider>
+  )
+}
+
+describe('ChannelSelector 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks()
+  })
+
+  it('应该正确渲染渠道选择器', async () => {
+    (channelClient.getAllChannels.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockChannels,
+    })
+
+    render(
+      <TestWrapper>
+        <ChannelSelector testId="channel-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('channel-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('请选择渠道')).toBeInTheDocument()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('channel-selector')
+    fireEvent.click(selectTrigger)
+
+    // 验证下拉菜单中的选项
+    await waitFor(() => {
+      expect(screen.getByText('微信小程序渠道')).toBeInTheDocument()
+      expect(screen.getByText('抖音广告渠道')).toBeInTheDocument()
+      expect(screen.getByText('快手直播渠道')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理加载状态', () => {
+    // Mock 延迟响应
+    (channelClient.getAllChannels.$get as any).mockImplementation(
+      () => new Promise(() => {}) // 永不解析的Promise
+    )
+
+    render(
+      <TestWrapper>
+        <ChannelSelector testId="channel-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('channel-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+  })
+
+  it('应该处理错误状态', async () => {
+    (channelClient.getAllChannels.$get as any).mockRejectedValue(new Error('API错误'))
+
+    render(
+      <TestWrapper>
+        <ChannelSelector testId="channel-selector" />
+      </TestWrapper>
+    )
+
+    // 等待错误状态
+    await waitFor(() => {
+      expect(screen.getByText('加载渠道列表失败')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理选择器值变化', async () => {
+    (channelClient.getAllChannels.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockChannels,
+    })
+
+    const mockOnChange = vi.fn()
+
+    render(
+      <TestWrapper>
+        <ChannelSelector onChange={mockOnChange} testId="channel-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('channel-selector')).toBeEnabled()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('channel-selector')
+    fireEvent.click(selectTrigger)
+
+    // 选择第一个选项
+    await waitFor(() => {
+      const firstOption = screen.getByText('微信小程序渠道')
+      fireEvent.click(firstOption)
+    })
+
+    // 验证onChange被调用
+    expect(mockOnChange).toHaveBeenCalledWith(1)
+  })
+
+  it('应该支持自定义占位符', async () => {
+    (channelClient.getAllChannels.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockChannels,
+    })
+
+    render(
+      <TestWrapper>
+        <ChannelSelector placeholder="选择渠道" testId="channel-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('选择渠道')).toBeInTheDocument()
+    })
+  })
+
+  it('应该支持禁用状态', async () => {
+    (channelClient.getAllChannels.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockChannels,
+    })
+
+    render(
+      <TestWrapper>
+        <ChannelSelector disabled={true} testId="channel-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      const selectTrigger = screen.getByTestId('channel-selector')
+      expect(selectTrigger).toBeDisabled()
+    })
+  })
+
+  it('应该支持预选值', async () => {
+    (channelClient.getAllChannels.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockChannels,
+    })
+
+    render(
+      <TestWrapper>
+        <ChannelSelector value={2} testId="channel-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('channel-selector')).toBeEnabled()
+    })
+
+    // 验证预选值已正确设置
+    // 在Radix UI Select中,预选值会显示在选择器触发器中
+    const selectTrigger = screen.getByTestId('channel-selector')
+    expect(selectTrigger).toHaveTextContent('抖音广告渠道')
+  })
+})

+ 10 - 8
docs/stories/008.002.transplant-channel-management-ui.story.md

@@ -1,7 +1,7 @@
 # Story 008.002: 移植渠道管理UI(channel → @d8d/allin-channel-management-ui)
 
 ## Status
-Ready for Development
+Ready for Review
 
 ## Story
 **As a** 开发者,
@@ -109,16 +109,16 @@ Ready for Development
   - [x] 修复测试失败和类型错误(修复了测试选择器冲突、路由名称问题、类型推导错误)
   - [x] 验证测试覆盖率(集成测试全部通过,类型检查通过)
 
-- [ ] 任务9:创建渠道选择器组件 (AC: 10)
-  - [ ] 创建`src/components/ChannelSelector.tsx`组件
+- [x] 任务9:创建渠道选择器组件 (AC: 10)
+  - [x] 创建`src/components/ChannelSelector.tsx`组件
     - **参考文件**:`allin-packages/platform-management-ui/src/components/PlatformSelector.tsx`
     - **架构**:使用React Query获取渠道列表,使用@d8d/shared-ui-components的Select组件
     - **功能**:渠道选择器,支持value/onChange等标准props,显示渠道名称
     - **用途**:作为可复用组件供其他UI包使用(如广告管理、内容管理等需要选择渠道的场景)
-  - [ ] 创建渠道选择器集成测试
+  - [x] 创建渠道选择器集成测试
     - **参考文件**:`allin-packages/platform-management-ui/tests/integration/platform-selector.integration.test.tsx`
     - **测试内容**:数据加载、选择功能、错误处理、禁用状态
-  - [ ] 更新package.json导出配置
+  - [x] 更新package.json导出配置
     - **导出**:在`src/index.ts`中导出ChannelSelector组件
     - **依赖**:确保组件可被其他模块导入使用
 
@@ -259,7 +259,7 @@ Ready for Development
 4. ✅ 前后端Schema一致性验证完成
 5. ✅ API路径映射正确性验证完成
 6. ✅ 技术栈转换完成(Ant Design → @d8d/shared-ui-components,Jotai → React Query,Ant Form → React Hook Form + Zod)
-7. ⬜ 第9个任务(渠道选择器组件)待完成
+7. ✅ 第9个任务(渠道选择器组件)已完成
 
 ### File List
 1. `allin-packages/channel-management-ui/package.json` - 包配置
@@ -271,8 +271,10 @@ Ready for Development
 7. `allin-packages/channel-management-ui/src/types/index.ts` - 类型定义
 8. `allin-packages/channel-management-ui/tests/integration/channel.integration.test.tsx` - 集成测试
 9. `allin-packages/channel-management-ui/src/schemas/channel.schema.ts` - 表单验证Schema
-10. `allin-packages/channel-management-ui/src/components/ChannelSelector.tsx` - 渠道选择器组件(待创建)
-11. `allin-packages/channel-management-ui/tests/integration/channel-selector.integration.test.tsx` - 渠道选择器集成测试(待创建)
+10. `allin-packages/channel-management-ui/src/components/ChannelSelector.tsx` - 渠道选择器组件
+11. `allin-packages/channel-management-ui/tests/integration/channel-selector.integration.test.tsx` - 渠道选择器集成测试
+12. `allin-packages/channel-management-ui/src/components/index.ts` - 组件导出文件
+13. `allin-packages/channel-management-ui/src/index.ts` - 包主导出文件
 
 ## QA Results
 *此部分由QA代理在QA审查后填写*