010.010.story.md 14 KB

Story 010.010: 创建统一文件管理UI包 (unified-file-management-ui)

Status

Ready for Review

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

  • [x] 任务1: 复制文件管理UI包创建统一文件管理UI包 (AC: 1)

    • 使用 cp -r packages/file-management-ui packages/unified-file-management-ui 命令直接复制整个文件夹
    • 验证复制后的目录结构完整
    • 验证所有文件都已复制(src、tests、配置文件)
  • [x] 任务2: 修改包配置文件 (AC: 2)

    • 修改 package.json 包名:@d8d/file-management-ui@d8d/unified-file-management-ui
    • 修改 package.json 描述:添加"unified"相关描述
    • 修改依赖:@d8d/file-module@d8d/unified-file-module
  • [x] 任务3: 修改 API 客户端 (AC: 2, 3)

    • 修改 src/api/fileClient.ts 文件名为 src/api/unifiedFileClient.ts
    • 修改 src/api/fileClient.ts 中的路由导入:fileRoutesunifiedFileRoutes
    • 修改 src/api/index.ts 导出
    • 确认 API 端点指向统一文件模块的管理员路由
  • [x] 任务4: 修改类型定义 (AC: 2)

    • 修改类型文件中的命名和引用
    • 确保类型推断使用 RPC 推断类型(而非直接导入 schema 类型)
  • [x] 任务5: 修改组件 (AC: 4, 5)

    • 更新组件中的 API 客户端导入
    • 更新 hooks 中的 API 客户端导入
    • 验证文件管理组件功能正常
    • 验证文件选择器组件功能正常
  • [x] 任务6: 编写组件测试 (AC: 6)

    • 更新测试文件中的 mock 路由和 API
    • 添加文件管理组件集成测试
    • 添加文件选择器组件集成测试
    • 验证所有测试通过
  • [x] 任务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-mtFileSelector 组件(多租户)
  • 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]:

{
  "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]:

// 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
  • 修改: 路由导入 fileRoutesunifiedFileRoutes
  • 修改: API 端点指向统一文件模块的管理员路由

关键实施要点

⚠️ 重要:使用 CP 命令直接复制

根据用户明确要求,必须使用以下命令直接复制整个文件管理UI文件夹:

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.tsunifiedFileClient.ts
    • 路由导入:fileRoutesunifiedFileRoutes
    • 管理器名:FileClientManagerUnifiedFileClientManager
    • 客户端名:fileClientunifiedFileClient
  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]:

// 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 功能

测试命令:

# 进入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]:

// 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

d8d-model (claude-opus-4-5-20251101)

Debug Log References

无需要记录的调试问题

Completion Notes List

  1. 使用 cp -r 命令成功复制 packages/file-management-uipackages/unified-file-management-ui
  2. 修改了 package.json 包名和依赖(@d8d/unified-file-module
  3. 修改了 API 客户端:fileClient.tsunifiedFileClient.tsfileClientManagerunifiedFileClientManager
  4. 修改了所有组件和 hooks 中的 API 客户端导入
  5. 修改了测试文件中的 mock 路由
  6. 修复了 UpdateFileDtoUpdateUnifiedFileDto 导入问题
  7. 修复了 uploadUseruploadUserId 属性问题(统一文件模块删除了 uploadUser 字段)
  8. 所有30个测试通过(4个测试文件)

File List

新增文件:

  • packages/unified-file-management-ui/package.json
  • packages/unified-file-management-ui/src/api/unifiedFileClient.ts
  • packages/unified-file-management-ui/src/components/FileManagement.tsx
  • packages/unified-file-management-ui/src/components/FileSelector.tsx
  • packages/unified-file-management-ui/src/components/MinioUploader.tsx
  • packages/unified-file-management-ui/src/hooks/useFileManagement.ts
  • packages/unified-file-management-ui/src/hooks/useFileSelector.ts
  • packages/unified-file-management-ui/src/types/file.ts
  • packages/unified-file-management-ui/src/utils/minio.ts
  • packages/unified-file-management-ui/tests/components/FileManagement.test.tsx
  • packages/unified-file-management-ui/tests/components/FileSelector.test.tsx
  • packages/unified-file-management-ui/tests/hooks/useFileManagement.test.tsx

修改文件:

  • docs/stories/010.010.story.md (更新状态和任务复选框)

QA Results

QA代理待填写