Users.tsx 31 KB

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