Преглед на файлове

✨ feat(user-management-ui): 创建单租户用户管理界面包

- 新增用户管理界面包 `@d8d/user-management-ui`,提供完整的用户CRUD操作、角色权限管理和用户状态管理功能
- 实现基于React + TypeScript + TanStack Query + React Hook Form技术栈的用户管理组件
- 添加完整的测试套件,包括单元测试和集成测试,确保功能质量
- 配置包构建和导出接口,支持独立部署和使用

📝 docs(stories): 更新用户管理界面故事文档

- 将认证管理UI包故事状态更新为"Ready for Review"
- 新增单租户用户管理界面包实现故事文档,包含详细的任务分解和技术规范

♻️ refactor(shared-ui-components): 扩展组件导出配置

- 为共享UI组件包添加完整的组件级导出配置,支持按需导入
- 扩展UI组件导出范围,包括表单、表格、对话框等常用组件

🔧 chore(tenant-management-ui): 优化租户管理界面

- 添加react-router-dom依赖,完善路由功能
- 修复TypeScript类型错误,优化组件代码质量
- 改进API客户端导入方式,提升代码可维护性
yourname преди 1 месец
родител
ревизия
e5529631b0
променени са 24 файла, в които са добавени 2152 реда и са изтрити 32 реда
  1. 1 1
      docs/stories/007.016.auth-management-ui-mt-package.story.md
  2. 175 0
      docs/stories/007.017.user-management-ui-package.story.md
  3. 240 0
      packages/shared-ui-components/package.json
  4. 12 1
      packages/shared-ui-components/src/components/ui/index.ts
  5. 1 0
      packages/tenant-management-ui/package.json
  6. 1 1
      packages/tenant-management-ui/src/client/api.ts
  7. 7 7
      packages/tenant-management-ui/src/components/TenantConfigPage.tsx
  8. 2 6
      packages/tenant-management-ui/src/components/TenantForm.tsx
  9. 5 16
      packages/tenant-management-ui/src/components/TenantsPage.tsx
  10. 46 0
      packages/user-management-ui/.eslintrc.js
  11. 35 0
      packages/user-management-ui/build.config.ts
  12. 93 0
      packages/user-management-ui/package.json
  13. 1 0
      packages/user-management-ui/src/api/index.ts
  14. 5 0
      packages/user-management-ui/src/api/userClient.ts
  15. 895 0
      packages/user-management-ui/src/components/UserManagement.tsx
  16. 1 0
      packages/user-management-ui/src/components/index.ts
  17. 2 0
      packages/user-management-ui/src/hooks/index.ts
  18. 3 0
      packages/user-management-ui/src/index.ts
  19. 272 0
      packages/user-management-ui/tests/integration/userManagement.integration.test.tsx
  20. 30 0
      packages/user-management-ui/tests/setup.ts
  21. 165 0
      packages/user-management-ui/tests/unit/UserManagement.test.tsx
  22. 33 0
      packages/user-management-ui/tsconfig.json
  23. 24 0
      packages/user-management-ui/vitest.config.ts
  24. 103 0
      pnpm-lock.yaml

+ 1 - 1
docs/stories/007.016.auth-management-ui-mt-package.story.md

@@ -2,7 +2,7 @@
 
 ## 状态
 
-Draft
+Ready for Review
 
 ## 故事
 

+ 175 - 0
docs/stories/007.017.user-management-ui-package.story.md

