|
|
@@ -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代理待填写_
|