Users.tsx 34 KB

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