@@ -0,0 +1,175 @@
+# 故事007.017: 单租户用户管理界面独立包实现
+
+## 状态
+
+Draft
+
+## 故事
+
+**作为** 系统管理员,
+**我想要** 有一个独立的单租户用户管理界面包,
+**以便** 可以在单租户系统中独立管理用户和角色权限,而不影响现有的多租户系统。
+
+## 验收标准
+
+1. **AC 1**: 成功创建单租户用户管理界面包 `@d8d/user-management-ui`,包含正确的包配置和依赖管理
+2. **AC 2**: 复制前端用户管理界面 `web/src/client/admin/pages/Users.tsx` 为单租户用户管理界面包
+3. **AC 3**: 实现完整的用户CRUD操作、角色权限管理和用户状态管理
+4. **AC 4**: 基于React + TypeScript + TanStack Query + React Hook Form技术栈
+5. **AC 5**: 依赖共享UI组件包 `@d8d/shared-ui-components` 中的基础组件
+6. **AC 6**: 依赖用户模块包 `@d8d/user-module` 提供API客户端和类型定义
+7. **AC 7**: 提供workspace包依赖复用机制
+8. **AC 8**: 支持独立测试和部署
+9. **AC 9**: 验证现有功能无回归
+
+## 任务 / 子任务
+
+- [ ] 任务 1 (AC: 1, 7): 创建单租户用户管理界面包结构
+  - [ ] 创建包目录:`packages/user-management-ui/`
+  - [ ] 创建基础包结构:`src/`、`tests/`、`package.json`
+  - [ ] 配置包依赖和构建脚本
+
+- [ ] 任务 2 (AC: 1): 配置包依赖和构建
+  - [ ] 创建 `packages/user-management-ui/package.json` 包配置
+  - [ ] 添加依赖:`@d8d/shared-ui-components`、`@d8d/user-module`
+  - [ ] 配置构建脚本和TypeScript配置
+
+- [ ] 任务 3 (AC: 2, 3): 复制并调整用户管理界面组件
+  - [ ] 复制 `web/src/client/admin/pages/Users.tsx` 为 `packages/user-management-ui/src/components/UserManagement.tsx`
+  - [ ] 更新组件导入路径,使用共享UI组件包
+  - [ ] 调整API客户端,使用用户模块包
+
+- [ ] 任务 4 (AC: 3, 6): 创建API客户端和类型定义
+  - [ ] 创建 `packages/user-management-ui/src/api/userClient.ts` API客户端
+  - [ ] 创建 `packages/user-management-ui/src/types/user.ts` 类型定义
+  - [ ] 确保所有类型定义与用户模块包对齐
+
+- [ ] 任务 5 (AC: 3, 4): 实现完整的用户管理功能
+  - [ ] 实现用户列表查询和分页功能
+  - [ ] 实现用户创建、编辑、删除功能
+  - [ ] 实现用户状态管理和角色权限管理
+  - [ ] 实现搜索和过滤功能
+
+- [ ] 任务 6 (AC: 8): 创建测试套件
+  - [ ] 创建单元测试:`packages/user-management-ui/tests/unit/UserManagement.test.tsx`
+  - [ ] 创建集成测试:`packages/user-management-ui/tests/integration/user-management.integration.test.tsx`
+  - [ ] 创建测试工具:`packages/user-management-ui/tests/test-utils.tsx`
+
+- [ ] 任务 7 (AC: 1, 7): 配置包导出接口
+  - [ ] 创建 `packages/user-management-ui/src/index.ts` 包导出主入口
+  - [ ] 确保所有导出组件、hook和类型定义正确
+  - [ ] 验证导出脚本正常工作
+
+- [ ] 任务 8 (AC: 9): 验证功能无回归
+  - [ ] 运行包构建:`pnpm build`
+  - [ ] 运行所有测试:`pnpm test`
+  - [ ] 验证用户管理功能正常
+  - [ ] 验证与现有系统兼容性
+
+## Dev Notes
+
+### 技术栈和架构上下文
+- **技术栈**: React 19 + TypeScript + TanStack Query + React Hook Form [Source: architecture/tech-stack.md#现有技术栈维护]
+- **前端框架**: React 19.1.0 用于用户界面构建 [Source: architecture/tech-stack.md#现有技术栈维护]
+- **状态管理**: React Query 5.83.0 用于服务端状态管理 [Source: architecture/tech-stack.md#现有技术栈维护]
+- **构建工具**: Vite 7.0.0 用于开发服务器和构建 [Source: architecture/tech-stack.md#现有技术栈维护]
+
+### 项目结构
+- **包位置**: `packages/user-management-ui/` [Source: architecture/source-tree.md#实际项目结构]
+- **源码结构**:
+  - `src/components/` - React组件
+  - `src/hooks/` - 自定义React hooks
+  - `src/api/` - API客户端
+  - `src/types/` - TypeScript类型定义
+  - `tests/unit/` - 单元测试
+  - `tests/integration/` - 集成测试
+- **依赖管理**: 使用pnpm workspace依赖管理 [Source: architecture/source-tree.md#集成指南]
+
+### 依赖关系
+- **共享UI组件包**: `@d8d/shared-ui-components` - 提供基础UI组件 [Source: architecture/source-tree.md#实际项目结构]
+- **单租户用户模块**: `@d8d/user-module` - 提供用户管理API [Source: docs/prd/epic-007-multi-tenant-package-replication.md#用户管理界面包]
+
+### 从前一个故事吸取的经验教训
+- **useQuery测试策略**: 使用真实的QueryClientProvider而不是mock react-query,在TestWrapper中提供完整的react-query上下文 [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试策略关键发现]
+- **UI组件测试策略**: 使用data-testid进行元素定位比placeholder/role更准确稳定,避免因UI变化导致测试失败 [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试策略关键发现]
+- **React Hook Form处理**: 需要过滤React Hook Form的props避免React警告,改进mock策略 [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试策略关键发现]
+- **Router上下文**: 需要提供BrowserRouter上下文或mock useNavigate [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试策略关键发现]
+
+### 测试标准
+- **测试框架**: Vitest + Testing Library [Source: architecture/testing-strategy.md#单元测试]
+- **测试位置**: `packages/user-management-ui/tests/unit/` 和 `packages/user-management-ui/tests/integration/` [Source: architecture/testing-strategy.md#单元测试]
+- **测试覆盖率目标**: ≥ 80% 单元测试覆盖率 [Source: architecture/testing-strategy.md#各层覆盖率要求]
+- **测试执行**: 使用 `pnpm test` 运行所有测试 [Source: architecture/testing-strategy.md#本地开发测试]
+- **测试模式**: 遵循测试金字塔模型,包含单元测试、组件测试和集成测试 [Source: architecture/testing-strategy.md#测试金字塔策略]
+
+### 关键实施要点
+- **包命名**: 使用标准命名约定,不添加特殊后缀 [Source: docs/prd/epic-007-multi-tenant-package-replication.md#包命名约定]
+- **API客户端**: 使用Hono客户端调用单租户用户API [Source: docs/stories/007.015.auth-management-ui-package.story.md#任务-5]
+- **导出接口**: 提供完整的组件、hook和类型定义导出 [Source: docs/stories/007.015.auth-management-ui-package.story.md#任务-7]
+- **组件复用**: 基于现有用户管理界面实现,确保功能完整性和一致性
+
+### 用户管理功能特性
+- **用户列表**: 支持分页、搜索、过滤功能
+- **用户CRUD**: 完整的创建、读取、更新、删除操作
+- **角色管理**: 用户角色分配和权限管理
+- **状态管理**: 用户启用/禁用状态控制
+- **头像管理**: 支持头像上传和显示
+- **表单验证**: 完整的表单验证和错误处理
+
+### 测试
+
+#### 测试标准和框架
+- **测试框架**: Vitest 3.2.4 + Testing Library 16.3.0 [Source: architecture/testing-strategy.md#工具版本]
+- **测试位置**:
+  - 单元测试: `packages/user-management-ui/tests/unit/**/*.test.tsx`
+  - 集成测试: `packages/user-management-ui/tests/integration/**/*.test.tsx`
+  [Source: architecture/testing-strategy.md#单元测试]
+
+#### 测试模式和策略
+- **useQuery测试**: 使用真实的QueryClientProvider而不是mock react-query [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试策略关键发现]
+- **元素定位**: 使用data-testid进行元素定位,比placeholder/role更准确稳定 [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试策略关键发现]
+- **Mock策略**: 使用智能mock过滤React Hook Form props [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试架构改进]
+- **测试工具**: 提供QueryClientProvider和必要的上下文 [Source: docs/stories/007.015.auth-management-ui-package.story.md#测试架构改进]
+
+#### 特定测试要求
+- **用户CRUD测试**: 验证用户创建、读取、更新、删除功能
+- **角色权限测试**: 验证用户角色分配和权限管理
+- **搜索过滤测试**: 验证搜索和过滤功能正常工作
+- **表单验证测试**: 验证表单验证和错误处理
+- **API集成测试**: 验证与用户模块的API集成
+
+#### 测试执行命令
+- 运行所有测试: `cd packages/user-management-ui && pnpm test`
+- 运行单元测试: `cd packages/user-management-ui && pnpm test:unit`
+- 运行集成测试: `cd packages/user-management-ui && pnpm test:integration`
+- 生成覆盖率报告: `cd packages/user-management-ui && pnpm test:coverage`
+
+## 变更日志
+
+| 日期 | 版本 | 描述 | 作者 |
+|------|------|------|------|
+| 2025-11-15 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+
+## Dev Agent Record
+
+*此部分将在实施过程中由开发代理填充*
+
+### Agent Model Used
+
+*将在实施过程中填充*
+
+### Debug Log References
+
+*将在实施过程中填充*
+
+### Completion Notes List
+
+*将在实施过程中填充*
+
+### File List
+
+*将在实施过程中填充*
+
+## QA Results
+
+*此部分将在质量保证审查过程中由QA代理填充*

+ 240 - 0
packages/shared-ui-components/package.json

@@ -16,6 +16,11 @@
       "import": "./src/components/index.ts",
       "require": "./src/components/index.ts"
     },
+    "./components/*": {
+      "types": "./src/components/*",
+      "import": "./src/components/*",
+      "require": "./src/components/*"
+    },
     "./components/admin": {
       "types": "./src/components/admin/index.ts",
       "import": "./src/components/admin/index.ts",
@@ -26,6 +31,236 @@
       "import": "./src/components/ui/index.ts",
       "require": "./src/components/ui/index.ts"
     },
+    "./components/ui/accordion": {
+      "types": "./src/components/ui/accordion.tsx",
+      "import": "./src/components/ui/accordion.tsx",
+      "require": "./src/components/ui/accordion.tsx"
+    },
+    "./components/ui/alert-dialog": {
+      "types": "./src/components/ui/alert-dialog.tsx",
+      "import": "./src/components/ui/alert-dialog.tsx",
+      "require": "./src/components/ui/alert-dialog.tsx"
+    },
+    "./components/ui/alert": {
+      "types": "./src/components/ui/alert.tsx",
+      "import": "./src/components/ui/alert.tsx",
+      "require": "./src/components/ui/alert.tsx"
+    },
+    "./components/ui/aspect-ratio": {
+      "types": "./src/components/ui/aspect-ratio.tsx",
+      "import": "./src/components/ui/aspect-ratio.tsx",
+      "require": "./src/components/ui/aspect-ratio.tsx"
+    },
+    "./components/ui/avatar": {
+      "types": "./src/components/ui/avatar.tsx",
+      "import": "./src/components/ui/avatar.tsx",
+      "require": "./src/components/ui/avatar.tsx"
+    },
+    "./components/ui/badge": {
+      "types": "./src/components/ui/badge.tsx",
+      "import": "./src/components/ui/badge.tsx",
+      "require": "./src/components/ui/badge.tsx"
+    },
+    "./components/ui/breadcrumb": {
+      "types": "./src/components/ui/breadcrumb.tsx",
+      "import": "./src/components/ui/breadcrumb.tsx",
+      "require": "./src/components/ui/breadcrumb.tsx"
+    },
+    "./components/ui/button": {
+      "types": "./src/components/ui/button.tsx",
+      "import": "./src/components/ui/button.tsx",
+      "require": "./src/components/ui/button.tsx"
+    },
+    "./components/ui/calendar": {
+      "types": "./src/components/ui/calendar.tsx",
+      "import": "./src/components/ui/calendar.tsx",
+      "require": "./src/components/ui/calendar.tsx"
+    },
+    "./components/ui/card": {
+      "types": "./src/components/ui/card.tsx",
+      "import": "./src/components/ui/card.tsx",
+      "require": "./src/components/ui/card.tsx"
+    },
+    "./components/ui/carousel": {
+      "types": "./src/components/ui/carousel.tsx",
+      "import": "./src/components/ui/carousel.tsx",
+      "require": "./src/components/ui/carousel.tsx"
+    },
+    "./components/ui/chart": {
+      "types": "./src/components/ui/chart.tsx",
+      "import": "./src/components/ui/chart.tsx",
+      "require": "./src/components/ui/chart.tsx"
+    },
+    "./components/ui/checkbox": {
+      "types": "./src/components/ui/checkbox.tsx",
+      "import": "./src/components/ui/checkbox.tsx",
+      "require": "./src/components/ui/checkbox.tsx"
+    },
+    "./components/ui/collapsible": {
+      "types": "./src/components/ui/collapsible.tsx",
+      "import": "./src/components/ui/collapsible.tsx",
+      "require": "./src/components/ui/collapsible.tsx"
+    },
+    "./components/ui/command": {
+      "types": "./src/components/ui/command.tsx",
+      "import": "./src/components/ui/command.tsx",
+      "require": "./src/components/ui/command.tsx"
+    },
+    "./components/ui/context-menu": {
+      "types": "./src/components/ui/context-menu.tsx",
+      "import": "./src/components/ui/context-menu.tsx",
+      "require": "./src/components/ui/context-menu.tsx"
+    },
+    "./components/ui/dialog": {
+      "types": "./src/components/ui/dialog.tsx",
+      "import": "./src/components/ui/dialog.tsx",
+      "require": "./src/components/ui/dialog.tsx"
+    },
+    "./components/ui/drawer": {
+      "types": "./src/components/ui/drawer.tsx",
+      "import": "./src/components/ui/drawer.tsx",
+      "require": "./src/components/ui/drawer.tsx"
+    },
+    "./components/ui/dropdown-menu": {
+      "types": "./src/components/ui/dropdown-menu.tsx",
+      "import": "./src/components/ui/dropdown-menu.tsx",
+      "require": "./src/components/ui/dropdown-menu.tsx"
+    },
+    "./components/ui/form": {
+      "types": "./src/components/ui/form.tsx",
+      "import": "./src/components/ui/form.tsx",
+      "require": "./src/components/ui/form.tsx"
+    },
+    "./components/ui/hover-card": {
+      "types": "./src/components/ui/hover-card.tsx",
+      "import": "./src/components/ui/hover-card.tsx",
+      "require": "./src/components/ui/hover-card.tsx"
+    },
+    "./components/ui/input-otp": {
+      "types": "./src/components/ui/input-otp.tsx",
+      "import": "./src/components/ui/input-otp.tsx",
+      "require": "./src/components/ui/input-otp.tsx"
+    },
+    "./components/ui/input": {
+      "types": "./src/components/ui/input.tsx",
+      "import": "./src/components/ui/input.tsx",
+      "require": "./src/components/ui/input.tsx"
+    },
+    "./components/ui/label": {
+      "types": "./src/components/ui/label.tsx",
+      "import": "./src/components/ui/label.tsx",
+      "require": "./src/components/ui/label.tsx"
+    },
+    "./components/ui/menubar": {
+      "types": "./src/components/ui/menubar.tsx",
+      "import": "./src/components/ui/menubar.tsx",
+      "require": "./src/components/ui/menubar.tsx"
+    },
+    "./components/ui/navigation-menu": {
+      "types": "./src/components/ui/navigation-menu.tsx",
+      "import": "./src/components/ui/navigation-menu.tsx",
+      "require": "./src/components/ui/navigation-menu.tsx"
+    },
+    "./components/ui/pagination": {
+      "types": "./src/components/ui/pagination.tsx",
+      "import": "./src/components/ui/pagination.tsx",
+      "require": "./src/components/ui/pagination.tsx"
+    },
+    "./components/ui/popover": {
+      "types": "./src/components/ui/popover.tsx",
+      "import": "./src/components/ui/popover.tsx",
+      "require": "./src/components/ui/popover.tsx"
+    },
+    "./components/ui/progress": {
+      "types": "./src/components/ui/progress.tsx",
+      "import": "./src/components/ui/progress.tsx",
+      "require": "./src/components/ui/progress.tsx"
+    },
+    "./components/ui/radio-group": {
+      "types": "./src/components/ui/radio-group.tsx",
+      "import": "./src/components/ui/radio-group.tsx",
+      "require": "./src/components/ui/radio-group.tsx"
+    },
+    "./components/ui/resizable": {
+      "types": "./src/components/ui/resizable.tsx",
+      "import": "./src/components/ui/resizable.tsx",
+      "require": "./src/components/ui/resizable.tsx"
+    },
+    "./components/ui/scroll-area": {
+      "types": "./src/components/ui/scroll-area.tsx",
+      "import": "./src/components/ui/scroll-area.tsx",
+      "require": "./src/components/ui/scroll-area.tsx"
+    },
+    "./components/ui/select": {
+      "types": "./src/components/ui/select.tsx",
+      "import": "./src/components/ui/select.tsx",
+      "require": "./src/components/ui/select.tsx"
+    },
+    "./components/ui/separator": {
+      "types": "./src/components/ui/separator.tsx",
+      "import": "./src/components/ui/separator.tsx",
+      "require": "./src/components/ui/separator.tsx"
+    },
+    "./components/ui/sheet": {
+      "types": "./src/components/ui/sheet.tsx",
+      "import": "./src/components/ui/sheet.tsx",
+      "require": "./src/components/ui/sheet.tsx"
+    },
+    "./components/ui/sidebar": {
+      "types": "./src/components/ui/sidebar.tsx",
+      "import": "./src/components/ui/sidebar.tsx",
+      "require": "./src/components/ui/sidebar.tsx"
+    },
+    "./components/ui/skeleton": {
+      "types": "./src/components/ui/skeleton.tsx",
+      "import": "./src/components/ui/skeleton.tsx",
+      "require": "./src/components/ui/skeleton.tsx"
+    },
+    "./components/ui/slider": {
+      "types": "./src/components/ui/slider.tsx",
+      "import": "./src/components/ui/slider.tsx",
+      "require": "./src/components/ui/slider.tsx"
+    },
+    "./components/ui/sonner": {
+      "types": "./src/components/ui/sonner.tsx",
+      "import": "./src/components/ui/sonner.tsx",
+      "require": "./src/components/ui/sonner.tsx"
+    },
+    "./components/ui/switch": {
+      "types": "./src/components/ui/switch.tsx",
+      "import": "./src/components/ui/switch.tsx",
+      "require": "./src/components/ui/switch.tsx"
+    },
+    "./components/ui/table": {
+      "types": "./src/components/ui/table.tsx",
+      "import": "./src/components/ui/table.tsx",
+      "require": "./src/components/ui/table.tsx"
+    },
+    "./components/ui/tabs": {
+      "types": "./src/components/ui/tabs.tsx",
+      "import": "./src/components/ui/tabs.tsx",
+      "require": "./src/components/ui/tabs.tsx"
+    },
+    "./components/ui/textarea": {
+      "types": "./src/components/ui/textarea.tsx",
+      "import": "./src/components/ui/textarea.tsx",
+      "require": "./src/components/ui/textarea.tsx"
+    },
+    "./components/ui/toggle-group": {
+      "types": "./src/components/ui/toggle-group.tsx",
+      "import": "./src/components/ui/toggle-group.tsx",
+      "require": "./src/components/ui/toggle-group.tsx"
+    },
+    "./components/ui/toggle": {
+      "types": "./src/components/ui/toggle.tsx",
+      "import": "./src/components/ui/toggle.tsx",
+      "require": "./src/components/ui/toggle.tsx"
+    },
+    "./components/ui/tooltip": {
+      "types": "./src/components/ui/tooltip.tsx",
+      "import": "./src/components/ui/tooltip.tsx",
+      "require": "./src/components/ui/tooltip.tsx"
+    },
     "./hooks": {
       "types": "./src/hooks/index.ts",
       "import": "./src/hooks/index.ts",
@@ -36,6 +271,11 @@
       "import": "./src/utils/index.ts",
       "require": "./src/utils/index.ts"
     },
+    "./utils/cn": {
+      "types": "./src/utils/cn.ts",
+      "import": "./src/utils/cn.ts",
+      "require": "./src/utils/cn.ts"
+    },
     "./types": {
       "types": "./src/types/index.ts",
       "import": "./src/types/index.ts",

+ 12 - 1
packages/shared-ui-components/src/components/ui/index.ts

@@ -2,4 +2,15 @@
 export * from './button';
 export * from './input';
 export * from './card';
-export * from './pagination';
+export * from './pagination';
+export * from './label';
+export * from './textarea';
+export * from './select';
+export * from './switch';
+export * from './table';
+export * from './badge';
+export * from './dialog';
+export * from './form';
+export * from './skeleton';
+export * from './popover';
+export * from './calendar';

+ 1 - 0
packages/tenant-management-ui/package.json

@@ -45,6 +45,7 @@
     "@tanstack/react-query": "^5.83.0",
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
+    "react-router-dom": "^7.7.0",
     "react-hook-form": "^7.61.1",
     "@hookform/resolvers": "^5.2.1",
     "hono": "^4.8.5",

+ 1 - 1
packages/tenant-management-ui/src/client/api.ts

@@ -1,5 +1,5 @@
 import { hc } from 'hono/client';
-import type { tenantRoutes } from '@d8d/tenant-module-mt/src/routes';
+import { tenantRoutes } from '@d8d/tenant-module-mt';
 
 // 创建租户管理API客户端
 export const tenantClient = hc<typeof tenantRoutes>('/api/tenants');

+ 7 - 7
packages/tenant-management-ui/src/components/TenantConfigPage.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
 import { useParams } from 'react-router-dom';
 import { useQuery } from '@tanstack/react-query';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
@@ -143,7 +143,7 @@ export const TenantConfigPage = () => {
                 <Label htmlFor="theme">主题</Label>
                 <Select
                   value={form.watch('theme')}
-                  onValueChange={(value) => form.setValue('theme', value)}
+                  onValueChange={(value: string) => form.setValue('theme', value)}
                 >
                   <SelectTrigger>
                     <SelectValue placeholder="选择主题" />
@@ -161,7 +161,7 @@ export const TenantConfigPage = () => {
                 <Label htmlFor="language">语言</Label>
                 <Select
                   value={form.watch('language')}
-                  onValueChange={(value) => form.setValue('language', value)}
+                  onValueChange={(value: string) => form.setValue('language', value)}
                 >
                   <SelectTrigger>
                     <SelectValue placeholder="选择语言" />
@@ -178,7 +178,7 @@ export const TenantConfigPage = () => {
                 <Label htmlFor="timezone">时区</Label>
                 <Select
                   value={form.watch('timezone')}
-                  onValueChange={(value) => form.setValue('timezone', value)}
+                  onValueChange={(value: string) => form.setValue('timezone', value)}
                 >
                   <SelectTrigger>
                     <SelectValue placeholder="选择时区" />
@@ -196,7 +196,7 @@ export const TenantConfigPage = () => {
                 <Label htmlFor="currency">货币</Label>
                 <Select
                   value={form.watch('currency')}
-                  onValueChange={(value) => form.setValue('currency', value)}
+                  onValueChange={(value: string) => form.setValue('currency', value)}
                 >
                   <SelectTrigger>
                     <SelectValue placeholder="选择货币" />
@@ -246,7 +246,7 @@ export const TenantConfigPage = () => {
                 <Switch
                   id="enableNotifications"
                   checked={form.watch('enableNotifications')}
-                  onCheckedChange={(checked) => form.setValue('enableNotifications', checked)}
+                  onCheckedChange={(checked: boolean) => form.setValue('enableNotifications', checked)}
                 />
               </div>
 
@@ -260,7 +260,7 @@ export const TenantConfigPage = () => {
                 <Switch
                   id="enableAuditLog"
                   checked={form.watch('enableAuditLog')}
-                  onCheckedChange={(checked) => form.setValue('enableAuditLog', checked)}
+                  onCheckedChange={(checked: boolean) => form.setValue('enableAuditLog', checked)}
                 />
               </div>
             </div>

+ 2 - 6
packages/tenant-management-ui/src/components/TenantForm.tsx

@@ -2,15 +2,11 @@ import React from 'react';
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { CreateTenantDto, UpdateTenantDto } from '@d8d/tenant-module-mt/schemas';
-import { Button } from '@d8d/shared-ui-components/components/ui/button';
-import { Input } from '@d8d/shared-ui-components/components/ui/input';
-import { Textarea } from '@d8d/shared-ui-components/components/ui/textarea';
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
-import { Switch } from '@d8d/shared-ui-components/components/ui/switch';
+import { Button, Input, Textarea, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Switch } from '@d8d/shared-ui-components';
 import type { InferResponseType } from 'hono/client';
 import type { tenantClient } from '@/client/api';
 
-type TenantResponse = InferResponseType<typeof tenantClient.$get, 200>['data'][0];
+type TenantResponse = InferResponseType<typeof tenantClient.index.$get, 200>['data'][0];
 
 interface TenantFormProps {
   tenant?: TenantResponse;

+ 5 - 16
packages/tenant-management-ui/src/components/TenantsPage.tsx

@@ -4,30 +4,19 @@ import { format } from 'date-fns';
 import { Plus, Search, Edit, Trash2, Filter, X } from 'lucide-react';
 import { tenantClient } from '@/client/api';
 import type { InferRequestType, InferResponseType } from 'hono/client';
-import { Button } from '@d8d/shared-ui-components/components/ui/button';
-import { Input } from '@d8d/shared-ui-components/components/ui/input';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
-import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { Button, Input, Card, CardContent, CardDescription, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Badge, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Skeleton, Switch, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Popover, PopoverContent, PopoverTrigger, Calendar } from '@d8d/shared-ui-components';
 import { DataTablePagination } from '@/components/DataTablePagination';
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { toast } from 'sonner';
-import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
-import { Switch } from '@d8d/shared-ui-components/components/ui/switch';
 import { CreateTenantDto, UpdateTenantDto } from '@d8d/tenant-module-mt/schemas';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
-import { Popover, PopoverContent, PopoverTrigger } from '@d8d/shared-ui-components/components/ui/popover';
-import { Calendar } from '@d8d/shared-ui-components/components/ui/calendar';
 import { cn } from '@/utils/cn';
 import { formatTenantStatus } from '@/utils/formatTenantStatus';
 
 // 使用RPC方式提取类型
-type CreateTenantRequest = InferRequestType<typeof tenantClient.$post>['json'];
+type CreateTenantRequest = InferRequestType<typeof tenantClient.index.$post>['json'];
 type UpdateTenantRequest = InferRequestType<typeof tenantClient[':id']['$put']>['json'];
-type TenantResponse = InferResponseType<typeof tenantClient.$get, 200>['data'][0];
+type TenantResponse = InferResponseType<typeof tenantClient.index.$get, 200>['data'][0];
 
 // 直接使用后端定义的 schema
 const createTenantFormSchema = CreateTenantDto;
@@ -94,7 +83,7 @@ export const TenantsPage = () => {
         filterParams.createdAt = filters.createdAt;
       }
 
-      const res = await tenantClient.$get({
+      const res = await tenantClient.index.$get({
         query: {
           page: searchParams.page,
           pageSize: searchParams.limit,
@@ -205,7 +194,7 @@ export const TenantsPage = () => {
   // 处理创建表单提交
   const handleCreateSubmit = async (data: CreateTenantFormData) => {
     try {
-      const res = await tenantClient.$post({
+      const res = await tenantClient.index.$post({
         json: data
       });
       if (res.status !== 201) {

+ 46 - 0
packages/user-management-ui/.eslintrc.js

@@ -0,0 +1,46 @@
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true,
+    node: true,
+  },
+  extends: [
+    'eslint:recommended',
+    '@typescript-eslint/recommended',
+    '@typescript-eslint/recommended-requiring-type-checking',
+  ],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaVersion: 'latest',
+    sourceType: 'module',
+    project: './tsconfig.json',
+  },
+  plugins: [
+    '@typescript-eslint',
+    'react',
+    'react-hooks',
+  ],
+  rules: {
+    // React specific rules
+    'react/jsx-uses-react': 'off',
+    'react/react-in-jsx-scope': 'off',
+    'react-hooks/rules-of-hooks': 'error',
+    'react-hooks/exhaustive-deps': 'warn',
+
+    // TypeScript rules
+    '@typescript-eslint/no-unused-vars': 'error',
+    '@typescript-eslint/explicit-function-return-type': 'off',
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
+    '@typescript-eslint/no-explicit-any': 'warn',
+
+    // General rules
+    'no-console': 'warn',
+    'prefer-const': 'error',
+    'no-var': 'error',
+  },
+  settings: {
+    react: {
+      version: 'detect',
+    },
+  },
+};

+ 35 - 0
packages/user-management-ui/build.config.ts

@@ -0,0 +1,35 @@
+import { defineBuildConfig } from 'unbuild';
+
+export default defineBuildConfig({
+  entries: [
+    'src/index',
+    'src/components/index',
+    'src/hooks/index',
+    'src/api/index'
+  ],
+  declaration: true,
+  clean: true,
+  rollup: {
+    emitCJS: true,
+    esbuild: {
+      target: 'node18'
+    }
+  },
+  externals: [
+    'react',
+    'react-dom',
+    '@tanstack/react-query',
+    'react-hook-form',
+    '@hookform/resolvers',
+    'hono',
+    'sonner',
+    'date-fns',
+    'lucide-react',
+    'class-variance-authority',
+    'clsx',
+    'tailwind-merge',
+    'zod',
+    'axios',
+    'dayjs'
+  ]
+});

+ 93 - 0
packages/user-management-ui/package.json

@@ -0,0 +1,93 @@
+{
+  "name": "@d8d/user-management-ui",
+  "version": "1.0.0",
+  "description": "用户管理界面包 - 提供用户管理的完整前端界面,包括用户CRUD操作、角色权限管理、用户状态管理等功能",
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./components": {
+      "types": "./src/components/index.ts",
+      "import": "./src/components/index.ts",
+      "require": "./src/components/index.ts"
+    },
+    "./hooks": {
+      "types": "./src/hooks/index.ts",
+      "import": "./src/hooks/index.ts",
+      "require": "./src/hooks/index.ts"
+    },
+    "./api": {
+      "types": "./src/api/index.ts",
+      "import": "./src/api/index.ts",
+      "require": "./src/api/index.ts"
+    }
+  },
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "build": "unbuild",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "test:watch": "vitest",
+    "test:coverage": "vitest run --coverage",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/user-module": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@tanstack/react-query": "^5.83.0",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "react-router": "^7.1.3",
+    "react-hook-form": "^7.61.1",
+    "@hookform/resolvers": "^5.2.1",
+    "hono": "^4.8.5",
+    "sonner": "^2.0.7",
+    "date-fns": "^4.1.0",
+    "lucide-react": "^0.536.0",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "tailwind-merge": "^3.3.1",
+    "zod": "^4.0.15",
+    "axios": "^1.7.9",
+    "dayjs": "^1.11.13"
+  },
+  "devDependencies": {
+    "@types/node": "^22.10.2",
+    "@types/react": "^19.1.8",
+    "@types/react-dom": "^19.1.6",
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4",
+    "@testing-library/react": "^16.3.0",
+    "@testing-library/jest-dom": "^6.8.0",
+    "@testing-library/user-event": "^14.6.1",
+    "jsdom": "^26.0.0",
+    "@typescript-eslint/eslint-plugin": "^8.18.1",
+    "@typescript-eslint/parser": "^8.18.1",
+    "eslint": "^9.17.0",
+    "unbuild": "^3.4.0"
+  },
+  "peerDependencies": {
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0"
+  },
+  "keywords": [
+    "user",
+    "management",
+    "admin",
+    "ui",
+    "react",
+    "crud",
+    "permissions"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

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

@@ -0,0 +1 @@
+export { userClient } from './userClient';

+ 5 - 0
packages/user-management-ui/src/api/userClient.ts

@@ -0,0 +1,5 @@
+import { hc } from 'hono/client';
+import { userRoutes } from '@d8d/user-module';
+
+// 创建用户模块的RPC客户端
+export const userClient = hc<typeof userRoutes>('/');

+ 895 - 0
packages/user-management-ui/src/components/UserManagement.tsx

@@ -0,0 +1,895 @@
+import React, { useState, useMemo, useCallback } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { format } from 'date-fns';
+import { Plus, Search, Edit, Trash2, Filter, X } from 'lucide-react';
+import { userClient } from '../api/userClient';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Input } from '@d8d/shared-ui-components/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
+import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
+import { Switch } from '@d8d/shared-ui-components/components/ui/switch';
+import { CreateUserDto, UpdateUserDto } from '@d8d/user-module/schemas';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { Popover, PopoverContent, PopoverTrigger } from '@d8d/shared-ui-components/components/ui/popover';
+import { Calendar } from '@d8d/shared-ui-components/components/ui/calendar';
+import { cn } from '@d8d/shared-ui-components/utils/cn';
+import { DisabledStatus } from '@d8d/shared-types';
+
+// 使用RPC方式提取类型
+type CreateUserRequest = InferRequestType<typeof userClient.index.$post>['json'];
+type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json'];
+type UserResponse = InferResponseType<typeof userClient.index.$get, 200>['data'][0];
+
+// 直接使用后端定义的 schema
+const createUserFormSchema = CreateUserDto;
+const updateUserFormSchema = UpdateUserDto;
+
+type CreateUserFormData = CreateUserRequest;
+type UpdateUserFormData = UpdateUserRequest;
+
+export const UserManagement = () => {
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    keyword: ''
+  });
+  const [filters, setFilters] = useState({
+    isDisabled: undefined as number | undefined,
+    roleIds: [] as number[],
+    createdAt: undefined as { gte?: string; lte?: string } | undefined
+  });
+  const [showFilters, setShowFilters] = useState(false);
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingUser, setEditingUser] = useState<UserResponse | null>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [userToDelete, setUserToDelete] = useState<number | null>(null);
+
+  const [isCreateForm, setIsCreateForm] = useState(true);
+
+  const createForm = useForm<CreateUserFormData>({
+    resolver: zodResolver(createUserFormSchema),
+    defaultValues: {
+      username: '',
+      nickname: undefined,
+      email: null,
+      phone: null,
+      name: null,
+      password: '',
+      isDisabled: DisabledStatus.ENABLED,
+    },
+  });
+
+  const updateForm = useForm<UpdateUserFormData>({
+    resolver: zodResolver(updateUserFormSchema),
+    defaultValues: {
+      username: undefined,
+      nickname: undefined,
+      email: null,
+      phone: null,
+      name: null,
+      password: undefined,
+      isDisabled: undefined,
+    },
+  });
+
+  const { data: usersData, isLoading, refetch } = useQuery({
+    queryKey: ['users', searchParams, filters],
+    queryFn: async () => {
+      const filterParams: Record<string, unknown> = {};
+
+      if (filters.isDisabled !== undefined) {
+        filterParams.isDisabled = filters.isDisabled;
+      }
+
+      if (filters.roleIds.length > 0) {
+        filterParams['roles.id'] = filters.roleIds;
+      }
+
+      if (filters.createdAt) {
+        filterParams.createdAt = filters.createdAt;
+      }
+
+      const res = await userClient.index.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.keyword,
+          filters: Object.keys(filterParams).length > 0 ? JSON.stringify(filterParams) : undefined
+        }
+      });
+      if (res.status !== 200) {
+        throw new Error('获取用户列表失败');
+      }
+      return await res.json();
+    }
+  });
+
+  const users = usersData?.data || [];
+  const totalCount = usersData?.pagination?.total || 0;
+
+  // 防抖搜索函数
+  const debounce = (func: Function, delay: number) => {
+    let timeoutId: NodeJS.Timeout;
+    return (...args: any[]) => {
+      clearTimeout(timeoutId);
+      timeoutId = setTimeout(() => func(...args), delay);
+    };
+  };
+
+  // 使用useCallback包装防抖搜索
+  const debouncedSearch = useCallback(
+    debounce((keyword: string) => {
+      setSearchParams(prev => ({ ...prev, keyword, page: 1 }));
+    }, 300),
+    []
+  );
+
+  // 处理搜索输入变化
+  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const keyword = e.target.value;
+    setSearchParams(prev => ({ ...prev, keyword }));
+    debouncedSearch(keyword);
+  };
+
+  // 处理搜索表单提交
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 处理分页
+  const handlePageChange = (page: number, limit: number) => {
+    setSearchParams(prev => ({ ...prev, page, limit }));
+  };
+
+  // 处理过滤条件变化
+  const handleFilterChange = (newFilters: Partial<typeof filters>) => {
+    setFilters(prev => ({ ...prev, ...newFilters }));
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 重置所有过滤条件
+  const resetFilters = () => {
+    setFilters({
+      isDisabled: undefined,
+      roleIds: [],
+      createdAt: undefined
+    });
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 检查是否有活跃的过滤条件
+  const hasActiveFilters = useMemo(() => {
+    return filters.isDisabled !== undefined ||
+           filters.roleIds.length > 0 ||
+           filters.createdAt !== undefined;
+  }, [filters]);
+
+  // 打开创建用户对话框
+  const handleCreateUser = () => {
+    setEditingUser(null);
+    setIsCreateForm(true);
+    createForm.reset({
+      username: '',
+      nickname: null,
+      email: null,
+      phone: null,
+      name: null,
+      password: '',
+      isDisabled: DisabledStatus.ENABLED,
+    });
+    setIsModalOpen(true);
+  };
+
+  // 打开编辑用户对话框
+  const handleEditUser = (user: UserResponse) => {
+    setEditingUser(user);
+    setIsCreateForm(false);
+    updateForm.reset({
+      username: user.username,
+      nickname: user.nickname,
+      email: user.email,
+      phone: user.phone,
+      name: user.name,
+      avatarFileId: user.avatarFileId,
+      isDisabled: user.isDisabled,
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理创建表单提交
+  const handleCreateSubmit = async (data: CreateUserFormData) => {
+    try {
+      const res = await userClient.index.$post({
+        json: data
+      });
+      if (res.status !== 201) {
+        throw new Error('创建用户失败');
+      }
+      toast.success('用户创建成功');
+      setIsModalOpen(false);
+      refetch();
+    } catch {
+      toast.error('创建失败,请重试');
+    }
+  };
+
+  // 处理更新表单提交
+  const handleUpdateSubmit = async (data: UpdateUserFormData) => {
+    if (!editingUser) return;
+
+    try {
+      const res = await userClient[':id']['$put']({
+        param: { id: editingUser.id },
+        json: data
+      });
+      if (res.status !== 200) {
+        throw new Error('更新用户失败');
+      }
+      toast.success('用户更新成功');
+      setIsModalOpen(false);
+      refetch();
+    } catch {
+      toast.error('更新失败,请重试');
+    }
+  };
+
+  // 处理删除用户
+  const handleDeleteUser = (id: number) => {
+    setUserToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  const confirmDelete = async () => {
+    if (!userToDelete) return;
+
+    try {
+      const res = await userClient[':id']['$delete']({
+        param: { id: userToDelete }
+      });
+      if (res.status !== 204) {
+        throw new Error('删除用户失败');
+      }
+      toast.success('用户删除成功');
+      refetch();
+    } catch {
+      toast.error('删除失败,请重试');
+    } finally {
+      setDeleteDialogOpen(false);
+      setUserToDelete(null);
+    }
+  };
+
+  // 渲染表格部分的骨架屏
+  const renderTableSkeleton = () => (
+    <div className="space-y-2">
+      {Array.from({ length: 5 }).map((_, index) => (
+        <div key={index} className="flex space-x-4">
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 flex-1" />
+          <Skeleton className="h-4 w-16" />
+        </div>
+      ))}
+    </div>
+  );
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">用户管理</h1>
+        <Button onClick={handleCreateUser}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建用户
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>用户列表</CardTitle>
+          <CardDescription>
+            管理系统中的所有用户,共 {totalCount} 位用户
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4 space-y-4">
+            <form onSubmit={handleSearch} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索用户名、昵称或邮箱..."
+                  value={searchParams.keyword}
+                  onChange={handleSearchChange}
+                  className="pl-8"
+                />
+              </div>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+              <Button
+                type="button"
+                variant="outline"
+                onClick={() => setShowFilters(!showFilters)}
+                className="flex items-center gap-2"
+              >
+                <Filter className="h-4 w-4" />
+                高级筛选
+                {hasActiveFilters && (
+                  <Badge variant="secondary" className="ml-1">
+                    {Object.values(filters).filter(v =>
+                      v !== undefined &&
+                      (!Array.isArray(v) || v.length > 0)
+                    ).length}
+                  </Badge>
+                )}
+              </Button>
+              {hasActiveFilters && (
+                <Button
+                  type="button"
+                  variant="ghost"
+                  onClick={resetFilters}
+                  className="flex items-center gap-2"
+                >
+                  <X className="h-4 w-4" />
+                  重置
+                </Button>
+              )}
+            </form>
+
+            {showFilters && (
+              <div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 border rounded-lg bg-muted/50">
+                {/* 状态筛选 */}
+                <div className="space-y-2">
+                  <label className="text-sm font-medium">用户状态</label>
+                  <Select
+                    value={filters.isDisabled === undefined ? 'all' : filters.isDisabled.toString()}
+                    onValueChange={(value) =>
+                      handleFilterChange({
+                        isDisabled: value === 'all' ? undefined : parseInt(value)
+                      })
+                    }
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="选择状态" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="all">全部状态</SelectItem>
+                      <SelectItem value="0">启用</SelectItem>
+                      <SelectItem value="1">禁用</SelectItem>
+                    </SelectContent>
+                  </Select>
+                </div>
+
+                {/* 角色筛选 */}
+                <div className="space-y-2">
+                  <label className="text-sm font-medium">用户角色</label>
+                  <Select
+                    value=""
+                    onValueChange={(value) => {
+                      const roleId = parseInt(value);
+                      if (!filters.roleIds.includes(roleId)) {
+                        handleFilterChange({
+                          roleIds: [...filters.roleIds, roleId]
+                        });
+                      }
+                    }}
+                  >
+                    <SelectTrigger>
+                      <SelectValue placeholder="选择角色" />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value="1">管理员</SelectItem>
+                      <SelectItem value="2">普通用户</SelectItem>
+                    </SelectContent>
+                  </Select>
+                  {filters.roleIds.length > 0 && (
+                    <div className="flex flex-wrap gap-2 mt-2">
+                      {filters.roleIds.map(roleId => (
+                        <Badge
+                          key={roleId}
+                          variant="secondary"
+                          className="flex items-center gap-1"
+                        >
+                          {roleId === 1 ? '管理员' : '普通用户'}
+                          <X
+                            className="h-3 w-3 cursor-pointer"
+                            onClick={() => handleFilterChange({
+                              roleIds: filters.roleIds.filter(id => id !== roleId)
+                            })}
+                          />
+                        </Badge>
+                      ))}
+                    </div>
+                  )}
+                </div>
+
+                {/* 创建时间筛选 */}
+                <div className="space-y-2">
+                  <label className="text-sm font-medium">创建时间</label>
+                  <Popover>
+                    <PopoverTrigger asChild>
+                      <Button
+                        variant="outline"
+                        className={cn(
+                          "w-full justify-start text-left font-normal",
+                          !filters.createdAt && "text-muted-foreground"
+                        )}
+                      >
+                        {filters.createdAt ?
+                          `${filters.createdAt.gte || ''} 至 ${filters.createdAt.lte || ''}` :
+                          '选择日期范围'
+                        }
+                      </Button>
+                    </PopoverTrigger>
+                    <PopoverContent className="w-auto p-0" align="start">
+                      <Calendar
+                        mode="range"
+                        selected={{
+                          from: filters.createdAt?.gte ? new Date(filters.createdAt.gte) : undefined,
+                          to: filters.createdAt?.lte ? new Date(filters.createdAt.lte) : undefined
+                        }}
+                        onSelect={(range) => {
+                          handleFilterChange({
+                            createdAt: range?.from && range?.to ? {
+                              gte: format(range.from, 'yyyy-MM-dd'),
+                              lte: format(range.to, 'yyyy-MM-dd')
+                            } : undefined
+                          });
+                        }}
+                        initialFocus
+                      />
+                    </PopoverContent>
+                  </Popover>
+                </div>
+              </div>
+            )}
+
+            {/* 过滤条件标签 */}
+            {hasActiveFilters && (
+              <div className="flex flex-wrap gap-2">
+                {filters.isDisabled !== undefined && (
+                  <Badge variant="secondary" className="flex items-center gap-1">
+                    状态: {filters.isDisabled === 0 ? '启用' : '禁用'}
+                    <X
+                      className="h-3 w-3 cursor-pointer"
+                      onClick={() => handleFilterChange({ isDisabled: undefined })}
+                    />
+                  </Badge>
+                )}
+                {filters.roleIds.map(roleId => (
+                  <Badge key={roleId} variant="secondary" className="flex items-center gap-1">
+                    角色: {roleId === 1 ? '管理员' : '普通用户'}
+                    <X
+                      className="h-3 w-3 cursor-pointer"
+                      onClick={() => handleFilterChange({
+                        roleIds: filters.roleIds.filter(id => id !== roleId)
+                      })}
+                    />
+                  </Badge>
+                ))}
+                {filters.createdAt && (
+                  <Badge variant="secondary" className="flex items-center gap-1">
+                    创建时间: {filters.createdAt.gte || ''} 至 {filters.createdAt.lte || ''}
+                    <X
+                      className="h-3 w-3 cursor-pointer"
+                      onClick={() => handleFilterChange({ createdAt: undefined })}
+                    />
+                  </Badge>
+                )}
+              </div>
+            )}
+          </div>
+
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>头像</TableHead>
+                  <TableHead>用户名</TableHead>
+                  <TableHead>昵称</TableHead>
+                  <TableHead>邮箱</TableHead>
+                  <TableHead>真实姓名</TableHead>
+                  <TableHead>角色</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {isLoading ? (
+                  // 显示表格骨架屏
+                  <TableRow>
+                    <TableCell colSpan={8} className="p-4">
+                      {renderTableSkeleton()}
+                    </TableCell>
+                  </TableRow>
+                ) : (
+                  // 显示实际用户数据
+                  users.map((user) => (
+                    <TableRow key={user.id}>
+                      <TableCell>
+                      <div className="w-10 h-10">
+                        {user.avatarFile?.fullUrl ? (
+                          <img
+                            src={user.avatarFile.fullUrl}
+                            alt={user.username}
+                            className="w-10 h-10 rounded-full object-cover"
+                          />
+                        ) : (
+                          <div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
+                            <span className="text-sm font-medium text-gray-500">
+                              {user.username?.charAt(0)?.toUpperCase() || 'U'}
+                            </span>
+                          </div>
+                        )}
+                      </div>
+                    </TableCell>
+                    <TableCell className="font-medium">{user.username}</TableCell>
+                      <TableCell>{user.nickname || '-'}</TableCell>
+                      <TableCell>{user.email || '-'}</TableCell>
+                      <TableCell>{user.name || '-'}</TableCell>
+                      <TableCell>
+                        <Badge
+                          variant={user.roles?.some((role) => role.name === 'admin') ? 'destructive' : 'default'}
+                          className="capitalize"
+                        >
+                          {user.roles?.some((role) => role.name === 'admin') ? '管理员' : '普通用户'}
+                        </Badge>
+                      </TableCell>
+                      <TableCell>
+                        <Badge
+                          variant={user.isDisabled === 1 ? 'secondary' : 'default'}
+                        >
+                          {user.isDisabled === 1 ? '禁用' : '启用'}
+                        </Badge>
+                      </TableCell>
+                      <TableCell>
+                        {format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm')}
+                      </TableCell>
+                      <TableCell className="text-right">
+                        <div className="flex justify-end gap-2">
+                          <Button
+                            variant="ghost"
+                            size="icon"
+                            onClick={() => handleEditUser(user)}
+                          >
+                            <Edit className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            variant="ghost"
+                            size="icon"
+                            onClick={() => handleDeleteUser(user.id)}
+                          >
+                            <Trash2 className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      </TableCell>
+                    </TableRow>
+                  ))
+                )}
+              </TableBody>
+            </Table>
+          </div>
+
+          {/* 分页组件 - 需要外部提供或创建简化版本 */}
+          <div className="mt-4 flex items-center justify-between">
+            <div className="text-sm text-muted-foreground">
+              第 {searchParams.page} 页,共 {Math.ceil(totalCount / searchParams.limit)} 页
+            </div>
+            <div className="flex gap-2">
+              <Button
+                variant="outline"
+                size="sm"
+                disabled={searchParams.page <= 1}
+                onClick={() => handlePageChange(searchParams.page - 1, searchParams.limit)}
+              >
+                上一页
+              </Button>
+              <Button
+                variant="outline"
+                size="sm"
+                disabled={searchParams.page >= Math.ceil(totalCount / searchParams.limit)}
+                onClick={() => handlePageChange(searchParams.page + 1, searchParams.limit)}
+              >
+                下一页
+              </Button>
+            </div>
+          </div>
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑用户对话框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>
+              {editingUser ? '编辑用户' : '创建用户'}
+            </DialogTitle>
+            <DialogDescription>
+              {editingUser ? '编辑现有用户信息' : '创建一个新的用户账户'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户名
+                        <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入用户名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="nickname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>昵称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入昵称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="email"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>邮箱</FormLabel>
+                      <FormControl>
+                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>真实姓名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入真实姓名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        密码
+                        <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="请输入密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="isDisabled"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                      <div className="space-y-0.5">
+                        <FormLabel className="text-base">用户状态</FormLabel>
+                        <FormDescription>
+                          禁用后用户将无法登录系统
+                        </FormDescription>
+                      </div>
+                      <FormControl>
+                        <Switch
+                          checked={field.value === 1}
+                          onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
+                        />
+                      </FormControl>
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">
+                    创建用户
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户名
+                        <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入用户名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="nickname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>昵称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入昵称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="email"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>邮箱</FormLabel>
+                      <FormControl>
+                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>真实姓名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入真实姓名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>新密码</FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="留空则不修改密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="isDisabled"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                      <div className="space-y-0.5">
+                        <FormLabel className="text-base">用户状态</FormLabel>
+                        <FormDescription>
+                          禁用后用户将无法登录系统
+                        </FormDescription>
+                      </div>
+                      <FormControl>
+                        <Switch
+                          checked={field.value === 1}
+                          onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
+                        />
+                      </FormControl>
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">
+                    更新用户
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个用户吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button variant="destructive" onClick={confirmDelete}>
+              删除
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};

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

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

+ 2 - 0
packages/user-management-ui/src/hooks/index.ts

@@ -0,0 +1,2 @@
+// 用户管理相关的自定义hooks
+export {};

+ 3 - 0
packages/user-management-ui/src/index.ts

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

+ 272 - 0
packages/user-management-ui/tests/integration/userManagement.integration.test.tsx

@@ -0,0 +1,272 @@
+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 { UserManagement } from '../../src/components/UserManagement';
+
+// Mock API client
+vi.mock('../../src/api/userClient', () => ({
+  userClient: {
+    $get: vi.fn(),
+    $post: vi.fn(),
+    ':id': {
+      $put: vi.fn(),
+      $delete: vi.fn(),
+    },
+  },
+}));
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+  },
+}));
+
+const createTestQueryClient = () =>
+  new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+const renderWithProviders = (component: React.ReactElement) => {
+  const queryClient = createTestQueryClient();
+  return render(
+    <QueryClientProvider client={queryClient}>
+      {component}
+    </QueryClientProvider>
+  );
+};
+
+describe('UserManagement Integration Tests', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('should complete full user CRUD flow', async () => {
+    const mockUsers = {
+      data: [
+        {
+          id: 1,
+          username: 'existinguser',
+          nickname: 'Existing User',
+          email: 'existing@example.com',
+          phone: '1234567890',
+          name: 'Existing Name',
+          isDisabled: 0,
+          createdAt: '2024-01-01T00:00:00Z',
+          roles: [{ id: 1, name: 'admin' }],
+          avatarFile: null,
+        },
+      ],
+      pagination: {
+        total: 1,
+        page: 1,
+        pageSize: 10,
+      },
+    };
+
+    const { userClient } = await import('../../src/api/userClient');
+    const { toast } = await import('sonner');
+
+    // Mock initial user list
+    (userClient.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockUsers,
+    });
+
+    renderWithProviders(<UserManagement />);
+
+    // Wait for initial data to load
+    await waitFor(() => {
+      expect(screen.getByText('existinguser')).toBeInTheDocument();
+    });
+
+    // Test create user
+    const createButton = screen.getByText('创建用户');
+    fireEvent.click(createButton);
+
+    // Fill create form
+    const usernameInput = screen.getByPlaceholderText('请输入用户名');
+    const passwordInput = screen.getByPlaceholderText('请输入密码');
+    const emailInput = screen.getByPlaceholderText('请输入邮箱');
+
+    fireEvent.change(usernameInput, { target: { value: 'newuser' } });
+    fireEvent.change(passwordInput, { target: { value: 'password123' } });
+    fireEvent.change(emailInput, { target: { value: 'new@example.com' } });
+
+    // Mock successful creation
+    (userClient.$post as any).mockResolvedValue({
+      status: 201,
+    });
+
+    const submitButton = screen.getByText('创建用户');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(userClient.$post).toHaveBeenCalledWith({
+        json: {
+          username: 'newuser',
+          password: 'password123',
+          email: 'new@example.com',
+          nickname: undefined,
+          phone: null,
+          name: null,
+          isDisabled: 0,
+        },
+      });
+      expect(toast.success).toHaveBeenCalledWith('用户创建成功');
+    });
+
+    // Test edit user
+    const editButtons = screen.getAllByRole('button', { name: /edit/i });
+    fireEvent.click(editButtons[0]);
+
+    // Verify edit form is populated
+    await waitFor(() => {
+      expect(screen.getByDisplayValue('existinguser')).toBeInTheDocument();
+    });
+
+    // Update user
+    const updateUsernameInput = screen.getByDisplayValue('existinguser');
+    fireEvent.change(updateUsernameInput, { target: { value: 'updateduser' } });
+
+    // Mock successful update
+    (userClient[':id']['$put'] as any).mockResolvedValue({
+      status: 200,
+    });
+
+    const updateButton = screen.getByText('更新用户');
+    fireEvent.click(updateButton);
+
+    await waitFor(() => {
+      expect(userClient[':id']['$put']).toHaveBeenCalledWith({
+        param: { id: 1 },
+        json: {
+          username: 'updateduser',
+          nickname: 'Existing User',
+          email: 'existing@example.com',
+          phone: '1234567890',
+          name: 'Existing Name',
+          password: undefined,
+          avatarFileId: null,
+          isDisabled: 0,
+        },
+      });
+      expect(toast.success).toHaveBeenCalledWith('用户更新成功');
+    });
+
+    // Test delete user
+    const deleteButtons = screen.getAllByRole('button', { name: /trash/i });
+    fireEvent.click(deleteButtons[0]);
+
+    // Confirm deletion
+    expect(screen.getByText('确认删除')).toBeInTheDocument();
+
+    // Mock successful deletion
+    (userClient[':id']['$delete'] as any).mockResolvedValue({
+      status: 204,
+    });
+
+    const confirmDeleteButton = screen.getByText('删除');
+    fireEvent.click(confirmDeleteButton);
+
+    await waitFor(() => {
+      expect(userClient[':id']['$delete']).toHaveBeenCalledWith({
+        param: { id: 1 },
+      });
+      expect(toast.success).toHaveBeenCalledWith('用户删除成功');
+    });
+  });
+
+  it('should handle API errors gracefully', async () => {
+    const { userClient } = await import('../../src/api/userClient');
+    const { toast } = await import('sonner');
+
+    // Mock API error
+    (userClient.$get as any).mockRejectedValue(new Error('API Error'));
+
+    renderWithProviders(<UserManagement />);
+
+    // Should handle error without crashing
+    await waitFor(() => {
+      expect(screen.getByText('用户管理')).toBeInTheDocument();
+    });
+
+    // Test create user error
+    const createButton = screen.getByText('创建用户');
+    fireEvent.click(createButton);
+
+    const usernameInput = screen.getByPlaceholderText('请输入用户名');
+    const passwordInput = screen.getByPlaceholderText('请输入密码');
+
+    fireEvent.change(usernameInput, { target: { value: 'testuser' } });
+    fireEvent.change(passwordInput, { target: { value: 'password' } });
+
+    // Mock creation error
+    (userClient.$post as any).mockRejectedValue(new Error('Creation failed'));
+
+    const submitButton = screen.getByText('创建用户');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(toast.error).toHaveBeenCalledWith('创建失败,请重试');
+    });
+  });
+
+  it('should handle search and filter integration', async () => {
+    const { userClient } = await import('../../src/api/userClient');
+
+    const mockUsers = {
+      data: [],
+      pagination: { total: 0, page: 1, pageSize: 10 },
+    };
+
+    (userClient.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockUsers,
+    });
+
+    renderWithProviders(<UserManagement />);
+
+    // Test search
+    const searchInput = screen.getByPlaceholderText('搜索用户名、昵称或邮箱...');
+    fireEvent.change(searchInput, { target: { value: 'searchterm' } });
+
+    await waitFor(() => {
+      expect(userClient.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: 'searchterm',
+          filters: undefined,
+        },
+      });
+    });
+
+    // Test filter
+    const filterButton = screen.getByText('高级筛选');
+    fireEvent.click(filterButton);
+
+    // Apply status filter
+    const statusSelect = screen.getByText('选择状态');
+    fireEvent.click(statusSelect);
+
+    const enabledOption = screen.getByText('启用');
+    fireEvent.click(enabledOption);
+
+    await waitFor(() => {
+      expect(userClient.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: 'searchterm',
+          filters: expect.stringContaining('isDisabled'),
+        },
+      });
+    });
+  });
+});

+ 30 - 0
packages/user-management-ui/tests/setup.ts

@@ -0,0 +1,30 @@
+import '@testing-library/jest-dom';
+
+// Mock window.matchMedia
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: vi.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: vi.fn(), // deprecated
+    removeListener: vi.fn(), // deprecated
+    addEventListener: vi.fn(),
+    removeEventListener: vi.fn(),
+    dispatchEvent: vi.fn(),
+  })),
+});
+
+// Mock ResizeObserver
+global.ResizeObserver = vi.fn().mockImplementation(() => ({
+  observe: vi.fn(),
+  unobserve: vi.fn(),
+  disconnect: vi.fn(),
+}));
+
+// Mock IntersectionObserver
+global.IntersectionObserver = vi.fn().mockImplementation(() => ({
+  observe: vi.fn(),
+  unobserve: vi.fn(),
+  disconnect: vi.fn(),
+}));

+ 165 - 0
packages/user-management-ui/tests/unit/UserManagement.test.tsx

@@ -0,0 +1,165 @@
+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 { UserManagement } from '../../src/components/UserManagement';
+
+// Mock API client
+vi.mock('../../src/api/userClient', () => ({
+  userClient: {
+    $get: vi.fn(),
+    $post: vi.fn(),
+    ':id': {
+      $put: vi.fn(),
+      $delete: vi.fn(),
+    },
+  },
+}));
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+  },
+}));
+
+const createTestQueryClient = () =>
+  new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+const renderWithProviders = (component: React.ReactElement) => {
+  const queryClient = createTestQueryClient();
+  return render(
+    <QueryClientProvider client={queryClient}>
+      {component}
+    </QueryClientProvider>
+  );
+};
+
+describe('UserManagement', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('should render user management page', () => {
+    renderWithProviders(<UserManagement />);
+
+    expect(screen.getByText('用户管理')).toBeInTheDocument();
+    expect(screen.getByText('创建用户')).toBeInTheDocument();
+  });
+
+  it('should display user list when data is loaded', async () => {
+    const mockUsers = {
+      data: [
+        {
+          id: 1,
+          username: 'testuser',
+          nickname: 'Test User',
+          email: 'test@example.com',
+          phone: '1234567890',
+          name: 'Test Name',
+          isDisabled: 0,
+          createdAt: '2024-01-01T00:00:00Z',
+          roles: [{ id: 1, name: 'admin' }],
+          avatarFile: null,
+        },
+      ],
+      pagination: {
+        total: 1,
+        page: 1,
+        pageSize: 10,
+      },
+    };
+
+    const { userClient } = await import('../../src/api/userClient');
+    (userClient.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockUsers,
+    });
+
+    renderWithProviders(<UserManagement />);
+
+    await waitFor(() => {
+      expect(screen.getByText('testuser')).toBeInTheDocument();
+      expect(screen.getByText('Test User')).toBeInTheDocument();
+      expect(screen.getByText('test@example.com')).toBeInTheDocument();
+    });
+  });
+
+  it('should open create user modal when create button is clicked', () => {
+    renderWithProviders(<UserManagement />);
+
+    const createButton = screen.getByText('创建用户');
+    fireEvent.click(createButton);
+
+    expect(screen.getByText('创建用户')).toBeInTheDocument();
+    expect(screen.getByPlaceholderText('请输入用户名')).toBeInTheDocument();
+  });
+
+  it('should handle search functionality', async () => {
+    const { userClient } = await import('../../src/api/userClient');
+    (userClient.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => ({
+        data: [],
+        pagination: { total: 0, page: 1, pageSize: 10 },
+      }),
+    });
+
+    renderWithProviders(<UserManagement />);
+
+    const searchInput = screen.getByPlaceholderText('搜索用户名、昵称或邮箱...');
+    fireEvent.change(searchInput, { target: { value: 'test' } });
+
+    await waitFor(() => {
+      expect(userClient.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: 'test',
+          filters: undefined,
+        },
+      });
+    });
+  });
+
+  it('should handle filter functionality', async () => {
+    const { userClient } = await import('../../src/api/userClient');
+    (userClient.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => ({
+        data: [],
+        pagination: { total: 0, page: 1, pageSize: 10 },
+      }),
+    });
+
+    renderWithProviders(<UserManagement />);
+
+    // Open filters
+    const filterButton = screen.getByText('高级筛选');
+    fireEvent.click(filterButton);
+
+    // Select status filter
+    const statusSelect = screen.getByText('选择状态');
+    fireEvent.click(statusSelect);
+
+    const enabledOption = screen.getByText('启用');
+    fireEvent.click(enabledOption);
+
+    await waitFor(() => {
+      expect(userClient.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: '',
+          filters: expect.any(String),
+        },
+      });
+    });
+  });
+});

+ 33 - 0
packages/user-management-ui/tsconfig.json

@@ -0,0 +1,33 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "outDir": "./dist",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": [
+    "src/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}

+ 24 - 0
packages/user-management-ui/vitest.config.ts

@@ -0,0 +1,24 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    setupFiles: ['./tests/setup.ts'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'node_modules/',
+        'tests/',
+        '**/*.d.ts',
+        '**/*.config.*'
+      ]
+    }
+  },
+  resolve: {
+    alias: {
+      '@': './src'
+    }
+  }
+});

