Approved
As a 超级管理员, I want 一个统一广告管理的UI包, so that 可以在租户管理后台统一管理所有广告和广告类型。
packages/unified-advertisement-management-ui 包,包含完整的组件、API客户端、类型定义@d8d/unified-advertisements-module 的管理员路由/api/v1/admin/unified-advertisements 管理员端点[ ] 任务1: 创建包结构和配置文件 (AC: 1)
packages/unified-advertisement-management-ui 目录package.json,配置包名为 @d8d/unified-advertisement-management-uitsconfig.jsonvitest.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.tsUnifiedAdvertisementResponse, CreateUnifiedAdvertisementRequest, UpdateUnifiedAdvertisementRequest[ ] 任务4: 实现广告管理组件 (AC: 2, 6)
src/components/UnifiedAdvertisementManagement.tsxdata-testid 属性@d8d/shared-ui-components 中的 shadcn/ui 组件src/components/index.ts 导出组件[ ] 任务5: 实现广告类型管理组件 (AC: 3, 6)
src/components/UnifiedAdvertisementTypeManagement.tsxdata-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 确保所有测试通过新包位置:
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/ # 组件测试
参考模块:
packages/advertisement-management-ui-mtpackages/advertisement-type-management-ui-mt@d8d/unified-advertisements-module管理员接口 (使用 tenantAuthMiddleware):
// 广告管理端点
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 # 删除广告类型
[Source: docs/architecture/ui-package-standards.md#RPC客户端实现规范]
// 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#类型推断最佳实践]
// 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 组件
// ✅ 正确:条件渲染两个独立的Form组件
{isCreateForm ? (
<Form {...createForm}>
{/* 创建表单内容 */}
</Form>
) : (
<Form {...updateForm}>
{/* 编辑表单内容 */}
</Form>
)}
// ❌ 错误:在单个Form组件上动态切换props(可能导致类型不兼容)
<Form {...(isCreateForm ? createForm : updateForm)}>
{/* 表单内容 */}
</Form>
测试选择器优化: 为关键交互元素添加 data-testid 属性
[Source: docs/architecture/ui-package-standards.md#测试选择器优化规范]
// 在组件中添加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组件测试环境修复]
// 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();
[Source: docs/architecture/ui-package-standards.md#package.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 学到的关键点:
tenantAuthMiddleware,只有超级管理员(ID=1)可访问@ManyToOne 关联 FileMt 和 UnifiedAdvertisementTypeadvertisements-module-mt 完全一致code 字段唯一索引本故事需要注意的点:
/api/v1/admin/unified-advertisements),不是用户路由tests/integration/tests/components/tests/setup.ts[Source: docs/architecture/testing-strategy.md]
| 测试类型 | 最低要求 | 目标要求 |
|---|---|---|
| 组件测试 | 70% | 80% |
| 集成测试 | 50% | 60% |
# 进入UI包目录
cd packages/unified-advertisement-management-ui
# 运行所有测试
pnpm test
# 生成覆盖率报告
pnpm test:coverage
# 类型检查
pnpm typecheck
| Date | Version | Description | Author |
|---|---|---|---|
| 2026-01-03 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
待开发代理填写
待开发代理填写
待开发代理填写
待开发代理填写
待QA代理填写