Просмотр исходного кода

docs: 批准故事 010.010 - 创建统一文件管理UI包

- 从单租户文件管理UI复制创建统一文件管理UI包
- 必须使用 cp 命令直接复制整个文件夹
- API指向统一文件模块的管理员路由
- 包含文件管理组件和文件选择器组件
- 测试覆盖率要求70%以上

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 2 недель назад
Родитель
Сommit
2483730ed8
2 измененных файлов с 374 добавлено и 1 удалено
  1. 2 1
      docs/prd/epic-010-unified-ad-management.md
  2. 372 0
      docs/stories/010.010.story.md

+ 2 - 1
docs/prd/epic-010-unified-ad-management.md

@@ -20,6 +20,7 @@
 | 1.13 | 2026-01-03 | 添加故事010.009-011:拆分统一文件模块为三个故事 | James (Claude Code) |
 | 1.14 | 2026-01-04 | 批准故事010.009:创建统一文件后端模块 | Bob (Scrum Master) |
 | 1.15 | 2026-01-04 | 完成故事010.009:创建统一文件后端模块(22个测试,覆盖率59.47%) | Claude (Dev Agent) |
+| 1.16 | 2026-01-04 | 批准故事010.010:创建统一文件管理UI包 | Bob (Scrum Master) |
 
 ## 史诗目标
 
