Users.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. import React, { useState, useMemo } 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 handleSearch = (e: React.FormEvent) => {
  107. e.preventDefault();
  108. setSearchParams(prev => ({ ...prev, page: 1 }));
  109. };
  110. // 处理分页
  111. const handlePageChange = (page: number, limit: number) => {
  112. setSearchParams(prev => ({ ...prev, page, limit }));
  113. };
  114. // 处理过滤条件变化
  115. const handleFilterChange = (newFilters: Partial<typeof filters>) => {
  116. setFilters(prev => ({ ...prev, ...newFilters }));
  117. setSearchParams(prev => ({ ...prev, page: 1 }));
  118. };
  119. // 重置所有过滤条件
  120. const resetFilters = () => {
  121. setFilters({
  122. isDisabled: undefined,
  123. roleIds: [],
  124. createdAt: undefined
  125. });
  126. setSearchParams(prev => ({ ...prev, page: 1 }));
  127. };
  128. // 检查是否有活跃的过滤条件
  129. const hasActiveFilters = useMemo(() => {
  130. return filters.isDisabled !== undefined ||
  131. filters.roleIds.length > 0 ||
  132. filters.createdAt !== undefined;
  133. }, [filters]);
  134. // 打开创建用户对话框
  135. const handleCreateUser = () => {
  136. setEditingUser(null);
  137. setIsCreateForm(true);
  138. createForm.reset({
  139. username: '',
  140. nickname: null,
  141. email: null,
  142. phone: null,
  143. name: null,
  144. password: '',
  145. isDisabled: DisabledStatus.ENABLED,
  146. });
  147. setIsModalOpen(true);
  148. };
  149. // 打开编辑用户对话框
  150. const handleEditUser = (user: UserResponse) => {
  151. setEditingUser(user);
  152. setIsCreateForm(false);
  153. updateForm.reset({
  154. username: user.username,
  155. nickname: user.nickname,
  156. email: user.email,
  157. phone: user.phone,
  158. name: user.name,
  159. isDisabled: user.isDisabled,
  160. });
  161. setIsModalOpen(true);
  162. };
  163. // 处理创建表单提交
  164. const handleCreateSubmit = async (data: CreateUserFormData) => {
  165. try {
  166. const res = await userClient.$post({
  167. json: data
  168. });
  169. if (res.status !== 201) {
  170. throw new Error('创建用户失败');
  171. }
  172. toast.success('用户创建成功');
  173. setIsModalOpen(false);
  174. refetch();
  175. } catch {
  176. toast.error('创建失败,请重试');
  177. }
  178. };
  179. // 处理更新表单提交
  180. const handleUpdateSubmit = async (data: UpdateUserFormData) => {
  181. if (!editingUser) return;
  182. try {
  183. const res = await userClient[':id']['$put']({
  184. param: { id: editingUser.id },
  185. json: data
  186. });
  187. if (res.status !== 200) {
  188. throw new Error('更新用户失败');
  189. }
  190. toast.success('用户更新成功');
  191. setIsModalOpen(false);
  192. refetch();
  193. } catch {
  194. toast.error('更新失败,请重试');
  195. }
  196. };
  197. // 处理删除用户
  198. const handleDeleteUser = (id: number) => {
  199. setUserToDelete(id);
  200. setDeleteDialogOpen(true);
  201. };
  202. const confirmDelete = async () => {
  203. if (!userToDelete) return;
  204. try {
  205. const res = await userClient[':id']['$delete']({
  206. param: { id: userToDelete }
  207. });
  208. if (res.status !== 204) {
  209. throw new Error('删除用户失败');
  210. }
  211. toast.success('用户删除成功');
  212. refetch();
  213. } catch {
  214. toast.error('删除失败,请重试');
  215. } finally {
  216. setDeleteDialogOpen(false);
  217. setUserToDelete(null);
  218. }
  219. };
  220. // 渲染加载骨架
  221. if (isLoading) {
  222. return (
  223. <div className="space-y-4">
  224. <div className="flex justify-between items-center">
  225. <h1 className="text-2xl font-bold">用户管理</h1>
  226. <Button disabled>
  227. <Plus className="mr-2 h-4 w-4" />
  228. 创建用户
  229. </Button>
  230. </div>
  231. <Card>
  232. <CardHeader>
  233. <Skeleton className="h-6 w-1/4" />
  234. </CardHeader>
  235. <CardContent>
  236. <div className="space-y-2">
  237. <Skeleton className="h-4 w-full" />
  238. <Skeleton className="h-4 w-full" />
  239. <Skeleton className="h-4 w-full" />
  240. </div>
  241. </CardContent>
  242. </Card>
  243. </div>
  244. );
  245. }
  246. return (
  247. <div className="space-y-4">
  248. <div className="flex justify-between items-center">
  249. <h1 className="text-2xl font-bold">用户管理</h1>
  250. <Button onClick={handleCreateUser}>
  251. <Plus className="mr-2 h-4 w-4" />
  252. 创建用户
  253. </Button>
  254. </div>
  255. <Card>
  256. <CardHeader>
  257. <CardTitle>用户列表</CardTitle>
  258. <CardDescription>
  259. 管理系统中的所有用户,共 {totalCount} 位用户
  260. </CardDescription>
  261. </CardHeader>
  262. <CardContent>
  263. <div className="mb-4 space-y-4">
  264. <form onSubmit={handleSearch} className="flex gap-2">
  265. <div className="relative flex-1 max-w-sm">
  266. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  267. <Input
  268. placeholder="搜索用户名、昵称或邮箱..."
  269. value={searchParams.keyword}
  270. onChange={(e) => setSearchParams(prev => ({ ...prev, keyword: e.target.value }))}
  271. className="pl-8"
  272. />
  273. </div>
  274. <Button type="submit" variant="outline">
  275. 搜索
  276. </Button>
  277. <Button
  278. type="button"
  279. variant="outline"
  280. onClick={() => setShowFilters(!showFilters)}
  281. className="flex items-center gap-2"
  282. >
  283. <Filter className="h-4 w-4" />
  284. 高级筛选
  285. {hasActiveFilters && (
  286. <Badge variant="secondary" className="ml-1">
  287. {Object.values(filters).filter(v =>
  288. v !== undefined &&
  289. (!Array.isArray(v) || v.length > 0)
  290. ).length}
  291. </Badge>
  292. )}
  293. </Button>
  294. {hasActiveFilters && (
  295. <Button
  296. type="button"
  297. variant="ghost"
  298. onClick={resetFilters}
  299. className="flex items-center gap-2"
  300. >
  301. <X className="h-4 w-4" />
  302. 重置
  303. </Button>
  304. )}
  305. </form>
  306. {showFilters && (
  307. <div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 border rounded-lg bg-muted/50">
  308. {/* 状态筛选 */}
  309. <div className="space-y-2">
  310. <label className="text-sm font-medium">用户状态</label>
  311. <Select
  312. value={filters.isDisabled?.toString() || ''}
  313. onValueChange={(value) =>
  314. handleFilterChange({
  315. isDisabled: value === '' ? undefined : parseInt(value)
  316. })
  317. }
  318. >
  319. <SelectTrigger>
  320. <SelectValue placeholder="选择状态" />
  321. </SelectTrigger>
  322. <SelectContent>
  323. <SelectItem value="">全部状态</SelectItem>
  324. <SelectItem value="0">启用</SelectItem>
  325. <SelectItem value="1">禁用</SelectItem>
  326. </SelectContent>
  327. </Select>
  328. </div>
  329. {/* 角色筛选 */}
  330. <div className="space-y-2">
  331. <label className="text-sm font-medium">用户角色</label>
  332. <Select
  333. value=""
  334. onValueChange={(value) => {
  335. const roleId = parseInt(value);
  336. if (!filters.roleIds.includes(roleId)) {
  337. handleFilterChange({
  338. roleIds: [...filters.roleIds, roleId]
  339. });
  340. }
  341. }}
  342. >
  343. <SelectTrigger>
  344. <SelectValue placeholder="选择角色" />
  345. </SelectTrigger>
  346. <SelectContent>
  347. <SelectItem value="1">管理员</SelectItem>
  348. <SelectItem value="2">普通用户</SelectItem>
  349. </SelectContent>
  350. </Select>
  351. {filters.roleIds.length > 0 && (
  352. <div className="flex flex-wrap gap-2 mt-2">
  353. {filters.roleIds.map(roleId => (
  354. <Badge
  355. key={roleId}
  356. variant="secondary"
  357. className="flex items-center gap-1"
  358. >
  359. {roleId === 1 ? '管理员' : '普通用户'}
  360. <X
  361. className="h-3 w-3 cursor-pointer"
  362. onClick={() => handleFilterChange({
  363. roleIds: filters.roleIds.filter(id => id !== roleId)
  364. })}
  365. />
  366. </Badge>
  367. ))}
  368. </div>
  369. )}
  370. </div>
  371. {/* 创建时间筛选 */}
  372. <div className="space-y-2">
  373. <label className="text-sm font-medium">创建时间</label>
  374. <Popover>
  375. <PopoverTrigger asChild>
  376. <Button
  377. variant="outline"
  378. className={cn(
  379. "w-full justify-start text-left font-normal",
  380. !filters.createdAt && "text-muted-foreground"
  381. )}
  382. >
  383. {filters.createdAt ?
  384. `${filters.createdAt.gte || ''} 至 ${filters.createdAt.lte || ''}` :
  385. '选择日期范围'
  386. }
  387. </Button>
  388. </PopoverTrigger>
  389. <PopoverContent className="w-auto p-0" align="start">
  390. <Calendar
  391. mode="range"
  392. selected={{
  393. from: filters.createdAt?.gte ? new Date(filters.createdAt.gte) : undefined,
  394. to: filters.createdAt?.lte ? new Date(filters.createdAt.lte) : undefined
  395. }}
  396. onSelect={(range) => {
  397. handleFilterChange({
  398. createdAt: range?.from && range?.to ? {
  399. gte: format(range.from, 'yyyy-MM-dd'),
  400. lte: format(range.to, 'yyyy-MM-dd')
  401. } : undefined
  402. });
  403. }}
  404. initialFocus
  405. />
  406. </PopoverContent>
  407. </Popover>
  408. </div>
  409. </div>
  410. )}
  411. {/* 过滤条件标签 */}
  412. {hasActiveFilters && (
  413. <div className="flex flex-wrap gap-2">
  414. {filters.isDisabled !== undefined && (
  415. <Badge variant="secondary" className="flex items-center gap-1">
  416. 状态: {filters.isDisabled === 0 ? '启用' : '禁用'}
  417. <X
  418. className="h-3 w-3 cursor-pointer"
  419. onClick={() => handleFilterChange({ isDisabled: undefined })}
  420. />
  421. </Badge>
  422. )}
  423. {filters.roleIds.map(roleId => (
  424. <Badge key={roleId} variant="secondary" className="flex items-center gap-1">
  425. 角色: {roleId === 1 ? '管理员' : '普通用户'}
  426. <X
  427. className="h-3 w-3 cursor-pointer"
  428. onClick={() => handleFilterChange({
  429. roleIds: filters.roleIds.filter(id => id !== roleId)
  430. })}
  431. />
  432. </Badge>
  433. ))}
  434. {filters.createdAt && (
  435. <Badge variant="secondary" className="flex items-center gap-1">
  436. 创建时间: {filters.createdAt.gte || ''} 至 {filters.createdAt.lte || ''}
  437. <X
  438. className="h-3 w-3 cursor-pointer"
  439. onClick={() => handleFilterChange({ createdAt: undefined })}
  440. />
  441. </Badge>
  442. )}
  443. </div>
  444. )}
  445. </div>
  446. <div className="rounded-md border">
  447. <Table>
  448. <TableHeader>
  449. <TableRow>
  450. <TableHead>用户名</TableHead>
  451. <TableHead>昵称</TableHead>
  452. <TableHead>邮箱</TableHead>
  453. <TableHead>真实姓名</TableHead>
  454. <TableHead>角色</TableHead>
  455. <TableHead>状态</TableHead>
  456. <TableHead>创建时间</TableHead>
  457. <TableHead className="text-right">操作</TableHead>
  458. </TableRow>
  459. </TableHeader>
  460. <TableBody>
  461. {users.map((user) => (
  462. <TableRow key={user.id}>
  463. <TableCell className="font-medium">{user.username}</TableCell>
  464. <TableCell>{user.nickname || '-'}</TableCell>
  465. <TableCell>{user.email || '-'}</TableCell>
  466. <TableCell>{user.name || '-'}</TableCell>
  467. <TableCell>
  468. <Badge
  469. variant={user.roles?.some((role) => role.name === 'admin') ? 'destructive' : 'default'}
  470. className="capitalize"
  471. >
  472. {user.roles?.some((role) => role.name === 'admin') ? '管理员' : '普通用户'}
  473. </Badge>
  474. </TableCell>
  475. <TableCell>
  476. <Badge
  477. variant={user.isDisabled === 1 ? 'secondary' : 'default'}
  478. >
  479. {user.isDisabled === 1 ? '禁用' : '启用'}
  480. </Badge>
  481. </TableCell>
  482. <TableCell>
  483. {format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm')}
  484. </TableCell>
  485. <TableCell className="text-right">
  486. <div className="flex justify-end gap-2">
  487. <Button
  488. variant="ghost"
  489. size="icon"
  490. onClick={() => handleEditUser(user)}
  491. >
  492. <Edit className="h-4 w-4" />
  493. </Button>
  494. <Button
  495. variant="ghost"
  496. size="icon"
  497. onClick={() => handleDeleteUser(user.id)}
  498. >
  499. <Trash2 className="h-4 w-4" />
  500. </Button>
  501. </div>
  502. </TableCell>
  503. </TableRow>
  504. ))}
  505. </TableBody>
  506. </Table>
  507. </div>
  508. <DataTablePagination
  509. currentPage={searchParams.page}
  510. totalCount={totalCount}
  511. pageSize={searchParams.limit}
  512. onPageChange={handlePageChange}
  513. />
  514. </CardContent>
  515. </Card>
  516. {/* 创建/编辑用户对话框 */}
  517. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  518. <DialogContent className="sm:max-w-[500px]">
  519. <DialogHeader>
  520. <DialogTitle>
  521. {editingUser ? '编辑用户' : '创建用户'}
  522. </DialogTitle>
  523. <DialogDescription>
  524. {editingUser ? '编辑现有用户信息' : '创建一个新的用户账户'}
  525. </DialogDescription>
  526. </DialogHeader>
  527. {isCreateForm ? (
  528. <Form {...createForm}>
  529. <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
  530. <FormField
  531. control={createForm.control}
  532. name="username"
  533. render={({ field }) => (
  534. <FormItem>
  535. <FormLabel className="flex items-center">
  536. 用户名
  537. <span className="text-red-500 ml-1">*</span>
  538. </FormLabel>
  539. <FormControl>
  540. <Input placeholder="请输入用户名" {...field} />
  541. </FormControl>
  542. <FormMessage />
  543. </FormItem>
  544. )}
  545. />
  546. <FormField
  547. control={createForm.control}
  548. name="nickname"
  549. render={({ field }) => (
  550. <FormItem>
  551. <FormLabel>昵称</FormLabel>
  552. <FormControl>
  553. <Input placeholder="请输入昵称" {...field} />
  554. </FormControl>
  555. <FormMessage />
  556. </FormItem>
  557. )}
  558. />
  559. <FormField
  560. control={createForm.control}
  561. name="email"
  562. render={({ field }) => (
  563. <FormItem>
  564. <FormLabel>邮箱</FormLabel>
  565. <FormControl>
  566. <Input type="email" placeholder="请输入邮箱" {...field} />
  567. </FormControl>
  568. <FormMessage />
  569. </FormItem>
  570. )}
  571. />
  572. <FormField
  573. control={createForm.control}
  574. name="phone"
  575. render={({ field }) => (
  576. <FormItem>
  577. <FormLabel>手机号</FormLabel>
  578. <FormControl>
  579. <Input placeholder="请输入手机号" {...field} />
  580. </FormControl>
  581. <FormMessage />
  582. </FormItem>
  583. )}
  584. />
  585. <FormField
  586. control={createForm.control}
  587. name="name"
  588. render={({ field }) => (
  589. <FormItem>
  590. <FormLabel>真实姓名</FormLabel>
  591. <FormControl>
  592. <Input placeholder="请输入真实姓名" {...field} />
  593. </FormControl>
  594. <FormMessage />
  595. </FormItem>
  596. )}
  597. />
  598. <FormField
  599. control={createForm.control}
  600. name="password"
  601. render={({ field }) => (
  602. <FormItem>
  603. <FormLabel className="flex items-center">
  604. 密码
  605. <span className="text-red-500 ml-1">*</span>
  606. </FormLabel>
  607. <FormControl>
  608. <Input type="password" placeholder="请输入密码" {...field} />
  609. </FormControl>
  610. <FormMessage />
  611. </FormItem>
  612. )}
  613. />
  614. <FormField
  615. control={createForm.control}
  616. name="isDisabled"
  617. render={({ field }) => (
  618. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  619. <div className="space-y-0.5">
  620. <FormLabel className="text-base">用户状态</FormLabel>
  621. <FormDescription>
  622. 禁用后用户将无法登录系统
  623. </FormDescription>
  624. </div>
  625. <FormControl>
  626. <Switch
  627. checked={field.value === 1}
  628. onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
  629. />
  630. </FormControl>
  631. </FormItem>
  632. )}
  633. />
  634. <DialogFooter>
  635. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  636. 取消
  637. </Button>
  638. <Button type="submit">
  639. 创建用户
  640. </Button>
  641. </DialogFooter>
  642. </form>
  643. </Form>
  644. ) : (
  645. <Form {...updateForm}>
  646. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  647. <FormField
  648. control={updateForm.control}
  649. name="username"
  650. render={({ field }) => (
  651. <FormItem>
  652. <FormLabel className="flex items-center">
  653. 用户名
  654. <span className="text-red-500 ml-1">*</span>
  655. </FormLabel>
  656. <FormControl>
  657. <Input placeholder="请输入用户名" {...field} />
  658. </FormControl>
  659. <FormMessage />
  660. </FormItem>
  661. )}
  662. />
  663. <FormField
  664. control={updateForm.control}
  665. name="nickname"
  666. render={({ field }) => (
  667. <FormItem>
  668. <FormLabel>昵称</FormLabel>
  669. <FormControl>
  670. <Input placeholder="请输入昵称" {...field} />
  671. </FormControl>
  672. <FormMessage />
  673. </FormItem>
  674. )}
  675. />
  676. <FormField
  677. control={updateForm.control}
  678. name="email"
  679. render={({ field }) => (
  680. <FormItem>
  681. <FormLabel>邮箱</FormLabel>
  682. <FormControl>
  683. <Input type="email" placeholder="请输入邮箱" {...field} />
  684. </FormControl>
  685. <FormMessage />
  686. </FormItem>
  687. )}
  688. />
  689. <FormField
  690. control={updateForm.control}
  691. name="phone"
  692. render={({ field }) => (
  693. <FormItem>
  694. <FormLabel>手机号</FormLabel>
  695. <FormControl>
  696. <Input placeholder="请输入手机号" {...field} />
  697. </FormControl>
  698. <FormMessage />
  699. </FormItem>
  700. )}
  701. />
  702. <FormField
  703. control={updateForm.control}
  704. name="name"
  705. render={({ field }) => (
  706. <FormItem>
  707. <FormLabel>真实姓名</FormLabel>
  708. <FormControl>
  709. <Input placeholder="请输入真实姓名" {...field} />
  710. </FormControl>
  711. <FormMessage />
  712. </FormItem>
  713. )}
  714. />
  715. <FormField
  716. control={updateForm.control}
  717. name="password"
  718. render={({ field }) => (
  719. <FormItem>
  720. <FormLabel>新密码</FormLabel>
  721. <FormControl>
  722. <Input type="password" placeholder="留空则不修改密码" {...field} />
  723. </FormControl>
  724. <FormMessage />
  725. </FormItem>
  726. )}
  727. />
  728. <FormField
  729. control={updateForm.control}
  730. name="isDisabled"
  731. render={({ field }) => (
  732. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  733. <div className="space-y-0.5">
  734. <FormLabel className="text-base">用户状态</FormLabel>
  735. <FormDescription>
  736. 禁用后用户将无法登录系统
  737. </FormDescription>
  738. </div>
  739. <FormControl>
  740. <Switch
  741. checked={field.value === 1}
  742. onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
  743. />
  744. </FormControl>
  745. </FormItem>
  746. )}
  747. />
  748. <DialogFooter>
  749. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  750. 取消
  751. </Button>
  752. <Button type="submit">
  753. 更新用户
  754. </Button>
  755. </DialogFooter>
  756. </form>
  757. </Form>
  758. )}
  759. </DialogContent>
  760. </Dialog>
  761. {/* 删除确认对话框 */}
  762. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  763. <DialogContent>
  764. <DialogHeader>
  765. <DialogTitle>确认删除</DialogTitle>
  766. <DialogDescription>
  767. 确定要删除这个用户吗?此操作无法撤销。
  768. </DialogDescription>
  769. </DialogHeader>
  770. <DialogFooter>
  771. <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
  772. 取消
  773. </Button>
  774. <Button variant="destructive" onClick={confirmDelete}>
  775. 删除
  776. </Button>
  777. </DialogFooter>
  778. </DialogContent>
  779. </Dialog>
  780. </div>
  781. );
  782. };