+ 103 - 0
pnpm-lock.yaml

@@ -1943,6 +1943,9 @@ importers:
       react-hook-form:
         specifier: ^7.61.1
         version: 7.65.0(react@19.2.0)
+      react-router-dom:
+        specifier: ^7.7.0
+        version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
       sonner:
         specifier: ^2.0.7
         version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -2048,6 +2051,106 @@ importers:
         specifier: ^3.2.4
         version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
 
+  packages/user-management-ui:
+    dependencies:
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@d8d/shared-ui-components':
+        specifier: workspace:*
+        version: link:../shared-ui-components
+      '@d8d/user-module':
+        specifier: workspace:*
+        version: link:../user-module
+      '@hookform/resolvers':
+        specifier: ^5.2.1
+        version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
+      '@tanstack/react-query':
+        specifier: ^5.83.0
+        version: 5.90.5(react@19.2.0)
+      axios:
+        specifier: ^1.7.9
+        version: 1.12.2(debug@4.4.3)
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      date-fns:
+        specifier: ^4.1.0
+        version: 4.1.0
+      dayjs:
+        specifier: ^1.11.13
+        version: 1.11.18
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      lucide-react:
+        specifier: ^0.536.0
+        version: 0.536.0(react@19.2.0)
+      react:
+        specifier: ^19.1.0
+        version: 19.2.0
+      react-dom:
+        specifier: ^19.1.0
+        version: 19.2.0(react@19.2.0)
+      react-hook-form:
+        specifier: ^7.61.1
+        version: 7.65.0(react@19.2.0)
+      react-router:
+        specifier: ^7.1.3
+        version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      sonner:
+        specifier: ^2.0.7
+        version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      tailwind-merge:
+        specifier: ^3.3.1
+        version: 3.3.1
+      zod:
+        specifier: ^4.0.15
+        version: 4.1.12
+    devDependencies:
+      '@testing-library/jest-dom':
+        specifier: ^6.8.0
+        version: 6.9.1
+      '@testing-library/react':
+        specifier: ^16.3.0
+        version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@testing-library/user-event':
+        specifier: ^14.6.1
+        version: 14.6.1(@testing-library/dom@10.4.1)
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.0
+      '@types/react':
+        specifier: ^19.1.8
+        version: 19.2.2
+      '@types/react-dom':
+        specifier: ^19.1.6
+        version: 19.2.2(@types/react@19.2.2)
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.38.0(jiti@2.6.1)
+      jsdom:
+        specifier: ^26.0.0
+        version: 26.1.0
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      unbuild:
+        specifier: ^3.4.0
+        version: 3.6.1(sass@1.93.2)(typescript@5.8.3)(vue@3.5.22(typescript@5.8.3))
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
   packages/user-module:
     dependencies:
       '@d8d/auth-module':