@@ -376,7 +377,7 @@ packages/unified-file-module/
     └── integration/ (8个测试)
 ```
 
-### Story 10: 创建统一文件管理UI包 📝 待开始
+### Story 10: 创建统一文件管理UI包 ✅ 已批准
 
 **标题**: 创建统一文件管理UI包 (unified-file-management-ui)
 

+ 372 - 0
docs/stories/010.010.story.md

@@ -0,0 +1,372 @@
+# Story 010.010: 创建统一文件管理UI包 (unified-file-management-ui)
+
+## Status
+Approved
+
+## Story
+
+**As a** 开发者,
+**I want** 从单租户文件管理UI复制并改造创建统一文件管理UI包(unified-file-management-ui),
+**so that** 统一广告管理UI可以使用无租户隔离的文件选择器组件,确保架构一致性。
+
+## Acceptance Criteria
+
+1. 创建 `packages/unified-file-management-ui` 包(**使用 `cp -r` 命令直接复制 `packages/file-management-ui` 整个文件夹**)
+2. 修改包名和模块引用(从 `@d8d/file-management-ui` 改为 `@d8d/unified-file-management-ui`,从 `@d8d/file-module` 改为 `@d8d/unified-file-module`)
+3. 修改 API 客户端指向统一文件模块的管理员路由(`/api/v1/admin/unified-files`)
+4. 实现文件管理组件(列表、上传、删除)
+5. 实现文件选择器组件(供统一广告管理UI等其他UI包使用)
+6. 编写完整的组件测试和集成测试
+7. 测试覆盖率达到70%以上
+
+## Tasks / Subtasks
+
+- [ ] **任务1: 复制文件管理UI包创建统一文件管理UI包** (AC: 1)
+  - [ ] **使用 `cp -r packages/file-management-ui packages/unified-file-management-ui` 命令直接复制整个文件夹**
+  - [ ] 验证复制后的目录结构完整
+  - [ ] 验证所有文件都已复制(src、tests、配置文件)
+
+- [ ] **任务2: 修改包配置文件** (AC: 2)
+  - [ ] 修改 `package.json` 包名:`@d8d/file-management-ui` → `@d8d/unified-file-management-ui`
+  - [ ] 修改 `package.json` 描述:添加"unified"相关描述
+  - [ ] 修改依赖:`@d8d/file-module` → `@d8d/unified-file-module`
+
+- [ ] **任务3: 修改 API 客户端** (AC: 2, 3)
+  - [ ] 修改 `src/api/fileClient.ts` 文件名为 `src/api/unifiedFileClient.ts`
+  - [ ] 修改 `src/api/fileClient.ts` 中的路由导入:`fileRoutes` → `unifiedFileRoutes`
+  - [ ] 修改 `src/api/index.ts` 导出
+  - [ ] 确认 API 端点指向统一文件模块的管理员路由
+
+- [ ] **任务4: 修改类型定义** (AC: 2)
+  - [ ] 修改类型文件中的命名和引用
+  - [ ] 确保类型推断使用 RPC 推断类型(而非直接导入 schema 类型)
+
+- [ ] **任务5: 修改组件** (AC: 4, 5)
+  - [ ] 更新组件中的 API 客户端导入
+  - [ ] 更新 hooks 中的 API 客户端导入
+  - [ ] 验证文件管理组件功能正常
+  - [ ] 验证文件选择器组件功能正常
+
+- [ ] **任务6: 编写组件测试** (AC: 6)
+  - [ ] 更新测试文件中的 mock 路由和 API
+  - [ ] 添加文件管理组件集成测试
+  - [ ] 添加文件选择器组件集成测试
+  - [ ] 验证所有测试通过
+
+- [ ] **任务7: 类型检查和代码质量** (AC: 7)
+  - [ ] 运行 `pnpm typecheck` 确保无TypeScript类型错误
+  - [ ] 运行 `pnpm test` 确保所有测试通过
+  - [ ] 运行 `pnpm test:coverage` 确保测试覆盖率达到70%以上
+
+## Dev Notes
+
+### 前一故事关键要点(来自 010.009)
+
+**史诗010的统一设计模式**:
+- **统一文件模块只在超级管理员后台使用,所有路由都使用 `tenantAuthMiddleware`**
+- **API路由路径**: `/api/v1/admin/unified-files`(管理员路由)
+- **不需要用户展示路由**(与统一广告模块不同,统一文件模块没有用户展示路由)
+- 统一模块的Entity没有 `tenant_id` 字段
+- API路由路径使用相对路径(不包含 `/api/v1` 前缀)
+
+**路由规范** [Source: docs/prd/epic-010-unified-ad-management.md, docs/architecture/backend-module-package-standards.md]:
+- 路由路径必须使用相对路径(如 `/` 和 `/:id`),不包含 `/api/v1` 前缀
+- 路由 `request.params` 必须明确定义,使用 `z.coerce.number<number>()` 进行类型转换
+
+### 当前问题说明
+
+**架构不一致性** [Source: docs/prd/epic-010-unified-ad-management.md]:
+- 统一广告管理UI (`unified-advertisement-management-ui`) 当前使用 `@d8d/file-management-ui-mt` 的 `FileSelector` 组件(多租户)
+- `FileSelector` 组件关联的是多租户文件模块(有 `tenant_id` 字段)
+- 统一广告管理UI本身是无租户隔离的,但使用的文件选择器却是多租户版本
+- 这造成了架构不一致性
+
+### 源UI包:单租户文件管理UI (file-management-ui)
+
+**包结构** [Source: packages/file-management-ui/]:
+```
+packages/file-management-ui/
+├── package.json
+├── tsconfig.json
+├── vitest.config.ts
+├── build.config.ts
+├── src/
+│   ├── index.ts
+│   ├── api/
+│   │   ├── fileClient.ts
+│   │   └── index.ts
+│   ├── components/
+│   │   ├── FileManagement.tsx
+│   │   ├── FileSelector.tsx
+│   │   ├── MinioUploader.tsx
+│   │   └── index.ts
+│   ├── hooks/
+│   │   ├── useFileManagement.ts
+│   │   ├── useFileSelector.ts
+│   │   └── index.ts
+│   ├── types/
+│   │   └── file.ts
+│   └── utils/
+│       ├── cn.ts
+│       ├── minio.ts
+│       └── index.ts
+└── tests/
+    ├── setup.ts
+    ├── components/
+    │   ├── FileManagement.test.tsx
+    │   └── FileSelector.test.tsx
+    ├── hooks/
+    │   └── useFileManagement.test.tsx
+    └── utils/
+        └── index.test.ts
+```
+
+**包名**: `@d8d/file-management-ui` [Source: packages/file-management-ui/package.json]
+
+**依赖** [Source: packages/file-management-ui/package.json]:
+```json
+{
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@d8d/file-module": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.9",
+    "axios": "^1.7.9",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "date-fns": "^4.1.0",
+    "dayjs": "^1.11.13",
+    "hono": "^4.8.5",
+    "lucide-react": "^0.536.0",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "react-hook-form": "^7.61.1",
+    "react-router": "^7.1.3",
+    "sonner": "^2.0.7",
+    "tailwind-merge": "^3.3.1",
+    "zod": "^4.0.15"
+  }
+}
+```
+
+**API 客户端模式** [Source: packages/file-management-ui/src/api/fileClient.ts]:
+```typescript
+// RPC 客户端管理器模式(参考)
+import { fileRoutes } from '@d8d/file-module';
+import { rpcClient } from '@d8d/shared-ui-components/utils/hc';
+
+export class FileClientManager {
+  private static instance: FileClientManager;
+  private client: ReturnType<typeof rpcClient<typeof fileRoutes>> | null = null;
+
+  public static getInstance(): FileClientManager {
+    if (!FileClientManager.instance) {
+      FileClientManager.instance = new FileClientManager();
+    }
+    return FileClientManager.instance;
+  }
+
+  public init(baseUrl: string = '/'): ReturnType<typeof rpcClient<typeof fileRoutes>> {
+    return this.client = rpcClient<typeof fileRoutes>(baseUrl);
+  }
+
+  public get(): ReturnType<typeof rpcClient<typeof fileRoutes>> {
+    if (!this.client) {
+      return this.init()
+    }
+    return this.client;
+  }
+
+  public reset(): void {
+    this.client = null;
+  }
+}
+
+const fileClientManager = FileClientManager.getInstance();
+export const fileClient = fileClientManager.get();
+export { FileClientManager, fileClientManager };
+```
+
+**⚠️ 依赖变更说明**:
+- **修改**: `@d8d/file-module` → `@d8d/unified-file-module`
+- **修改**: 路由导入 `fileRoutes` → `unifiedFileRoutes`
+- **修改**: API 端点指向统一文件模块的管理员路由
+
+### 关键实施要点
+
+**⚠️ 重要:使用 CP 命令直接复制**
+
+根据用户明确要求,必须使用以下命令直接复制整个文件管理UI文件夹:
+
+```bash
+cp -r packages/file-management-ui packages/unified-file-management-ui
+```
+
+**为什么使用 cp 命令直接复制**:
+1. **保证完整性**: 复制整个文件夹确保所有文件(包括测试、配置、源码)都被包含
+2. **减少错误**: 避免手动创建文件时遗漏某些文件或配置
+3. **保持一致性**: 确保新UI包的结构与原UI包完全一致
+4. **提高效率**: 一次性复制后只需修改必要的部分,而不是从头创建
+
+**复制后的修改清单**:
+
+1. **包名修改**: `@d8d/file-management-ui` → `@d8d/unified-file-management-ui`
+2. **依赖修改**: `@d8d/file-module` → `@d8d/unified-file-module`
+3. **API 客户端修改**:
+   - 文件名:`fileClient.ts` → `unifiedFileClient.ts`
+   - 路由导入:`fileRoutes` → `unifiedFileRoutes`
+   - 管理器名:`FileClientManager` → `UnifiedFileClientManager`
+   - 客户端名:`fileClient` → `unifiedFileClient`
+4. **类型定义修改**: 使用 RPC 推断类型(而非直接导入 schema 类型)
+5. **组件修改**: 更新 API 客户端导入
+6. **Hooks 修改**: 更新 API 客户端导入
+7. **测试修改**: 更新 mock 路由和 API
+
+### UI包开发规范
+
+**包结构规范** [Source: docs/architecture/ui-package-standards.md]:
+```
+packages/<module-name>-ui/
+├── package.json                    # 包配置
+├── tsconfig.json                   # TypeScript配置
+├── vite.config.ts                  # Vite构建配置
+├── src/
+│   ├── index.ts                    # 主入口文件
+│   ├── components/                 # React组件
+│   ├── api/                        # RPC客户端管理器
+│   ├── hooks/                      # 自定义Hooks
+│   ├── types/                      # TypeScript类型定义
+│   └── utils/                      # 工具函数
+└── tests/                         # 测试文件
+    └── integration/                # 集成测试
+```
+
+**RPC客户端实现规范** [Source: docs/architecture/ui-package-standards.md]:
+- 必须实现 ClientManager 类来管理 RPC 客户端生命周期
+- 使用单例模式确保客户端只初始化一次
+- 提供初始化、获取、重置方法
+
+**类型推断最佳实践** [Source: docs/architecture/coding-standards.md]:
+- **必须使用 RPC 推断类型**,而不是直接导入 schema 类型
+- 避免使用 Date/string 类型不匹配的问题
+- 参考现有UI包(如广告管理UI)的类型定义模式
+
+**测试规范** [Source: docs/architecture/ui-package-standards.md]:
+- 使用 Vitest 进行测试
+- 使用 `@testing-library/react` 进行组件测试
+- Mock RPC 客户端和依赖
+- 测试覆盖率要求达到70%以上
+
+### 项目位置
+
+**新UI包位置**: `packages/unified-file-management-ui/` [Source: docs/architecture/source-tree.md]
+
+**测试标准**: Vitest [Source: docs/architecture/tech-stack.md]
+
+### Testing
+
+**测试文件位置** [Source: docs/architecture/ui-package-standards.md]:
+- 组件测试: `packages/unified-file-management-ui/tests/components/`
+- Hooks测试: `packages/unified-file-management-ui/tests/hooks/`
+- 工具测试: `packages/unified-file-management-ui/tests/utils/`
+
+**测试框架**:
+- Vitest [Source: docs/architecture/tech-stack.md]
+- @testing-library/react [Source: packages/file-management-ui/package.json]
+
+**测试配置** [Source: packages/file-management-ui/vitest.config.ts]:
+```typescript
+// vitest.config.ts
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+    setupFiles: ['./tests/setup.ts']
+  }
+});
+```
+
+**测试覆盖要求**:
+- 测试覆盖率达到70%以上
+- 组件测试:覆盖主要交互场景
+- 集成测试:覆盖 API 调用和状态管理
+- Hooks 测试:覆盖自定义 hooks 功能
+
+**测试命令**:
+```bash
+# 进入UI包目录
+cd packages/unified-file-management-ui
+
+# 运行所有测试
+pnpm test
+
+# 运行测试并监听变化
+pnpm test:watch
+
+# 生成覆盖率报告
+pnpm test:coverage
+
+# 类型检查
+pnpm typecheck
+```
+
+**测试 Setup 配置** [Source: docs/architecture/ui-package-standards.md]:
+```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();
+
+// Mock pointer events for Radix UI Select component
+Element.prototype.hasPointerCapture = vi.fn(() => true) as any;
+Element.prototype.releasePointerCapture = vi.fn() as any;
+Element.prototype.setPointerCapture = vi.fn() as any;
+
+// Mock ResizeObserver (必须使用 class 模式)
+global.ResizeObserver = class MockResizeObserver {
+  constructor(callback: ResizeObserverCallback) {
+    (this as any).callback = callback;
+  }
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+};
+```
+
+## Change Log
+
+| Date | Version | Description | Author |
+|------|---------|-------------|--------|
+| 2026-01-04 | 1.1 | 批准故事 | Bob (Scrum Master) |
+| 2026-01-04 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+
+## Dev Agent Record
+
+### Agent Model Used
+_待开发代理填写_
+
+### Debug Log References
+_待开发代理填写_
+
+### Completion Notes List
+_待开发代理填写_
+
+### File List
+_待开发代理填写_
+
+## QA Results
+_QA代理待填写_