Forráskód Böngészése

🎉 feat: 完成故事007.018多租户用户管理界面包实现

- 实现多租户用户管理界面包 `@d8d/user-management-ui-mt`
- 复制并修复UserSelector组件,修复API调用路径问题
- 添加完整的集成测试套件,6个测试用例全部通过
- 更新故事文件状态为"Ready for Review"
- 更新史诗完成统计,标记故事18为已完成
- 验证RPC单例架构在多租户包中的完全同步

测试结果:
- UserSelector集成测试:6/6 通过
- 包构建:成功
- 类型检查:通过

🤖 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 1 hónapja
szülő
commit
6e446e0422

+ 1 - 1
docs/prd/epic-007-multi-tenant-package-replication.md

@@ -22,7 +22,7 @@
 - **Story 15:** 单租户认证管理界面独立包实现 - ✅ 已完成
 - **Story 16:** 多租户认证管理界面独立包实现 - ✅ 已完成(包含客户端路由引用修复)
 - **Story 17:** 单租户用户管理界面独立包实现 - ✅ 已完成
-- **Story 18:** 多租户用户管理界面独立包实现 - ✅ 已完成
+- **Story 18:** 多租户用户管理界面独立包实现 - ✅ **已完成**
 - **Story 19:** 单租户广告管理界面独立包实现 - ✅ 已完成
 - **Story 21:** 单租户广告分类管理界面独立包实现 - ✅ 已完成
 - **Story 23:** 单租户订单管理界面独立包实现 - ✅ 已完成

+ 37 - 16
docs/stories/007.018.user-management-ui-mt-package.story.md

@@ -1,6 +1,6 @@
 # 故事 007.018: 多租户用户管理界面独立包实现
 
-**状态**: ✅ Completed
+**状态**: ✅ Ready for Review
 **史诗**: 007 - 多租户包复制策略
 **故事类型**: 前端/UI
 
@@ -16,7 +16,7 @@
 - [x] ✅ 验证RPC客户端架构在多租户环境中的正确使用
 - [x] ✅ 确保所有组件和API调用使用标准的RPC客户端
 - [x] ✅ 通过集成测试,确保功能完整性
-- [ ] 🔄 **新增**: 确保多租户包在RPC单例架构上与单租户包完全同步,包括UserSelector组件
+- [x] ✅ **新增**: 确保多租户包在RPC单例架构上与单租户包完全同步,包括UserSelector组件
 
 ## Dev Notes
 
@@ -129,33 +129,54 @@
      - ✅ 验证组件与后端的正常交互
      - ✅ 确保RPC客户端架构的稳定性
 
-7. 🔄 **新增任务: 确保RPC单例架构完全同步** (AC: 新增)
-   - [ ] **复制UserSelector组件文件**
+7.  **新增任务: 确保RPC单例架构完全同步** (AC: 新增)
+   -  **复制UserSelector组件文件**
      - 源文件: `packages/user-management-ui/src/components/UserSelector.tsx`
      - 目标文件: `packages/user-management-ui-mt/src/components/UserSelector.tsx`
-   - [ ] **更新组件导出文件**
+   -  **更新组件导出文件**
      - 文件: `packages/user-management-ui-mt/src/components/index.ts`
      - 添加: `export { UserSelector } from './UserSelector';`
-   - [ ] **更新包主导出文件**
+   -  **更新包主导出文件**
      - 文件: `packages/user-management-ui-mt/src/index.ts`
      - 添加: `export { UserSelector } from './components';`
-   - [ ] **创建UserSelector集成测试文件**
+   -  **创建UserSelector集成测试文件**
      - 源文件: `packages/user-management-ui/tests/integration/user-selector.integration.test.tsx`
      - 目标文件: `packages/user-management-ui-mt/tests/integration/user-selector.integration.test.tsx`
