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. if (isLoading) {
  243. return (
  244. <div className="space-y-4">
  245. <div className="flex justify-between items-center">
  246. <h1 className="text-2xl font-bold">用户管理</h1>
  247. <Button disabled>
  248. <Plus className="mr-2 h-4 w-4" />
  249. 创建用户
  250. </Button>
  251. </div>
  252. <Card>
  253. <CardHeader>
  254. <Skeleton className="h-6 w-1/4" />
  255. </CardHeader>
  256. <CardContent>
  257. <div className="space-y-2">
  258. <Skeleton className="h-4 w-full" />
  259. <Skeleton className="h-4 w-full" />
  260. <Skeleton className="h-4 w-full" />
  261. </div>
  262. </CardContent>
  263. </Card>
  264. </div>
  265. );
  266. }
  267. return (
  268. <div className="space-y-4">
  269. <div className="flex justify-between items-center">
  270. <h1 className="text-2xl font-bold">用户管理</h1>
  271. <Button onClick={handleCreateUser}>
  272. <Plus className="mr-2 h-4 w-4" />
  273. 创建用户
  274. </Button>
  275. </div>
  276. <Card>
  277. <CardHeader>
  278. <CardTitle>用户列表</CardTitle>
  279. <CardDescription>
  280. 管理系统中的所有用户,共 {totalCount} 位用户
  281. </CardDescription>
  282. </CardHeader>
  283. <CardContent>
  284. <div className="mb-4 space-y-4">
  285. <form onSubmit={handleSearch} className="flex gap-2">
  286. <div className="relative flex-1 max-w-sm">
  287. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  288. <Input
  289. placeholder="搜索用户名、昵称或邮箱..."
  290. value={searchParams.keyword}
  291. onChange={handleSearchChange}
  292. className="pl-8"
  293. />
  294. </div>
  295. <Button type="submit" variant="outline">
  296. 搜索
  297. </Button>
  298. <Button
  299. type="button"
  300. variant="outline"
  301. onClick={() => setShowFilters(!showFilters)}
  302. className="flex items-center gap-2"
  303. >
  304. <Filter className="h-4 w-4" />
  305. 高级筛选
  306. {hasActiveFilters && (
  307. <Badge variant="secondary" className="ml-1">
  308. {Object.values(filters).filter(v =>
  309. v !== undefined &&
  310. (!Array.isArray(v) || v.length > 0)
  311. ).length}
  312. </Badge>
  313. )}
  314. </Button>
  315. {hasActiveFilters && (
  316. <Button
  317. type="button"
  318. variant="ghost"
  319. onClick={resetFilters}
  320. className="flex items-center gap-2"
  321. >
  322. <X className="h-4 w-4" />
  323. 重置
  324. </Button>
  325. )}
  326. </form>
  327. {showFilters && (
  328. <div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-4 border rounded-lg bg-muted/50">
  329. {/* 状态筛选 */}
  330. <div className="space-y-2">
  331. <label className="text-sm font-medium">用户状态</label>
  332. <Select
  333. value={filters.isDisabled?.toString() || ''}
  334. onValueChange={(value) =>
  335. handleFilterChange({
  336. isDisabled: value === '' ? undefined : parseInt(value)
  337. })
  338. }
  339. >
  340. <SelectTrigger>
  341. <SelectValue placeholder="选择状态" />
  342. </SelectTrigger>
  343. <SelectContent>
  344. <SelectItem value="">全部状态</SelectItem>
  345. <SelectItem value="0">启用</SelectItem>
  346. <SelectItem value="1">禁用</SelectItem>
  347. </SelectContent>
  348. </Select>
  349. </div>
  350. {/* 角色筛选 */}
  351. <div className="space-y-2">
  352. <label className="text-sm font-medium">用户角色</label>
  353. <Select
  354. value=""
  355. onValueChange={(value) => {
  356. const roleId = parseInt(value);
  357. if (!filters.roleIds.includes(roleId)) {
  358. handleFilterChange({
  359. roleIds: [...filters.roleIds, roleId]
  360. });
  361. }
  362. }}
  363. >
  364. <SelectTrigger>
  365. <SelectValue placeholder="选择角色" />
  366. </SelectTrigger>
  367. <SelectContent>
  368. <SelectItem value="1">管理员</SelectItem>
  369. <SelectItem value="2">普通用户</SelectItem>
  370. </SelectContent>
  371. </Select>
  372. {filters.roleIds.length > 0 && (
  373. <div className="flex flex-wrap gap-2 mt-2">
  374. {filters.roleIds.map(roleId => (
  375. <Badge
  376. key={roleId}
  377. variant="secondary"
  378. className="flex items-center gap-1"
  379. >
  380. {roleId === 1 ? '管理员' : '普通用户'}
  381. <X
  382. className="h-3 w-3 cursor-pointer"
  383. onClick={() => handleFilterChange({
  384. roleIds: filters.roleIds.filter(id => id !== roleId)
  385. })}
  386. />
  387. </Badge>
  388. ))}
  389. </div>
  390. )}
  391. </div>
  392. {/* 创建时间筛选 */}
  393. <div className="space-y-2">
  394. <label className="text-sm font-medium">创建时间</label>
  395. <Popover>
  396. <PopoverTrigger asChild>
  397. <Button
  398. variant="outline"
  399. className={cn(
  400. "w-full justify-start text-left font-normal",
  401. !filters.createdAt && "text-muted-foreground"
  402. )}
  403. >
  404. {filters.createdAt ?
  405. `${filters.createdAt.gte || ''} 至 ${filters.createdAt.lte || ''}` :
  406. '选择日期范围'
  407. }
  408. </Button>
  409. </PopoverTrigger>
  410. <PopoverContent className="w-auto p-0" align="start">
  411. <Calendar
  412. mode="range"
  413. selected={{
  414. from: filters.createdAt?.gte ? new Date(filters.createdAt.gte) : undefined,
  415. to: filters.createdAt?.lte ? new Date(filters.createdAt.lte) : undefined
  416. }}
  417. onSelect={(range) => {
  418. handleFilterChange({
  419. createdAt: range?.from && range?.to ? {
  420. gte: format(range.from, 'yyyy-MM-dd'),
  421. lte: format(range.to, 'yyyy-MM-dd')
  422. } : undefined
  423. });
  424. }}
  425. initialFocus
  426. />
  427. </PopoverContent>
  428. </Popover>
  429. </div>
  430. </div>
  431. )}
  432. {/* 过滤条件标签 */}
  433. {hasActiveFilters && (
  434. <div className="flex flex-wrap gap-2">
  435. {filters.isDisabled !== undefined && (
  436. <Badge variant="secondary" className="flex items-center gap-1">
  437. 状态: {filters.isDisabled === 0 ? '启用' : '禁用'}
  438. <X
  439. className="h-3 w-3 cursor-pointer"
  440. onClick={() => handleFilterChange({ isDisabled: undefined })}
  441. />
  442. </Badge>
  443. )}
  444. {filters.roleIds.map(roleId => (
  445. <Badge key={roleId} variant="secondary" className="flex items-center gap-1">
  446. 角色: {roleId === 1 ? '管理员' : '普通用户'}
  447. <X
  448. className="h-3 w-3 cursor-pointer"
  449. onClick={() => handleFilterChange({
  450. roleIds: filters.roleIds.filter(id => id !== roleId)
  451. })}
  452. />
  453. </Badge>
  454. ))}
  455. {filters.createdAt && (
  456. <Badge variant="secondary" className="flex items-center gap-1">
  457. 创建时间: {filters.createdAt.gte || ''} 至 {filters.createdAt.lte || ''}
  458. <X
  459. className="h-3 w-3 cursor-pointer"
  460. onClick={() => handleFilterChange({ createdAt: undefined })}
  461. />
  462. </Badge>
  463. )}
  464. </div>
  465. )}
  466. </div>
  467. <div className="rounded-md border">
  468. <Table>
  469. <TableHeader>
  470. <TableRow>
  471. <TableHead>用户名</TableHead>
  472. <TableHead>昵称</TableHead>
  473. <TableHead>邮箱</TableHead>
  474. <TableHead>真实姓名</TableHead>
  475. <TableHead>角色</TableHead>
  476. <TableHead>状态</TableHead>
  477. <TableHead>创建时间</TableHead>
  478. <TableHead className="text-right">操作</TableHead>
  479. </TableRow>
  480. </TableHeader>
  481. <TableBody>
  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. </TableBody>
  527. </Table>
  528. </div>
  529. <DataTablePagination
  530. currentPage={searchParams.page}
  531. totalCount={totalCount}
  532. pageSize={searchParams.limit}
  533. onPageChange={handlePageChange}
  534. />
  535. </CardContent>
  536. </Card>
  537. {/* 创建/编辑用户对话框 */}
  538. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  539. <DialogContent className="sm:max-w-[500px]">
  540. <DialogHeader>
  541. <DialogTitle>
  542. {editingUser ? '编辑用户' : '创建用户'}
  543. </DialogTitle>
  544. <DialogDescription>
  545. {editingUser ? '编辑现有用户信息' : '创建一个新的用户账户'}
  546. </DialogDescription>
  547. </DialogHeader>
  548. {isCreateForm ? (
  549. <Form {...createForm}>
  550. <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
  551. <FormField
  552. control={createForm.control}
  553. name="username"
  554. render={({ field }) => (
  555. <FormItem>
  556. <FormLabel className="flex items-center">
  557. 用户名
  558. <span className="text-red-500 ml-1">*</span>
  559. </FormLabel>
  560. <FormControl>
  561. <Input placeholder="请输入用户名" {...field} />
  562. </FormControl>
  563. <FormMessage />
  564. </FormItem>
  565. )}
  566. />
  567. <FormField
  568. control={createForm.control}
  569. name="nickname"
  570. render={({ field }) => (
  571. <FormItem>
  572. <FormLabel>昵称</FormLabel>
  573. <FormControl>
  574. <Input placeholder="请输入昵称" {...field} />
  575. </FormControl>
  576. <FormMessage />
  577. </FormItem>
  578. )}
  579. />
  580. <FormField
  581. control={createForm.control}
  582. name="email"
  583. render={({ field }) => (
  584. <FormItem>
  585. <FormLabel>邮箱</FormLabel>
  586. <FormControl>
  587. <Input type="email" placeholder="请输入邮箱" {...field} />
  588. </FormControl>
  589. <FormMessage />
  590. </FormItem>
  591. )}
  592. />
  593. <FormField
  594. control={createForm.control}
  595. name="phone"
  596. render={({ field }) => (
  597. <FormItem>
  598. <FormLabel>手机号</FormLabel>
  599. <FormControl>
  600. <Input placeholder="请输入手机号" {...field} />
  601. </FormControl>
  602. <FormMessage />
  603. </FormItem>
  604. )}
  605. />
  606. <FormField
  607. control={createForm.control}
  608. name="name"
  609. render={({ field }) => (
  610. <FormItem>
  611. <FormLabel>真实姓名</FormLabel>
  612. <FormControl>
  613. <Input placeholder="请输入真实姓名" {...field} />
  614. </FormControl>
  615. <FormMessage />
  616. </FormItem>
  617. )}
  618. />
  619. <FormField
  620. control={createForm.control}
  621. name="password"
  622. render={({ field }) => (
  623. <FormItem>
  624. <FormLabel className="flex items-center">
  625. 密码
  626. <span className="text-red-500 ml-1">*</span>
  627. </FormLabel>
  628. <FormControl>
  629. <Input type="password" placeholder="请输入密码" {...field} />
  630. </FormControl>
  631. <FormMessage />
  632. </FormItem>
  633. )}
  634. />
  635. <FormField
  636. control={createForm.control}
  637. name="isDisabled"
  638. render={({ field }) => (
  639. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  640. <div className="space-y-0.5">
  641. <FormLabel className="text-base">用户状态</FormLabel>
  642. <FormDescription>
  643. 禁用后用户将无法登录系统
  644. </FormDescription>
  645. </div>
  646. <FormControl>
  647. <Switch
  648. checked={field.value === 1}
  649. onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
  650. />
  651. </FormControl>
  652. </FormItem>
  653. )}
  654. />
  655. <DialogFooter>
  656. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  657. 取消
  658. </Button>
  659. <Button type="submit">
  660. 创建用户
  661. </Button>
  662. </DialogFooter>
  663. </form>
  664. </Form>
  665. ) : (
  666. <Form {...updateForm}>
  667. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  668. <FormField
  669. control={updateForm.control}
  670. name="username"
  671. render={({ field }) => (
  672. <FormItem>
  673. <FormLabel className="flex items-center">
  674. 用户名
  675. <span className="text-red-500 ml-1">*</span>
  676. </FormLabel>
  677. <FormControl>
  678. <Input placeholder="请输入用户名" {...field} />
  679. </FormControl>
  680. <FormMessage />
  681. </FormItem>
  682. )}
  683. />
  684. <FormField
  685. control={updateForm.control}
  686. name="nickname"
  687. render={({ field }) => (
  688. <FormItem>
  689. <FormLabel>昵称</FormLabel>
  690. <FormControl>
  691. <Input placeholder="请输入昵称" {...field} />
  692. </FormControl>
  693. <FormMessage />
  694. </FormItem>
  695. )}
  696. />
  697. <FormField
  698. control={updateForm.control}
  699. name="email"
  700. render={({ field }) => (
  701. <FormItem>
  702. <FormLabel>邮箱</FormLabel>
  703. <FormControl>
  704. <Input type="email" placeholder="请输入邮箱" {...field} />
  705. </FormControl>
  706. <FormMessage />
  707. </FormItem>
  708. )}
  709. />
  710. <FormField
  711. control={updateForm.control}
  712. name="phone"
  713. render={({ field }) => (
  714. <FormItem>
  715. <FormLabel>手机号</FormLabel>
  716. <FormControl>
  717. <Input placeholder="请输入手机号" {...field} />
  718. </FormControl>
  719. <FormMessage />
  720. </FormItem>
  721. )}
  722. />
  723. <FormField
  724. control={updateForm.control}
  725. name="name"
  726. render={({ field }) => (
  727. <FormItem>
  728. <FormLabel>真实姓名</FormLabel>
  729. <FormControl>
  730. <Input placeholder="请输入真实姓名" {...field} />
  731. </FormControl>
  732. <FormMessage />
  733. </FormItem>
  734. )}
  735. />
  736. <FormField
  737. control={updateForm.control}
  738. name="password"
  739. render={({ field }) => (
  740. <FormItem>
  741. <FormLabel>新密码</FormLabel>
  742. <FormControl>
  743. <Input type="password" placeholder="留空则不修改密码" {...field} />
  744. </FormControl>
  745. <FormMessage />
  746. </FormItem>
  747. )}
  748. />
  749. <FormField
  750. control={updateForm.control}
  751. name="isDisabled"
  752. render={({ field }) => (
  753. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  754. <div className="space-y-0.5">
  755. <FormLabel className="text-base">用户状态</FormLabel>
  756. <FormDescription>
  757. 禁用后用户将无法登录系统
  758. </FormDescription>
  759. </div>
  760. <FormControl>
  761. <Switch
  762. checked={field.value === 1}
  763. onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
  764. />
  765. </FormControl>
  766. </FormItem>
  767. )}
  768. />
  769. <DialogFooter>
  770. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  771. 取消
  772. </Button>
  773. <Button type="submit">
  774. 更新用户
  775. </Button>
  776. </DialogFooter>
  777. </form>
  778. </Form>
  779. )}
  780. </DialogContent>
  781. </Dialog>
  782. {/* 删除确认对话框 */}
  783. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  784. <DialogContent>
  785. <DialogHeader>
  786. <DialogTitle>确认删除</DialogTitle>
  787. <DialogDescription>
  788. 确定要删除这个用户吗?此操作无法撤销。
  789. </DialogDescription>
  790. </DialogHeader>
  791. <DialogFooter>
  792. <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
  793. 取消
  794. </Button>
  795. <Button variant="destructive" onClick={confirmDelete}>
  796. 删除
  797. </Button>
  798. </DialogFooter>
  799. </DialogContent>
  800. </Dialog>
  801. </div>
  802. );
  803. };