2
0

UserManagement.tsx 33 KB

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