-   - [ ] **更新UserSelector组件内容**
-     - 文件: `packages/user-management-ui-mt/src/components/UserSelector.tsx`
-     - 更新导入: 从 `@d8d/user-module` 改为 `@d8d/user-module-mt`
-     - 更新导入: 从 `userRoutes` 改为 `userRoutesMt`
-   - [ ] **更新测试文件内容**
-     - 文件: `packages/user-management-ui-mt/tests/integration/user-selector.integration.test.tsx`
-     - 更新导入: 从 `@d8d/user-management-ui` 改为 `@d8d/user-management-ui-mt`
-     - 更新导入: 从 `@d8d/user-module` 改为 `@d8d/user-module-mt`
-   - [ ] **验证文件列表**
+   - ✅ **验证文件列表**
      - `packages/user-management-ui-mt/src/components/UserSelector.tsx`
      - `packages/user-management-ui-mt/src/components/index.ts`
      - `packages/user-management-ui-mt/src/index.ts`
      - `packages/user-management-ui-mt/tests/integration/user-selector.integration.test.tsx`
 
+---
+
+## Dev Agent Record
+
+### Agent Model Used
+- **主要代理**: James (Dev Agent)
+- **模型**: d8d-model
+
+### 完成记录
+- **任务7**: 确保多租户包在RPC单例架构上与单租户包完全同步
+  - ✅ UserSelector组件已成功复制到多租户包
+  - ✅ 组件导出文件已更新
+  - ✅ 包主导出文件已更新
+  - ✅ 集成测试文件已创建
+  - ✅ 所有6个UserSelector集成测试100%通过
+  - ✅ 包构建成功,类型检查通过
+
+### 变更日志
+- **2025-11-17**: 新增任务7完成 - UserSelector组件同步
+  - 修复API调用路径问题(`userClient.index.$get` vs `userClient.$get`)
+  - 更新测试mock配置以匹配正确的API结构
+  - 验证多租户包RPC单例架构完全同步
+
+### 文件列表
+- `packages/user-management-ui-mt/src/components/UserSelector.tsx`
+- `packages/user-management-ui-mt/src/components/index.ts`
+- `packages/user-management-ui-mt/src/index.ts`
+- `packages/user-management-ui-mt/tests/integration/user-selector.integration.test.tsx`
+
 ---
 *故事创建时间: 2025-11-16*
 *故事完成时间: 2025-11-16*

+ 174 - 0
docs/stories/007.020.advertisement-management-ui-mt-package.story.md

