UserManagement.tsx 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. import React, { useState, useMemo, useCallback } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { format } from 'date-fns';
  4. import { Plus, Search, Edit, Trash2, Filter, X } from 'lucide-react';
  5. import { userClient, userClientManager } from '../api/userClient';
  6. import type { InferRequestType, InferResponseType } from 'hono/client';
  7. import { z } from 'zod';
  8. import { Button } from '@d8d/shared-ui-components/components/ui/button';
  9. import { Input } from '@d8d/shared-ui-components/components/ui/input';
  10. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
  11. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
  12. import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
  13. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
  14. import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
  15. import { useForm } from 'react-hook-form';
  16. import { zodResolver } from '@hookform/resolvers/zod';
  17. import { toast } from 'sonner';
  18. import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
  19. import { Switch } from '@d8d/shared-ui-components/components/ui/switch';
  20. import { CreateUserDtoMt, UpdateUserDtoMt, RoleSchemaMt } from '@d8d/user-module-mt/schemas';
  21. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
  22. import { Popover, PopoverContent, PopoverTrigger } from '@d8d/shared-ui-components/components/ui/popover';
  23. import { Calendar } from '@d8d/shared-ui-components/components/ui/calendar';
  24. import { cn } from '@d8d/shared-ui-components/utils/cn';
  25. import { DisabledStatus } from '@d8d/shared-types';
  26. // 使用RPC方式提取类型
  27. type CreateUserRequest = InferRequestType<typeof userClient.index.$post>['json'];
  28. type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json'];
  29. type UserResponse = InferResponseType<typeof userClient.index.$get, 200>['data'][0];
  30. // 直接使用后端定义的 schema
  31. const createUserFormSchema = CreateUserDtoMt;
  32. const updateUserFormSchema = UpdateUserDtoMt;
  33. type CreateUserFormData = CreateUserRequest;
  34. type UpdateUserFormData = UpdateUserRequest;
  35. // 适配后端返回的字符串日期格式
  36. type Role = Omit<z.infer<typeof RoleSchemaMt>, 'createdAt' | 'updatedAt'> & {
  37. createdAt: string;
  38. updatedAt: string;
  39. };
  40. export const UserManagement = () => {
  41. const [searchParams, setSearchParams] = useState({
  42. page: 1,
  43. limit: 10,
  44. keyword: ''
  45. });
  46. const [filters, setFilters] = useState({
  47. isDisabled: undefined as number | undefined,
  48. roleIds: [] as number[],
  49. createdAt: undefined as { gte?: string; lte?: string } | undefined
  50. });
  51. const [showFilters, setShowFilters] = useState(false);
  52. const [isModalOpen, setIsModalOpen] = useState(false);
  53. const [editingUser, setEditingUser] = useState<UserResponse | null>(null);
  54. const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  55. const [userToDelete, setUserToDelete] = useState<number | null>(null);
  56. const [isCreateForm, setIsCreateForm] = useState(true);
  57. const createForm = useForm<CreateUserFormData>({
  58. resolver: zodResolver(createUserFormSchema),
  59. defaultValues: {
  60. username: '',
  61. nickname: null,
  62. email: null,
  63. phone: null,
  64. name: null,
  65. password: '',
  66. isDisabled: DisabledStatus.ENABLED,
  67. },
  68. });
  69. const updateForm = useForm<UpdateUserFormData>({
  70. resolver: zodResolver(updateUserFormSchema),
  71. defaultValues: {
  72. username: '',
  73. nickname: null,
  74. email: null,
  75. phone: null,
  76. name: null,
  77. password: '',
  78. isDisabled: 0,
  79. },
  80. });
  81. const { data: usersData, isLoading, refetch } = useQuery({
  82. queryKey: ['users', searchParams, filters],
  83. queryFn: async () => {
  84. const filterParams: Record<string, unknown> = {};
  85. if (filters.isDisabled !== undefined) {
  86. filterParams.isDisabled = filters.isDisabled;
  87. }
  88. if (filters.roleIds.length > 0) {
  89. filterParams['roles.id'] = filters.roleIds;
  90. }
  91. if (filters.createdAt) {
  92. filterParams.createdAt = filters.createdAt;
  93. }
  94. const res = await userClientManager.get().index.$get({
  95. query: {
  96. page: searchParams.page,
  97. pageSize: searchParams.limit,
  98. keyword: searchParams.keyword,
  99. filters: Object.keys(filterParams).length > 0 ? JSON.stringify(filterParams) : undefined
  100. }
  101. });
  102. if (res.status !== 200) {
  103. throw new Error('获取用户列表失败');
  104. }
  105. return await res.json();
  106. }
  107. });
  108. const users = usersData?.data || [];
  109. const totalCount = usersData?.pagination?.total || 0;
  110. // 防抖搜索函数
  111. const debounce = (func: Function, delay: number) => {
  112. let timeoutId: NodeJS.Timeout;
  113. return (...args: any[]) => {
  114. clearTimeout(timeoutId);
  115. timeoutId = setTimeout(() => func(...args), delay);
  116. };
  117. };
  118. // 使用useCallback包装防抖搜索
  119. const debouncedSearch = useCallback(
  120. debounce((keyword: string) => {
  121. setSearchParams(prev => ({ ...prev, keyword, page: 1 }));
  122. }, 300),
  123. []
  124. );
  125. // 处理搜索输入变化
  126. const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  127. const keyword = e.target.value;
  128. setSearchParams(prev => ({ ...prev, keyword }));
  129. debouncedSearch(keyword);
  130. };
  131. // 处理搜索表单提交
  132. const handleSearch = (e: React.FormEvent) => {
  133. e.preventDefault();
  134. setSearchParams(prev => ({ ...prev, page: 1 }));
  135. };
  136. // 处理分页
  137. const handlePageChange = (page: number, limit: number) => {
  138. setSearchParams(prev => ({ ...prev, page, limit }));
  139. };
  140. // 处理过滤条件变化
  141. const handleFilterChange = (newFilters: Partial<typeof filters>) => {
  142. setFilters(prev => ({ ...prev, ...newFilters }));
  143. setSearchParams(prev => ({ ...prev, page: 1 }));
  144. };
  145. // 重置所有过滤条件
  146. const resetFilters = () => {
  147. setFilters({
  148. isDisabled: undefined,
  149. roleIds: [],
  150. createdAt: undefined
  151. });
  152. setSearchParams(prev => ({ ...prev, page: 1 }));
  153. };
  154. // 检查是否有活跃的过滤条件
  155. const hasActiveFilters = useMemo(() => {
  156. return filters.isDisabled !== undefined ||
  157. filters.roleIds.length > 0 ||
  158. filters.createdAt !== undefined;
  159. }, [filters]);
  160. // 打开创建用户对话框
  161. const handleCreateUser = () => {
  162. setEditingUser(null);
  163. setIsCreateForm(true);
  164. createForm.reset({
  165. username: '',
  166. nickname: null,
  167. email: null,
  168. phone: null,
  169. name: null,
  170. password: '',
  171. isDisabled: DisabledStatus.ENABLED,
  172. });
  173. setIsModalOpen(true);
  174. };
  175. // 打开编辑用户对话框
  176. const handleEditUser = (user: UserResponse) => {
  177. setEditingUser(user);
  178. setIsCreateForm(false);
  179. updateForm.reset({
  180. username: user.username,
  181. nickname: user.nickname,
  182. email: user.email,
  183. phone: user.phone,
  184. name: user.name,
  185. avatarFileId: user.avatarFileId,
  186. isDisabled: user.isDisabled,
  187. });
  188. setIsModalOpen(true);
  189. };
  190. // 处理创建表单提交
  191. const handleCreateSubmit = async (data: CreateUserFormData) => {
  192. try {
  193. const res = await userClientManager.get().index.$post({
  194. json: data
  195. });
  196. if (res.status !== 201) {
  197. throw new Error('创建用户失败');
  198. }
  199. toast.success('用户创建成功');
  200. setIsModalOpen(false);
  201. refetch();
  202. } catch {
  203. toast.error('创建失败,请重试');
  204. }
  205. };
  206. // 处理更新表单提交
  207. const handleUpdateSubmit = async (data: UpdateUserFormData) => {
  208. if (!editingUser) return;
  209. try {
  210. const res = await userClientManager.get()[':id']['$put']({
  211. param: { id: editingUser.id },
  212. json: data
  213. });
  214. if (res.status !== 200) {
  215. throw new Error('更新用户失败');
  216. }
  217. toast.success('用户更新成功');
  218. setIsModalOpen(false);
  219. refetch();
  220. } catch {
  221. toast.error('更新失败,请重试');
  222. }
  223. };
  224. // 处理删除用户
  225. const handleDeleteUser = (id: number) => {
  226. setUserToDelete(id);
  227. setDeleteDialogOpen(true);
  228. };
  229. const confirmDelete = async () => {
  230. if (!userToDelete) return;
  231. try {
  232. const res = await userClientManager.get()[':id']['$delete']({
  233. param: { id: userToDelete }
  234. });
  235. if (res.status !== 204) {
  236. throw new Error('删除用户失败');
  237. }
  238. toast.success('用户删除成功');
  239. refetch();
  240. } catch {
  241. toast.error('删除失败,请重试');
  242. } finally {
  243. setDeleteDialogOpen(false);
  244. setUserToDelete(null);
  245. }
  246. };
  247. // 渲染表格部分的骨架屏
  248. const renderTableSkeleton = () => (
  249. <div className="space-y-2">
  250. {Array.from({ length: 5 }).map((_, index) => (
  251. <div key={index} className="flex space-x-4">
  252. <Skeleton className="h-4 flex-1" />
  253. <Skeleton className="h-4 flex-1" />
  254. <Skeleton className="h-4 flex-1" />
  255. <Skeleton className="h-4 flex-1" />
  256. <Skeleton className="h-4 flex-1" />
  257. <Skeleton className="h-4 flex-1" />
  258. <Skeleton className="h-4 flex-1" />
  259. <Skeleton className="h-4 w-16" />
  260. </div>
  261. ))}
  262. </div>
  263. );
  264. return (
  265. <div className="space-y-4">
  266. <div className="flex justify-between items-center">
  267. <h1 className="text-2xl font-bold">用户管理</h1>
  268. <Button onClick={handleCreateUser} data-testid="create-user-button">
  269. <Plus className="mr-2 h-4 w-4" />
  270. 创建用户
  271. </Button>
  272. </div>
  273. <Card>
  274. <CardHeader>
  275. <CardTitle>用户列表</CardTitle>
  276. <CardDescription>
  277. 管理系统中的所有用户,共 {totalCount} 位用户
  278. </CardDescription>
  279. </CardHeader>
  280. <CardContent>
  281. <div className="mb-4 space-y-4">
  282. <form onSubmit={handleSearch} className="flex gap-2">
  283. <div className="relative flex-1 max-w-sm">
  284. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  285. <Input
  286. placeholder="搜索用户名、昵称或邮箱..."
  287. value={searchParams.keyword || ''}
  288. onChange={handleSearchChange}
  289. className="pl-8"
  290. />
  291. </div>
  292. <Button type="submit" variant="outline">
  293. 搜索
  294. </Button>
  295. <Button
  296. type="button"
  297. variant="outline"
  298. onClick={() => setShowFilters(!showFilters)}
  299. className="flex items-center gap-2"
  300. data-testid="advanced-filter-button"
  301. >
  302. <Filter className="h-4 w-4" />
  303. 高级筛选
  304. {hasActiveFilters && (
  305. <Badge variant="secondary" className="ml-1">
  306. {Object.values(filters).filter(v =>
  307. v !== undefined &&
  308. (!Array.isArray(v) || v.length > 0)
  309. ).length}
  310. </Badge>
  311. )}
  312. </Button>
  313. {hasActiveFilters && (
  314. <Button
  315. type="button"
  316. variant="ghost"
  317. onClick={resetFilters}
  318. className="flex items-center gap-2"
  319. >
  320. <X className="h-4 w-4" />
  321. 重置
  322. </Button>
  323. )}
  324. </form>
  325. {showFilters && (
  326. <div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 border rounded-lg bg-muted/50">
  327. {/* 状态筛选 */}
  328. <div className="space-y-2">
  329. <label className="text-sm font-medium">用户状态</label>
  330. <Select
  331. value={filters.isDisabled === undefined ? 'all' : filters.isDisabled.toString()}
  332. onValueChange={(value) =>
  333. handleFilterChange({
  334. isDisabled: value === 'all' ? undefined : parseInt(value)
  335. })
  336. }
  337. >
  338. <SelectTrigger data-testid="status-filter-trigger">
  339. <SelectValue placeholder="选择状态" />
  340. </SelectTrigger>
  341. <SelectContent>
  342. <SelectItem value="all" data-testid="status-all-option">全部状态</SelectItem>
  343. <SelectItem value="0" data-testid="status-enabled-option">启用</SelectItem>
  344. <SelectItem value="1" data-testid="status-disabled-option">禁用</SelectItem>
  345. </SelectContent>
  346. </Select>
  347. </div>
  348. {/* 角色筛选 */}
  349. <div className="space-y-2">
  350. <label className="text-sm font-medium">用户角色</label>
  351. <Select
  352. value=""
  353. onValueChange={(value) => {
  354. const roleId = parseInt(value);
  355. if (!filters.roleIds.includes(roleId)) {
  356. handleFilterChange({
  357. roleIds: [...filters.roleIds, roleId]
  358. });
  359. }
  360. }}
  361. >
  362. <SelectTrigger>
  363. <SelectValue placeholder="选择角色" />
  364. </SelectTrigger>
  365. <SelectContent>
  366. <SelectItem value="1">管理员</SelectItem>
  367. <SelectItem value="2">普通用户</SelectItem>
  368. </SelectContent>
  369. </Select>
  370. {filters.roleIds.length > 0 && (
  371. <div className="flex flex-wrap gap-2 mt-2">
  372. {filters.roleIds.map(roleId => (
  373. <Badge
  374. key={roleId}
  375. variant="secondary"
  376. className="flex items-center gap-1"
  377. >
  378. {roleId === 1 ? '管理员' : '普通用户'}
  379. <X
  380. className="h-3 w-3 cursor-pointer"
  381. onClick={() => handleFilterChange({
  382. roleIds: filters.roleIds.filter(id => id !== roleId)
  383. })}
  384. />
  385. </Badge>
  386. ))}
  387. </div>
  388. )}
  389. </div>
  390. {/* 创建时间筛选 */}
  391. <div className="space-y-2">
  392. <label className="text-sm font-medium">创建时间</label>
  393. <Popover>
  394. <PopoverTrigger asChild>
  395. <Button
  396. variant="outline"
  397. className={cn(
  398. "w-full justify-start text-left font-normal",
  399. !filters.createdAt && "text-muted-foreground"
  400. )}
  401. >
  402. {filters.createdAt ?
  403. `${filters.createdAt.gte || ''} 至 ${filters.createdAt.lte || ''}` :
  404. '选择日期范围'
  405. }
  406. </Button>
  407. </PopoverTrigger>
  408. <PopoverContent className="w-auto p-0" align="start">
  409. <Calendar
  410. mode="range"
  411. selected={{
  412. from: filters.createdAt?.gte ? new Date(filters.createdAt.gte) : undefined,
  413. to: filters.createdAt?.lte ? new Date(filters.createdAt.lte) : undefined
  414. }}
  415. onSelect={(range) => {
  416. handleFilterChange({
  417. createdAt: range?.from && range?.to ? {
  418. gte: format(range.from, 'yyyy-MM-dd'),
  419. lte: format(range.to, 'yyyy-MM-dd')
  420. } : undefined
  421. });
  422. }}
  423. initialFocus
  424. />
  425. </PopoverContent>
  426. </Popover>
  427. </div>
  428. </div>
  429. )}
  430. {/* 过滤条件标签 */}
  431. {hasActiveFilters && (
  432. <div className="flex flex-wrap gap-2">
  433. {filters.isDisabled !== undefined && (
  434. <Badge variant="secondary" className="flex items-center gap-1">
  435. 状态: {filters.isDisabled === 0 ? '启用' : '禁用'}
  436. <X
  437. className="h-3 w-3 cursor-pointer"
  438. onClick={() => handleFilterChange({ isDisabled: undefined })}
  439. />
  440. </Badge>
  441. )}
  442. {filters.roleIds.map(roleId => (
  443. <Badge key={roleId} variant="secondary" className="flex items-center gap-1">
  444. 角色: {roleId === 1 ? '管理员' : '普通用户'}
  445. <X
  446. className="h-3 w-3 cursor-pointer"
  447. onClick={() => handleFilterChange({
  448. roleIds: filters.roleIds.filter(id => id !== roleId)
  449. })}
  450. />
  451. </Badge>
  452. ))}
  453. {filters.createdAt && (
  454. <Badge variant="secondary" className="flex items-center gap-1">
  455. 创建时间: {filters.createdAt.gte || ''} 至 {filters.createdAt.lte || ''}
  456. <X
  457. className="h-3 w-3 cursor-pointer"
  458. onClick={() => handleFilterChange({ createdAt: undefined })}
  459. />
  460. </Badge>
  461. )}
  462. </div>
  463. )}
  464. </div>
  465. <div className="rounded-md border">
  466. <Table>
  467. <TableHeader>
  468. <TableRow>
  469. <TableHead>头像</TableHead>
  470. <TableHead>用户名</TableHead>
  471. <TableHead>昵称</TableHead>
  472. <TableHead>邮箱</TableHead>
  473. <TableHead>真实姓名</TableHead>
  474. <TableHead>角色</TableHead>
  475. <TableHead>状态</TableHead>
  476. <TableHead>创建时间</TableHead>
  477. <TableHead className="text-right">操作</TableHead>
  478. </TableRow>
  479. </TableHeader>
  480. <TableBody>
  481. {isLoading ? (
  482. // 显示表格骨架屏
  483. <TableRow>
  484. <TableCell colSpan={8} className="p-4">
  485. {renderTableSkeleton()}
  486. </TableCell>
  487. </TableRow>
  488. ) : (
  489. // 显示实际用户数据
  490. users.map((user) => (
  491. <TableRow key={user.id}>
  492. <TableCell>
  493. <div className="w-10 h-10">
  494. {user.avatarFile?.fullUrl ? (
  495. <img
  496. src={user.avatarFile.fullUrl}
  497. alt={user.username}
  498. className="w-10 h-10 rounded-full object-cover"
  499. />
  500. ) : (
  501. <div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
  502. <span className="text-sm font-medium text-gray-500">
  503. {user.username?.charAt(0)?.toUpperCase() || 'U'}
  504. </span>
  505. </div>
  506. )}
  507. </div>
  508. </TableCell>
  509. <TableCell className="font-medium">{user.username}</TableCell>
  510. <TableCell>{user.nickname || '-'}</TableCell>
  511. <TableCell>{user.email || '-'}</TableCell>
  512. <TableCell>{user.name || '-'}</TableCell>
  513. <TableCell>
  514. <Badge
  515. variant={user.roles?.some((role) => role.name === 'admin') ? 'destructive' : 'default'}
  516. className="capitalize"
  517. >
  518. {user.roles?.some((role) => role.name === 'admin') ? '管理员' : '普通用户'}
  519. </Badge>
  520. </TableCell>
  521. <TableCell>
  522. <Badge
  523. variant={user.isDisabled === 1 ? 'secondary' : 'default'}
  524. >
  525. {user.isDisabled === 1 ? '禁用' : '启用'}
  526. </Badge>
  527. </TableCell>
  528. <TableCell>
  529. {format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm')}
  530. </TableCell>
  531. <TableCell className="text-right">
  532. <div className="flex justify-end gap-2">
  533. <Button
  534. variant="ghost"
  535. size="icon"
  536. onClick={() => handleEditUser(user)}
  537. aria-label="编辑用户"
  538. >
  539. <Edit className="h-4 w-4" />
  540. </Button>
  541. <Button
  542. variant="ghost"
  543. size="icon"
  544. onClick={() => handleDeleteUser(user.id)}
  545. aria-label="删除用户"
  546. >
  547. <Trash2 className="h-4 w-4" />
  548. </Button>
  549. </div>
  550. </TableCell>
  551. </TableRow>
  552. ))
  553. )}
  554. </TableBody>
  555. </Table>
  556. </div>
  557. {/* 分页组件 - 需要外部提供或创建简化版本 */}
  558. <div className="mt-4 flex items-center justify-between">
  559. <div className="text-sm text-muted-foreground">
  560. 第 {searchParams.page} 页,共 {Math.ceil(totalCount / searchParams.limit)} 页
  561. </div>
  562. <div className="flex gap-2">
  563. <Button
  564. variant="outline"
  565. size="sm"
  566. disabled={searchParams.page <= 1}
  567. onClick={() => handlePageChange(searchParams.page - 1, searchParams.limit)}
  568. >
  569. 上一页
  570. </Button>
  571. <Button
  572. variant="outline"
  573. size="sm"
  574. disabled={searchParams.page >= Math.ceil(totalCount / searchParams.limit)}
  575. onClick={() => handlePageChange(searchParams.page + 1, searchParams.limit)}
  576. >
  577. 下一页
  578. </Button>
  579. </div>
  580. </div>
  581. </CardContent>
  582. </Card>
  583. {/* 创建/编辑用户对话框 */}
  584. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  585. <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
  586. <DialogHeader>
  587. <DialogTitle>
  588. {editingUser ? '编辑用户' : '创建用户'}
  589. </DialogTitle>
  590. <DialogDescription>
  591. {editingUser ? '编辑现有用户信息' : '创建一个新的用户账户'}
  592. </DialogDescription>
  593. </DialogHeader>
  594. {isCreateForm ? (
  595. <Form {...createForm}>
  596. <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
  597. <FormField
  598. control={createForm.control}
  599. name="username"
  600. render={({ field }) => (
  601. <FormItem>
  602. <FormLabel className="flex items-center">
  603. 用户名
  604. <span className="text-red-500 ml-1">*</span>
  605. </FormLabel>
  606. <FormControl>
  607. <Input placeholder="请输入用户名" {...field} />
  608. </FormControl>
  609. <FormMessage />
  610. </FormItem>
  611. )}
  612. />
  613. <FormField
  614. control={createForm.control}
  615. name="nickname"
  616. render={({ field }) => (
  617. <FormItem>
  618. <FormLabel>昵称</FormLabel>
  619. <FormControl>
  620. <Input placeholder="请输入昵称" {...field} />
  621. </FormControl>
  622. <FormMessage />
  623. </FormItem>
  624. )}
  625. />
  626. <FormField
  627. control={createForm.control}
  628. name="email"
  629. render={({ field }) => (
  630. <FormItem>
  631. <FormLabel>邮箱</FormLabel>
  632. <FormControl>
  633. <Input type="email" placeholder="请输入邮箱" {...field} />
  634. </FormControl>
  635. <FormMessage />
  636. </FormItem>
  637. )}
  638. />
  639. <FormField
  640. control={createForm.control}
  641. name="phone"
  642. render={({ field }) => (
  643. <FormItem>
  644. <FormLabel>手机号</FormLabel>
  645. <FormControl>
  646. <Input placeholder="请输入手机号" {...field} />
  647. </FormControl>
  648. <FormMessage />
  649. </FormItem>
  650. )}
  651. />
  652. <FormField
  653. control={createForm.control}
  654. name="name"
  655. render={({ field }) => (
  656. <FormItem>
  657. <FormLabel>真实姓名</FormLabel>
  658. <FormControl>
  659. <Input placeholder="请输入真实姓名" {...field} />
  660. </FormControl>
  661. <FormMessage />
  662. </FormItem>
  663. )}
  664. />
  665. <FormField
  666. control={createForm.control}
  667. name="password"
  668. render={({ field }) => (
  669. <FormItem>
  670. <FormLabel className="flex items-center">
  671. 密码
  672. <span className="text-red-500 ml-1">*</span>
  673. </FormLabel>
  674. <FormControl>
  675. <Input type="password" placeholder="请输入密码" {...field} />
  676. </FormControl>
  677. <FormMessage />
  678. </FormItem>
  679. )}
  680. />
  681. <FormField
  682. control={createForm.control}
  683. name="isDisabled"
  684. render={({ field }) => (
  685. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  686. <div className="space-y-0.5">
  687. <FormLabel className="text-base">用户状态</FormLabel>
  688. <FormDescription>
  689. 禁用后用户将无法登录系统
  690. </FormDescription>
  691. </div>
  692. <FormControl>
  693. <Switch
  694. checked={field.value === 1}
  695. onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
  696. />
  697. </FormControl>
  698. </FormItem>
  699. )}
  700. />
  701. <DialogFooter>
  702. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  703. 取消
  704. </Button>
  705. <Button type="submit" data-testid="create-user-submit-button">
  706. 创建用户
  707. </Button>
  708. </DialogFooter>
  709. </form>
  710. </Form>
  711. ) : (
  712. <Form {...updateForm}>
  713. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  714. <FormField
  715. control={updateForm.control}
  716. name="username"
  717. render={({ field }) => (
  718. <FormItem>
  719. <FormLabel className="flex items-center">
  720. 用户名
  721. <span className="text-red-500 ml-1">*</span>
  722. </FormLabel>
  723. <FormControl>
  724. <Input placeholder="请输入用户名" {...field} />
  725. </FormControl>
  726. <FormMessage />
  727. </FormItem>
  728. )}
  729. />
  730. <FormField
  731. control={updateForm.control}
  732. name="nickname"
  733. render={({ field }) => (
  734. <FormItem>
  735. <FormLabel>昵称</FormLabel>
  736. <FormControl>
  737. <Input placeholder="请输入昵称" {...field} />
  738. </FormControl>
  739. <FormMessage />
  740. </FormItem>
  741. )}
  742. />
  743. <FormField
  744. control={updateForm.control}
  745. name="email"
  746. render={({ field }) => (
  747. <FormItem>
  748. <FormLabel>邮箱</FormLabel>
  749. <FormControl>
  750. <Input type="email" placeholder="请输入邮箱" {...field} />
  751. </FormControl>
  752. <FormMessage />
  753. </FormItem>
  754. )}
  755. />
  756. <FormField
  757. control={updateForm.control}
  758. name="phone"
  759. render={({ field }) => (
  760. <FormItem>
  761. <FormLabel>手机号</FormLabel>
  762. <FormControl>
  763. <Input placeholder="请输入手机号" {...field} />
  764. </FormControl>
  765. <FormMessage />
  766. </FormItem>
  767. )}
  768. />
  769. <FormField
  770. control={updateForm.control}
  771. name="name"
  772. render={({ field }) => (
  773. <FormItem>
  774. <FormLabel>真实姓名</FormLabel>
  775. <FormControl>
  776. <Input placeholder="请输入真实姓名" {...field} />
  777. </FormControl>
  778. <FormMessage />
  779. </FormItem>
  780. )}
  781. />
  782. <FormField
  783. control={updateForm.control}
  784. name="password"
  785. render={({ field }) => (
  786. <FormItem>
  787. <FormLabel>新密码</FormLabel>
  788. <FormControl>
  789. <Input type="password" placeholder="留空则不修改密码" {...field} />
  790. </FormControl>
  791. <FormMessage />
  792. </FormItem>
  793. )}
  794. />
  795. <FormField
  796. control={updateForm.control}
  797. name="isDisabled"
  798. render={({ field }) => (
  799. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  800. <div className="space-y-0.5">
  801. <FormLabel className="text-base">用户状态</FormLabel>
  802. <FormDescription>
  803. 禁用后用户将无法登录系统
  804. </FormDescription>
  805. </div>
  806. <FormControl>
  807. <Switch
  808. checked={field.value === 1}
  809. onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
  810. />
  811. </FormControl>
  812. </FormItem>
  813. )}
  814. />
  815. <DialogFooter>
  816. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  817. 取消
  818. </Button>
  819. <Button type="submit">
  820. 更新用户
  821. </Button>
  822. </DialogFooter>
  823. </form>
  824. </Form>
  825. )}
  826. </DialogContent>
  827. </Dialog>
  828. {/* 删除确认对话框 */}
  829. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  830. <DialogContent>
  831. <DialogHeader>
  832. <DialogTitle>确认删除</DialogTitle>
  833. <DialogDescription>
  834. 确定要删除这个用户吗?此操作无法撤销。
  835. </DialogDescription>
  836. </DialogHeader>
  837. <DialogFooter>
  838. <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
  839. 取消
  840. </Button>
  841. <Button variant="destructive" onClick={confirmDelete}>
  842. 删除
  843. </Button>
  844. </DialogFooter>
  845. </DialogContent>
  846. </Dialog>
  847. </div>
  848. );
  849. };