Przeglądaj źródła

docs(stories): 创建故事 010.002 - 统一广告管理UI包

- 创建统一广告管理UI包 (unified-advertisement-management-ui)
- 实现广告和广告类型管理组件
- RPC客户端指向管理员路由
- 使用RPC推断类型,避免直接导入schema
- 表单使用条件渲染两个独立的Form组件
- 包含组件集成测试要求

验收标准: 7项 | 任务: 8主27子 | 检查清单: READY (5/5)

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 2 tygodni temu
rodzic
commit
f44ea786b6
1 zmienionych plików z 405 dodań i 0 usunięć
  1. 405 0
      docs/stories/010.002.story.md

+ 405 - 0
docs/stories/010.002.story.md

@@ -0,0 +1,405 @@
+# Story 010.002: 创建统一广告管理UI包
+
+## Status
+Approved
+
+## Story
+
+**As a** 超级管理员,
+**I want** 一个统一广告管理的UI包,
+**so that** 可以在租户管理后台统一管理所有广告和广告类型。
+
+## Acceptance Criteria
+
+1. 创建 `packages/unified-advertisement-management-ui` 包,包含完整的组件、API客户端、类型定义
+2. 实现广告管理组件(列表、创建、编辑、删除),API端点指向 `@d8d/unified-advertisements-module` 的管理员路由
+3. 实现广告类型管理组件(列表、创建、编辑、删除)
+4. 使用 RPC 客户端管理器模式,指向 `/api/v1/admin/unified-advertisements` 管理员端点
+5. 类型定义使用 RPC 推断类型,避免直接导入 schema 类型
+6. 表单组件使用条件渲染两个独立的 Form 组件(创建和编辑),避免动态切换 props
+7. 包含组件测试和类型检查
+
+## Tasks / Subtasks
+
+- [ ] **任务1: 创建包结构和配置文件** (AC: 1)
+  - [ ] 创建 `packages/unified-advertisement-management-ui` 目录
+  - [ ] 创建 `package.json`,配置包名为 `@d8d/unified-advertisement-management-ui`
+  - [ ] 创建 `tsconfig.json`
+  - [ ] 创建 `vitest.config.ts`(设置 `fileParallelism: false`)
+  - [ ] 创建 `src/` 子目录:`components/`, `api/`, `types/`
+  - [ ] 创建 `tests/` 子目录:`integration/`, `components/`
+
+- [ ] **任务2: 实现 RPC 客户端管理器** (AC: 4)
+  - [ ] 创建 `src/api/unifiedAdvertisementClient.ts`,使用单例模式
+  - [ ] 从 `@d8d/unified-advertisements-module` 导入管理员路由
+  - [ ] 实现 `UnifiedAdvertisementClientManager` 类(`init()`, `get()`, `reset()` 方法)
+  - [ ] 创建 `src/api/index.ts` 导出客户端
+  - [ ] 为广告类型创建独立客户端 `unifiedAdvertisementTypeClient.ts`
+
+- [ ] **任务3: 定义类型** (AC: 5)
+  - [ ] 创建 `src/types/index.ts`
+  - [ ] 使用 RPC 推断类型:`UnifiedAdvertisementResponse`, `CreateUnifiedAdvertisementRequest`, `UpdateUnifiedAdvertisementRequest`
+  - [ ] 推断搜索参数和分页响应类型
+  - [ ] 为广告类型定义对应类型
+
+- [ ] **任务4: 实现广告管理组件** (AC: 2, 6)
+  - [ ] 创建 `src/components/UnifiedAdvertisementManagement.tsx`
+  - [ ] 使用 React Query 进行数据查询和变更
+  - [ ] 实现广告列表展示(表格)
+  - [ ] 实现创建和编辑表单(使用条件渲染两个独立的 Form 组件)
+  - [ ] 为关键元素添加 `data-testid` 属性
+  - [ ] 使用 `@d8d/shared-ui-components` 中的 shadcn/ui 组件
+  - [ ] 创建 `src/components/index.ts` 导出组件
+
+- [ ] **任务5: 实现广告类型管理组件** (AC: 3, 6)
+  - [ ] 创建 `src/components/UnifiedAdvertisementTypeManagement.tsx`
+  - [ ] 使用 React Query 进行数据查询和变更
+  - [ ] 实现广告类型列表展示
+  - [ ] 实现创建和编辑表单(使用条件渲染两个独立的 Form 组件)
+  - [ ] 为关键元素添加 `data-testid` 属性
+
+- [ ] **任务6: 创建包导出入口** (AC: 1)
+  - [ ] 创建 `src/index.ts`,导出组件、API客户端、类型
+  - [ ] 配置 `package.json` 的 `exports` 字段,支持子路径导出
+
+- [ ] **任务7: 编写组件集成测试** (AC: 7)
+  - [ ] 创建 `tests/setup.ts`,添加必要的 mock(sonner, scrollIntoView)
+  - [ ] 创建 `tests/integration/unified-advertisement-management.integration.test.tsx`
+  - [ ] 测试列表加载、创建、编辑、删除功能
+  - [ ] 测试表单验证和错误处理
+  - [ ] 创建广告类型组件的集成测试
+
+- [ ] **任务8: 代码质量检查** (AC: 1, 7)
+  - [ ] 运行 `pnpm typecheck` 确保无 TypeScript 错误
+  - [ ] 运行 `pnpm lint` 确保代码符合规范
+  - [ ] 运行 `pnpm test` 确保所有测试通过
+
+## Dev Notes
+
+### 项目结构信息
+
+**新包位置**:
+```
+packages/unified-advertisement-management-ui/
+├── package.json
+├── tsconfig.json
+├── vitest.config.ts
+├── src/
+│   ├── api/
+│   │   ├── unifiedAdvertisementClient.ts    # 广告RPC客户端管理器
+│   │   ├── unifiedAdvertisementTypeClient.ts # 广告类型RPC客户端管理器
+│   │   └── index.ts
+│   ├── components/
+│   │   ├── UnifiedAdvertisementManagement.tsx      # 广告管理组件
+│   │   ├── UnifiedAdvertisementTypeManagement.tsx  # 广告类型管理组件
+│   │   └── index.ts
+│   ├── types/
+│   │   └── index.ts         # 类型定义(RPC推断)
+│   └── index.ts             # 包入口
+└── tests/
+    ├── setup.ts             # 测试设置
+    ├── integration/         # 集成测试
+    └── components/          # 组件测试
+```
+
+**参考模块**:
+- 原多租户广告管理UI: `packages/advertisement-management-ui-mt`
+- 原多租户广告类型管理UI: `packages/advertisement-type-management-ui-mt`
+- 统一广告后端模块: `@d8d/unified-advertisements-module`
+
+### API端点设计
+
+**管理员接口** (使用 `tenantAuthMiddleware`):
+```typescript
+// 广告管理端点
+GET    /api/v1/admin/unified-advertisements       # 广告列表
+POST   /api/v1/admin/unified-advertisements       # 创建广告
+PUT    /api/v1/admin/unified-advertisements/:id   # 更新广告
+DELETE /api/v1/admin/unified-advertisements/:id   # 删除广告
+
+// 广告类型管理端点
+GET    /api/v1/admin/unified-advertisement-types       # 广告类型列表
+POST   /api/v1/admin/unified-advertisement-types       # 创建广告类型
+PUT    /api/v1/admin/unified-advertisement-types/:id   # 更新广告类型
+DELETE /api/v1/admin/unified-advertisement-types/:id   # 删除广告类型
+```
+
+### RPC客户端实现规范
+
+[Source: docs/architecture/ui-package-standards.md#RPC客户端实现规范]
+
+```typescript
+// src/api/unifiedAdvertisementClient.ts
+import { adminUnifiedAdRoutes } from '@d8d/unified-advertisements-module';
+import { rpcClient } from '@d8d/shared-ui-components/utils/hc';
+
+export class UnifiedAdvertisementClientManager {
+  private static instance: UnifiedAdvertisementClientManager;
+  private client: ReturnType<typeof rpcClient<typeof adminUnifiedAdRoutes>> | null = null;
+
+  private constructor() {}
+
+  public static getInstance(): UnifiedAdvertisementClientManager {
+    if (!UnifiedAdvertisementClientManager.instance) {
+      UnifiedAdvertisementClientManager.instance = new UnifiedAdvertisementClientManager();
+    }
+    return UnifiedAdvertisementClientManager.instance;
+  }
+
+  public init(baseUrl: string = '/'): ReturnType<typeof rpcClient<typeof adminUnifiedAdRoutes>> {
+    return this.client = rpcClient<typeof adminUnifiedAdRoutes>(baseUrl);
+  }
+
+  public get(): ReturnType<typeof rpcClient<typeof adminUnifiedAdRoutes>> {
+    if (!this.client) {
+      return this.init();
+    }
+    return this.client;
+  }
+
+  public reset(): void {
+    this.client = null;
+  }
+}
+
+const unifiedAdvertisementClientManager = UnifiedAdvertisementClientManager.getInstance();
+export const unifiedAdvertisementClient = unifiedAdvertisementClientManager.get();
+
+export {
+  UnifiedAdvertisementClientManager,
+  unifiedAdvertisementClientManager,
+  unifiedAdvertisementClient
+};
+```
+
+### 类型定义规范
+
+[Source: docs/architecture/ui-package-standards.md#类型推断最佳实践]
+
+```typescript
+// src/types/index.ts
+import type { InferResponseType, InferRequestType } from 'hono';
+import type { adminUnifiedAdRoutes } from '@d8d/unified-advertisements-module';
+import { unifiedAdvertisementClient } from '../api/unifiedAdvertisementClient';
+
+// ✅ 正确:使用RPC推断类型(推荐)
+export type UnifiedAdvertisementResponse = InferResponseType<typeof unifiedAdvertisementClient.index.$get, 200>;
+export type UnifiedAdvertisementListItem = UnifiedAdvertisementResponse['data'][0];
+export type CreateUnifiedAdvertisementRequest = InferRequestType<typeof unifiedAdvertisementClient.index.$post>;
+export type UpdateUnifiedAdvertisementRequest = InferRequestType<typeof unifiedAdvertisementClient[':id']['$put']>;
+
+// 搜索参数类型
+export interface UnifiedAdvertisementSearchParams {
+  page: number;
+  pageSize: number;
+  keyword?: string;
+}
+
+// ❌ 错误:直接导入schema类型(可能导致Date/string不匹配)
+// import type { UnifiedAdvertisement } from '@d8d/unified-advertisements-module/schemas';
+```
+
+### 表单组件规范
+
+[Source: docs/architecture/ui-package-standards.md#表单组件模式规范]
+
+**关键设计**: 使用条件渲染两个独立的 Form 组件
+
+```typescript
+// ✅ 正确:条件渲染两个独立的Form组件
+{isCreateForm ? (
+  <Form {...createForm}>
+    {/* 创建表单内容 */}
+  </Form>
+) : (
+  <Form {...updateForm}>
+    {/* 编辑表单内容 */}
+  </Form>
+)}
+
+// ❌ 错误:在单个Form组件上动态切换props(可能导致类型不兼容)
+<Form {...(isCreateForm ? createForm : updateForm)}>
+  {/* 表单内容 */}
+</Form>
+```
+
+### 测试规范
+
+**测试选择器优化**: 为关键交互元素添加 `data-testid` 属性
+
+[Source: docs/architecture/ui-package-standards.md#测试选择器优化规范]
+
+```typescript
+// 在组件中添加test ID
+<DialogTitle data-testid="create-unified-advertisement-modal-title">创建广告</DialogTitle>
+<Button data-testid="create-unified-advertisement-button">新建广告</Button>
+
+// 在测试中使用test ID
+const modalTitle = screen.getByTestId('create-unified-advertisement-modal-title');
+const createButton = screen.getByTestId('create-unified-advertisement-button');
+```
+
+**Radix UI组件测试环境修复**: 在测试setup文件中添加必要的DOM API mock
+
+[Source: docs/architecture/ui-package-standards.md#Radix UI组件测试环境修复]
+
+```typescript
+// tests/setup.ts
+import '@testing-library/jest-dom';
+import { vi } from 'vitest';
+
+// Mock sonner
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+    warning: vi.fn(),
+    info: vi.fn()
+  }
+}));
+
+// Mock scrollIntoView for Radix UI components
+Element.prototype.scrollIntoView = vi.fn();
+```
+
+### package.json配置参考
+
+[Source: docs/architecture/ui-package-standards.md#package.json配置]
+
+```json
+{
+  "name": "@d8d/unified-advertisement-management-ui",
+  "version": "1.0.0",
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts"
+    },
+    "./components": {
+      "types": "./src/components/index.ts",
+      "import": "./src/components/index.ts"
+    },
+    "./api": {
+      "types": "./src/api/index.ts",
+      "import": "./src/api/index.ts"
+    }
+  },
+  "scripts": {
+    "build": "unbuild",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "test:coverage": "vitest run --coverage",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@d8d/unified-advertisements-module": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.9",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "date-fns": "^4.1.0",
+    "hono": "^4.8.5",
+    "lucide-react": "^0.536.0",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "react-hook-form": "^7.61.1",
+    "sonner": "^2.0.7",
+    "tailwind-merge": "^3.3.1",
+    "zod": "^4.0.15"
+  }
+}
+```
+
+### 前一个故事的关键经验
+
+**从故事 010.001 学到的关键点**:
+1. 统一广告模块已创建完成,包含完整的管理员路由和用户展示路由
+2. 管理员路由使用 `tenantAuthMiddleware`,只有超级管理员(ID=1)可访问
+3. Entity 包含 `@ManyToOne` 关联 `FileMt` 和 `UnifiedAdvertisementType`
+4. API 响应结构与原 `advertisements-module-mt` 完全一致
+5. 广告类型包含 `code` 字段唯一索引
+
+**本故事需要注意的点**:
+1. API客户端必须指向管理员路由(`/api/v1/admin/unified-advertisements`),不是用户路由
+2. 类型必须使用 RPC 推断,避免直接导入 schema
+3. 表单使用条件渲染两个独立的 Form 组件
+4. 测试需要 mock sonner 和 scrollIntoView(Radix UI 组件需要)
+
+### 技术栈
+
+- **前端框架**: React 19.1.0 + TypeScript
+- **状态管理**: @tanstack/react-query
+- **表单**: react-hook-form + zod
+- **UI组件**: shadcn/ui (Radix UI)
+- **测试**: Vitest + Testing Library
+
+## Testing
+
+### 测试文件位置
+- 集成测试: `tests/integration/`
+- 组件测试: `tests/components/`
+- 测试工具: `tests/setup.ts`
+
+### 测试框架
+- **Vitest**: 主要测试运行器
+- **Testing Library**: React组件测试
+- **sonner mock**: Toast通知mock
+
+### 测试标准
+[Source: docs/architecture/testing-strategy.md]
+
+| 测试类型 | 最低要求 | 目标要求 |
+|----------|----------|----------|
+| 组件测试 | 70% | 80% |
+| 集成测试 | 50% | 60% |
+
+### 关键测试要求
+1. **API调用测试**: 验证RPC客户端正确调用管理员端点
+2. **类型安全测试**: 验证类型推断正确
+3. **表单验证测试**: 验证创建和编辑表单独立工作
+4. **错误处理测试**: 验证API错误正确显示给用户
+5. **test ID测试**: 使用data-testid选择器,避免文本冲突
+
+### 测试执行命令
+```bash
+# 进入UI包目录
+cd packages/unified-advertisement-management-ui
+
+# 运行所有测试
+pnpm test
+
+# 生成覆盖率报告
+pnpm test:coverage
+
+# 类型检查
+pnpm typecheck
+```
+
+## Change Log
+
+| Date | Version | Description | Author |
+|------|---------|-------------|--------|
+| 2026-01-03 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+
+## Dev Agent Record
+
+### Agent Model Used
+_待开发代理填写_
+
+### Debug Log References
+_待开发代理填写_
+
+### Completion Notes List
+_待开发代理填写_
+
+### File List
+_待开发代理填写_
+
+## QA Results
+_待QA代理填写_