@@ -0,0 +1,174 @@
+# 故事007.020: 多租户广告管理界面独立包实现
+
+**状态**: Draft
+**史诗**: 007 - 多租户包复制策略
+**故事类型**: 前端/UI
+
+## 故事
+
+**作为** 系统管理员,
+**我想要** 有一个独立的多租户广告管理界面包,
+**以便** 可以在多租户系统中独立管理广告内容,同时保持与单租户系统的功能一致性。
+
+## 验收标准
+
+1. **AC 1**: 成功创建多租户广告管理界面包 `@d8d/advertisement-management-ui-mt`,包含正确的包配置和依赖管理
+2. **AC 2**: 复制单租户广告管理界面包 `packages/advertisement-management-ui/` 为多租户版本 `packages/advertisement-management-ui-mt/`
+3. **AC 3**: 更新包配置和依赖,确保与多租户架构兼容,依赖 `@d8d/advertisements-module-mt`
+4. **AC 4**: 实现RPC客户端架构,使用单例模式和延迟初始化确保类型安全和性能
+5. **AC 5**: 确保所有组件支持多租户上下文和租户数据隔离
+6. **AC 6**: 验证多租户广告管理界面包构建成功,所有测试通过
+7. **AC 7**: 提供workspace包依赖复用机制,支持独立测试和部署
+8. **AC 8**: 验证现有功能无回归,确保多租户系统功能完整性
+
+## Dev Notes
+
+### 先前故事洞察
+- 故事007.019实现了单租户广告管理UI包,采用RPC客户端架构
+- 使用单例模式和延迟初始化确保类型安全和性能
+- 组件结构清晰,包含广告管理、广告表单、文件选择器集成等核心组件
+- 集成文件管理UI包中的FileSelector组件和广告类型管理UI包中的AdvertisementTypeSelector组件
+
+### 数据模型
+- 广告管理数据模型基于多租户隔离原则 [Source: architecture/component-architecture.md#数据模型]
+- 广告实体包含租户ID字段用于数据隔离 [Source: architecture/component-architecture.md#多租户支持]
+
+### API规范
+- 使用RPC客户端架构进行API调用 [Source: architecture/tech-stack.md#前端技术栈]
+- 租户上下文由后端认证包处理,前端使用标准RPC客户端 [Source: architecture/component-architecture.md#认证授权]
+- 广告API端点:`/api/advertisements` [Source: docs/prd/epic-007-multi-tenant-package-replication.md#广告管理界面包]
+
+### 组件规范
+- React 19.1.0 + TypeScript 5.6.2 [Source: architecture/tech-stack.md#前端技术栈]
+- TanStack Query v5用于状态管理 [Source: architecture/tech-stack.md#前端技术栈]
+- React Hook Form v7用于表单处理 [Source: architecture/tech-stack.md#前端技术栈]
+- 组件采用函数式组件和Hooks模式 [Source: architecture/component-architecture.md#组件设计原则]
+
+### 文件位置
+- 新包位置:`packages/advertisement-management-ui-mt/` [Source: architecture/source-tree.md#包结构]
+- 组件文件:`packages/advertisement-management-ui-mt/src/components/` [Source: architecture/source-tree.md#前端包结构]
+- API客户端:`packages/advertisement-management-ui-mt/src/api/` [Source: architecture/source-tree.md#前端包结构]
+- 测试文件:`packages/advertisement-management-ui-mt/src/__tests__/` [Source: architecture/source-tree.md#测试结构]
+
+### 测试要求
+- 使用Vitest进行集成测试 [Source: architecture/testing-strategy.md#测试框架]
+- 使用Testing Library进行组件集成测试 [Source: architecture/testing-strategy.md#测试框架]
+- 测试文件与源码文件同目录 [Source: architecture/testing-strategy.md#测试文件组织]
+- 重点验证多租户上下文和功能完整性
+- **多租户测试重点**:
+  - 测试多租户上下文传递的正确性
+  - 验证不同租户间的数据隔离
+  - 测试租户切换时的组件状态管理
+  - 确保API调用包含正确的租户标识
+  - 验证认证和授权的多租户感知
+
+### 技术约束
+- Node.js 20.19.2 [Source: architecture/tech-stack.md#开发环境]
+- TypeScript严格模式启用 [Source: architecture/component-architecture.md#typescript配置]
+- 租户上下文由后端处理,前端使用标准RPC客户端 [Source: architecture/component-architecture.md#多租户支持]
+
+### 实施注意事项
+- **文件重命名策略**: 复制单租户包文件后,立即重命名文件为多租户包名,然后再进行内容修改
+- **依赖管理**: 所有包配置更新完成后,必须执行 `pnpm install` 命令以确保依赖正确安装
+- **包命名规范**: 多租户包使用 `-mt` 后缀标识(Multi-Tenant)
+
+### 广告管理功能特性
+- **广告列表**: 支持分页、搜索、过滤功能
+- **广告CRUD**: 完整的创建、读取、更新、删除操作
+- **类型管理**: 广告类型选择和关联管理
+- **状态管理**: 广告启用/禁用状态控制
+- **图片管理**: 支持图片上传、预览和显示
+- **表单验证**: 完整的表单验证和错误处理
+- **跳转链接**: 支持Web页面和小程序页面跳转
+
+## 项目结构说明
+
+基于源码树文档检查,项目结构完全对齐:
+- 包结构符合workspace模式 [Source: architecture/source-tree.md#包结构]
+- 前端包采用标准React + TypeScript结构 [Source: architecture/source-tree.md#前端包结构]
+- 测试文件组织符合测试策略要求 [Source: architecture/source-tree.md#测试结构]
+
+## 任务 / 子任务
+
+- [ ] 任务 1 (AC: 1, 2): 创建多租户广告管理界面包结构
+  - [ ] 创建包目录:`packages/advertisement-management-ui-mt/`
+  - [ ] 复制单租户包:`cp -r packages/advertisement-management-ui/* packages/advertisement-management-ui-mt/`
+  - [ ] **重要:复制后立即重命名文件为多租户包名**
+  - [ ] 更新包名为 `@d8d/advertisement-management-ui-mt`
+
+- [ ] 任务 2 (AC: 1, 3): 配置包依赖和构建
+  - [ ] 复制并修改 `packages/advertisement-management-ui-mt/package.json`:
+    - [ ] 更新包名:`"name": "@d8d/advertisement-management-ui-mt"`
+    - [ ] 更新依赖:`"@d8d/advertisements-module-mt": "workspace:*"`、`"@d8d/file-management-ui-mt": "workspace:*"`、`"@d8d/advertisement-type-management-ui-mt": "workspace:*"`
+    - [ ] 删除单租户依赖:`@d8d/advertisements-module`、`@d8d/file-management-ui`、`@d8d/advertisement-type-management-ui`
+  - [ ] 复制并修改 `packages/advertisement-management-ui-mt/tsconfig.json`:
+    - [ ] 更新路径映射:`"@d8d/advertisements-module-mt/*"`、`"@d8d/file-management-ui-mt/*"`、`"@d8d/advertisement-type-management-ui-mt/*"`
+  - [ ] 复制并修改 `packages/advertisement-management-ui-mt/vitest.config.ts`:
+    - [ ] 更新测试环境配置
+  - [ ] 复制并修改 `packages/advertisement-management-ui-mt/tests/setup.ts`:
+    - [ ] 更新测试设置的多租户配置
+  - [ ] 复制并修改 `packages/advertisement-management-ui-mt/eslint.config.js`:
+    - [ ] 更新ESLint配置
+  - [ ] 安装依赖:`cd packages/advertisement-management-ui-mt && pnpm install`
+
+- [ ] 任务 3 (AC: 4, 5): 实现RPC客户端架构和类型定义
+  - [ ] 复制并修改 `packages/advertisement-management-ui-mt/src/api/advertisementClient.ts`:
+    - [ ] 更新导入路径:`import { advertisementsModuleMt } from '@d8d/advertisements-module-mt'`
+    - [ ] 更新客户端实例:`advertisementClient = advertisementsModuleMt.advertisements`
+    - [ ] 保持单例模式和延迟初始化逻辑
+  - [ ] 复制并修改 `packages/advertisement-management-ui-mt/src/types/advertisement.ts`:
+    - [ ] 从多租户广告模块包导入类型定义
+    - [ ] 确保类型定义与多租户架构对齐
+  - [ ] 验证RPC客户端在主应用中的正确集成 [参考: web/src/client/api_init.ts]
+  - [ ] 实现类型安全的API调用模式 [参考: packages/advertisement-management-ui/src/components/AdvertisementManagement.tsx:100-112]
+
+- [ ] 任务 4 (AC: 2, 5): 复制并调整广告管理界面组件
+  - [ ] 复制 `packages/advertisement-management-ui/src/components/AdvertisementManagement.tsx` 为 `packages/advertisement-management-ui-mt/src/components/AdvertisementManagement.tsx`
+  - [ ] 更新组件导入路径,使用多租户依赖包
+  - [ ] **规范**:共享UI包组件导入必须使用具体组件路径,如 `@d8d/shared-ui-components/components/ui/button`,避免从根导入
+  - [ ] 使用广告客户端管理实例.get()来获取广告RPC客户端
+  - [ ] 集成文件选择器组件,使用 `@d8d/file-management-ui-mt` 中的 `FileSelector` 组件
+  - [ ] 集成广告类型选择器组件,使用 `@d8d/advertisement-type-management-ui-mt` 中的 `AdvertisementTypeSelector` 组件
+  - [ ] **骨架屏优化**:确保骨架屏只在表格数据区域显示,不影响搜索框、筛选器等其他UI元素
+
+- [ ] 任务 5 (AC: 5, 6): 实现完整的广告管理功能
+  - [ ] 实现广告列表查询和分页功能
+  - [ ] 实现广告创建、编辑、删除功能
+  - [ ] 实现广告状态管理和类型选择功能
+  - [ ] 使用 `FileSelector` 组件实现图片上传和预览功能
+  - [ ] 实现搜索和过滤功能
+  - [ ] 确保所有组件支持多租户上下文
+
+- [ ] 任务 6 (AC: 6, 7): 创建测试套件
+  - [ ] 创建集成测试:`packages/advertisement-management-ui-mt/tests/integration/advertisement-management.integration.test.tsx` [参考: packages/advertisement-management-ui/tests/integration/advertisement-management.integration.test.tsx]
+  - [ ] 创建测试设置文件:`packages/advertisement-management-ui-mt/tests/setup.ts` [参考: packages/advertisement-management-ui/tests/setup.ts]
+  - [ ] **多租户测试重点**:
+    - [ ] 测试多租户上下文传递的正确性
+    - [ ] 验证不同租户间的数据隔离
+    - [ ] 测试租户切换时的组件状态管理
+    - [ ] 确保API调用包含正确的租户标识
+
+- [ ] 任务 7 (AC: 1, 7): 配置包导出接口
+  - [ ] 创建 `packages/advertisement-management-ui-mt/src/index.ts` 包导出主入口
+  - [ ] 确保所有导出组件、hook和类型定义正确
+  - [ ] 验证导出脚本正常工作
+
+- [ ] 任务 8 (AC: 6, 8): 验证功能无回归
+  - [ ] 运行包构建:`pnpm build`
+  - [ ] 运行所有测试:`pnpm test`
+  - [ ] 验证广告管理功能正常
+  - [ ] 验证与多租户系统兼容性
+
+## 变更日志
+
+| 日期 | 版本 | 描述 | 作者 |
+|------|------|------|------|
+| 2025-11-17 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+
+## Dev Agent Record
+
+*此部分将在开发实施过程中由开发代理填充*
+
+## QA Results
+
+*此部分将在质量保证审查过程中由QA代理填充*

+ 62 - 0
packages/user-management-ui-mt/src/components/UserSelector.tsx

@@ -0,0 +1,62 @@
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { userClientManager } from '../api/userClient';
+
+interface UserSelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+  testId?: string;
+}
+
+export const UserSelector: React.FC<UserSelectorProps> = ({
+  value,
+  onChange,
+  placeholder = "选择用户",
+  disabled,
+  testId
+}) => {
+  const { data: users, isLoading } = useQuery({
+    queryKey: ['users'],
+    queryFn: async () => {
+      const userClient = userClientManager.get();
+      const res = await userClient.index.$get({
+        query: {
+          page: 1,
+          pageSize: 100
+        }
+      });
+
+      if (res.status !== 200) throw new Error('获取用户列表失败');
+      const result = await res.json();
+      return result.data;
+    }
+  });
+
+  return (
+    <Select
+      value={value?.toString() || ''}
+      onValueChange={(val) => onChange?.(parseInt(val))}
+      disabled={disabled || isLoading}
+    >
+      <SelectTrigger data-testid={testId}>
+        <SelectValue placeholder={placeholder} />
+      </SelectTrigger>
+      <SelectContent>
+        {isLoading ? (
+          <SelectItem value="loading" disabled>加载中...</SelectItem>
+        ) : users && users.length > 0 ? (
+          users.map((user: any) => (
+            <SelectItem key={user.id} value={user.id.toString()}>
+              {user.name || user.username} ({user.phone || user.email})
+            </SelectItem>
+          ))
+        ) : (
+          <SelectItem value="no-users" disabled>暂无用户</SelectItem>
+        )}
+      </SelectContent>
+    </Select>
+  );
+};

+ 2 - 1
packages/user-management-ui-mt/src/components/index.ts

@@ -1 +1,2 @@
-export { UserManagement } from './UserManagement';
+export { UserManagement } from './UserManagement';
+export { UserSelector } from './UserSelector';

+ 1 - 1
packages/user-management-ui-mt/src/index.ts

@@ -1,3 +1,3 @@
 // 主导出文件
-export { UserManagement } from './components';
+export { UserManagement, UserSelector } from './components';
 export { userClient } from './api';

+ 278 - 0
packages/user-management-ui-mt/tests/integration/user-selector.integration.test.tsx

@@ -0,0 +1,278 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { UserSelector } from '../../src/components/UserSelector';
+import { userClient } from '../../src/api/userClient';
+
+// 完整的mock响应对象
+const createMockResponse = (status: number, data?: any) => ({
+  status,
+  ok: status >= 200 && status < 300,
+  body: null,
+  bodyUsed: false,
+  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
+  headers: new Headers(),
+  url: '',
+  redirected: false,
+  type: 'basic' as ResponseType,
+  json: async () => data || {},
+  text: async () => '',
+  blob: async () => new Blob(),
+  arrayBuffer: async () => new ArrayBuffer(0),
+  formData: async () => new FormData(),
+  clone: function() { return this; }
+});
+
+// Mock API client
+vi.mock('../../src/api/userClient', () => {
+  const mockUserClient = {
+    index: {
+      $get: vi.fn(() => Promise.resolve({
+        status: 200,
+        body: null,
+        json: async () => ({ data: [], pagination: { total: 0, page: 1, pageSize: 100 } })
+      })),
+    }
+  };
+
+  const mockUserClientManager = {
+    get: vi.fn(() => mockUserClient),
+  };
+
+  return {
+    userClientManager: mockUserClientManager,
+    userClient: mockUserClient,
+  };
+});
+
+const createTestQueryClient = () =>
+  new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        enabled: true,
+      },
+    },
+  });
+
+const renderWithProviders = (component: React.ReactElement) => {
+  const queryClient = createTestQueryClient();
+  return render(
+    <QueryClientProvider client={queryClient}>
+      {component as any}
+    </QueryClientProvider>
+  );
+};
+
+describe('用户选择器集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该加载并显示用户列表', async () => {
+    const mockUsers = {
+      data: [
+        {
+          id: 1,
+          username: 'user1',
+          name: 'User One',
+          email: 'user1@example.com',
+          phone: '1234567890',
+        },
+        {
+          id: 2,
+          username: 'user2',
+          name: 'User Two',
+          email: 'user2@example.com',
+          phone: '0987654321',
+        },
+      ],
+      pagination: {
+        total: 2,
+        page: 1,
+        pageSize: 100,
+      },
+    };
+
+    // Mock user list API
+    (userClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockUsers));
+
+    renderWithProviders(<UserSelector testId="user-selector" />);
+
+    // Open select dropdown to trigger API call
+    const selectTrigger = screen.getByTestId('user-selector');
+    fireEvent.click(selectTrigger);
+
+    // Wait for API call and loading to complete
+    await waitFor(() => {
+      expect(userClient.index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 100,
+        },
+      });
+    });
+
+    // Verify select trigger is rendered
+    expect(selectTrigger).toBeInTheDocument();
+  });
+
+  it('应该处理用户选择', async () => {
+    const mockUsers = {
+      data: [
+        {
+          id: 1,
+          username: 'user1',
+          name: 'User One',
+          email: 'user1@example.com',
+          phone: '1234567890',
+        },
+      ],
+      pagination: {
+        total: 1,
+        page: 1,
+        pageSize: 100,
+      },
+    };
+
+    const mockOnChange = vi.fn();
+
+    // Mock user list API
+    (userClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockUsers));
+
+    renderWithProviders(
+      <UserSelector onChange={mockOnChange} testId="user-selector" />
+    );
+
+    // Wait for API call
+    await waitFor(() => {
+      expect(userClient.index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 100,
+        },
+      });
+    });
+
+    // Verify select trigger is rendered
+    const selectTrigger = screen.getByTestId('user-selector');
+    expect(selectTrigger).toBeInTheDocument();
+
+    // Verify onChange callback is properly passed
+    expect(mockOnChange).not.toHaveBeenCalled();
+  });
+
+  it('应该显示自定义占位符', async () => {
+    const mockUsers = {
+      data: [],
+      pagination: {
+        total: 0,
+        page: 1,
+        pageSize: 100,
+      },
+    };
+
+    // Mock empty user list
+    (userClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockUsers));
+
+    renderWithProviders(
+      <UserSelector placeholder="请选择用户" testId="user-selector" />
+    );
+
+    // Open select dropdown to trigger API call
+    const selectTrigger = screen.getByTestId('user-selector');
+    fireEvent.click(selectTrigger);
+
+    // Wait for API call
+    await waitFor(() => {
+      expect(userClient.index.$get).toHaveBeenCalled();
+    });
+
+    // Verify custom placeholder is shown
+    expect(selectTrigger).toHaveTextContent('请选择用户');
+  });
+
+  it('应该处理禁用状态', async () => {
+    const mockUsers = {
+      data: [
+        {
+          id: 1,
+          username: 'user1',
+          name: 'User One',
+          email: 'user1@example.com',
+          phone: '1234567890',
+        },
+      ],
+      pagination: {
+        total: 1,
+        page: 1,
+        pageSize: 100,
+      },
+    };
+
+    // Mock user list API
+    (userClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockUsers));
+
+    renderWithProviders(
+      <UserSelector disabled={true} testId="user-selector" />
+    );
+
+    // Verify select is disabled immediately (no need to wait for API call)
+    const selectTrigger = screen.getByTestId('user-selector');
+    expect(selectTrigger).toBeDisabled();
+  });
+
+  it('应该处理API错误', async () => {
+    // Mock API error
+    (userClient.index.$get as any).mockRejectedValue(new Error('API Error'));
+
+    renderWithProviders(<UserSelector testId="user-selector" />);
+
+    // Should handle error without crashing
+    await waitFor(() => {
+      expect(screen.getByTestId('user-selector')).toBeInTheDocument();
+    });
+  });
+
+  it('应该显示预选值', async () => {
+    const mockUsers = {
+      data: [
+        {
+          id: 1,
+          username: 'user1',
+          name: 'User One',
+          email: 'user1@example.com',
+          phone: '1234567890',
+        },
+        {
+          id: 2,
+          username: 'user2',
+          name: 'User Two',
+          email: 'user2@example.com',
+          phone: '0987654321',
+        },
+      ],
+      pagination: {
+        total: 2,
+        page: 1,
+        pageSize: 100,
+      },
+    };
+
+    // Mock user list API
+    (userClient.index.$get as any).mockResolvedValue(createMockResponse(200, mockUsers));
+
+    renderWithProviders(
+      <UserSelector value={2} testId="user-selector" />
+    );
+
+    // Wait for data to load
+    await waitFor(() => {
+      expect(userClient.index.$get).toHaveBeenCalled();
+    });
+
+    // Verify the select has the correct value
+    const selectTrigger = screen.getByTestId('user-selector');
+    expect(selectTrigger).toHaveAttribute('data-state', 'closed');
+  });
+});