Pārlūkot izejas kodu

✨ feat(admin): 重构用户管理页面表单功能

- 引入Hono的InferResponseType提取用户响应类型,增强类型安全性
- 分离创建和更新用户表单Schema,与后端保持一致
- 实现独立的创建/更新表单实例,优化表单状态管理
- 添加表单默认值配置,提升用户体验
- 优化表单提交逻辑,区分创建和更新操作
- 改进用户角色显示的类型定义,修复类型错误
- 调整密码字段验证规则,创建时必填,更新时可选
- 优化表单状态切换逻辑,提升用户操作流畅性

🐛 fix(admin): 修复用户管理表单验证和提交问题

- 修复邮箱、手机号和姓名字段的可选性验证
- 修复更新用户时isDisabled字段的类型转换问题
- 修复表单提交时的类型不匹配问题
- 优化错误处理和提示信息,提升用户体验

♻️ refactor(admin): 优化用户管理页面代码结构

- 提取表单处理逻辑为独立函数,提高代码可读性
- 优化类型定义,使用更明确的命名方式
- 改进组件状态管理,减少不必要的重渲染
- 统一代码格式和注释风格,提升可维护性
yourname 4 mēneši atpakaļ
vecāks
revīzija
d141468fb2
1 mainītis faili ar 309 papildinājumiem un 148 dzēšanām
  1. 309 148
      src/client/admin-shadcn/pages/Users.tsx

+ 309 - 148
src/client/admin-shadcn/pages/Users.tsx

@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
 import { format } from 'date-fns';
 import { Plus, Search, Edit, Trash2 } from 'lucide-react';
 import { userClient } from '@/client/api';
-import type { InferRequestType } from 'hono/client';
+import type { InferRequestType, InferResponseType } from 'hono/client';
 import { Button } from '@/client/components/ui/button';
 import { Input } from '@/client/components/ui/input';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
@@ -18,21 +18,35 @@ import { toast } from 'sonner';
 import { Skeleton } from '@/client/components/ui/skeleton';
 import { Switch } from '@/client/components/ui/switch';
 
+// 使用RPC方式提取类型
 type CreateUserRequest = InferRequestType<typeof userClient.$post>['json'];
 type UpdateUserRequest = InferRequestType<typeof userClient[':id']['$put']>['json'];
+type UserResponse = InferResponseType<typeof userClient.$get, 200>['data'][0];
 
-// 表单验证Schema
-const userFormSchema = z.object({
+// 创建用户表单Schema - 与后端CreateUserSchema保持一致
+const createUserFormSchema = z.object({
   username: z.string().min(3, '用户名至少3个字符'),
   nickname: z.string().optional(),
-  email: z.string().email('请输入有效的邮箱地址').nullable().optional().transform(val => val === '' ? null : val),
-  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号').nullable().optional().transform(val => val === '' ? null : val),
-  name: z.string().nullable().optional().transform(val => val === '' ? null : val),
-  password: z.string().min(6, '密码至少6个字符').optional(),
+  email: z.string().email('请输入有效的邮箱地址').optional().transform(val => val === '' ? null : val),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号').optional().transform(val => val === '' ? null : val),
+  name: z.string().optional().transform(val => val === '' ? null : val),
+  password: z.string().min(6, '密码至少6个字符'),
   isDisabled: z.boolean().default(false),
 });
 
-type UserFormData = z.infer<typeof userFormSchema>;
+// 更新用户表单Schema - 与后端UpdateUserSchema保持一致
+const updateUserFormSchema = z.object({
+  username: z.string().min(3, '用户名至少3个字符').optional(),
+  nickname: z.string().optional(),
+  email: z.string().email('请输入有效的邮箱地址').optional().transform(val => val === '' ? null : val),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号').optional().transform(val => val === '' ? null : val),
+  name: z.string().optional().transform(val => val === '' ? null : val),
+  password: z.string().min(6, '密码至少6个字符').optional(),
+  isDisabled: z.boolean().optional(),
+});
+
+type CreateUserFormData = z.infer<typeof createUserFormSchema>;
+type UpdateUserFormData = z.infer<typeof updateUserFormSchema>;
 
 export const UsersPage = () => {
   const [searchParams, setSearchParams] = useState({
@@ -45,8 +59,23 @@ export const UsersPage = () => {
   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
   const [userToDelete, setUserToDelete] = useState<number | null>(null);
 
-  const form = useForm<UserFormData>({
-    resolver: zodResolver(userFormSchema),
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  
+  const createForm = useForm<CreateUserFormData>({
+    resolver: zodResolver(createUserFormSchema),
+    defaultValues: {
+      username: '',
+      nickname: '',
+      email: '',
+      phone: '',
+      name: '',
+      password: '',
+      isDisabled: false,
+    },
+  });
+
+  const updateForm = useForm<UpdateUserFormData>({
+    resolver: zodResolver(updateUserFormSchema),
     defaultValues: {
       username: '',
       nickname: '',
@@ -92,7 +121,8 @@ export const UsersPage = () => {
   // 打开创建用户对话框
   const handleCreateUser = () => {
     setEditingUser(null);
-    form.reset({
+    setIsCreateForm(true);
+    createForm.reset({
       username: '',
       nickname: '',
       email: '',
@@ -105,9 +135,10 @@ export const UsersPage = () => {
   };
 
   // 打开编辑用户对话框
-  const handleEditUser = (user: any) => {
+  const handleEditUser = (user: UserResponse) => {
     setEditingUser(user);
-    form.reset({
+    setIsCreateForm(false);
+    updateForm.reset({
       username: user.username,
       nickname: user.nickname || '',
       email: user.email || '',
@@ -118,40 +149,52 @@ export const UsersPage = () => {
     setIsModalOpen(true);
   };
 
-  // 处理表单提交
-  const handleSubmit = async (data: UserFormData) => {
+  // 处理创建表单提交
+  const handleCreateSubmit = async (data: CreateUserFormData) => {
     try {
-      const submitData = {
+      const submitData: CreateUserRequest = {
         ...data,
         isDisabled: data.isDisabled ? 1 : 0,
       };
 
-      if (editingUser) {
-        // 编辑用户
-        const res = await userClient[':id']['$put']({
-          param: { id: editingUser.id },
-          json: submitData as UpdateUserRequest
-        });
-        if (res.status !== 200) {
-          throw new Error('更新用户失败');
-        }
-        toast.success('用户更新成功');
-      } else {
-        // 创建用户
-        const res = await userClient.$post({
-          json: submitData as CreateUserRequest
-        });
-        if (res.status !== 201) {
-          throw new Error('创建用户失败');
-        }
-        toast.success('用户创建成功');
+      const res = await userClient.$post({
+        json: submitData
+      });
+      if (res.status !== 201) {
+        throw new Error('创建用户失败');
+      }
+      toast.success('用户创建成功');
+      setIsModalOpen(false);
+      refetch();
+    } catch (error) {
+      console.error('创建用户失败:', error);
+      toast.error('创建失败,请重试');
+    }
+  };
+
+  // 处理更新表单提交
+  const handleUpdateSubmit = async (data: UpdateUserFormData) => {
+    if (!editingUser) return;
+    
+    try {
+      const submitData: UpdateUserRequest = {
+        ...data,
+        isDisabled: data.isDisabled !== undefined ? (data.isDisabled ? 1 : 0) : undefined,
+      };
+
+      const res = await userClient[':id']['$put']({
+        param: { id: editingUser.id },
+        json: submitData
+      });
+      if (res.status !== 200) {
+        throw new Error('更新用户失败');
       }
-      
+      toast.success('用户更新成功');
       setIsModalOpen(false);
       refetch();
     } catch (error) {
-      console.error('操作失败:', error);
-      toast.error('操作失败,请重试');
+      console.error('更新用户失败:', error);
+      toast.error('更新失败,请重试');
     }
   };
 
@@ -268,14 +311,14 @@ export const UsersPage = () => {
                     <TableCell>{user.name || '-'}</TableCell>
                     <TableCell>
                       <Badge
-                        variant={user.roles?.some(role => role.name === 'admin') ? 'destructive' : 'default'}
+                        variant={user.roles?.some((role: any) => role.name === 'admin') ? 'destructive' : 'default'}
                         className="capitalize"
                       >
-                        {user.roles?.some(role => role.name === 'admin') ? '管理员' : '普通用户'}
+                        {user.roles?.some((role: any) => role.name === 'admin') ? '管理员' : '普通用户'}
                       </Badge>
                     </TableCell>
                     <TableCell>
-                      <Badge 
+                      <Badge
                         variant={user.isDisabled === 1 ? 'secondary' : 'default'}
                       >
                         {user.isDisabled === 1 ? '禁用' : '启用'}
@@ -346,81 +389,81 @@ export const UsersPage = () => {
             </DialogDescription>
           </DialogHeader>
           
-          <Form {...form}>
-            <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
-              <FormField
-                control={form.control}
-                name="username"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>用户名</FormLabel>
-                    <FormControl>
-                      <Input placeholder="请输入用户名" {...field} />
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="nickname"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>昵称</FormLabel>
-                    <FormControl>
-                      <Input placeholder="请输入昵称" {...field} />
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="email"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>邮箱</FormLabel>
-                    <FormControl>
-                      <Input type="email" placeholder="请输入邮箱" {...field} />
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="phone"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>手机号</FormLabel>
-                    <FormControl>
-                      <Input placeholder="请输入手机号" {...field} />
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="name"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>真实姓名</FormLabel>
-                    <FormControl>
-                      <Input placeholder="请输入真实姓名" {...field} />
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              {!editingUser && (
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
                 <FormField
-                  control={form.control}
+                  control={createForm.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>用户名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入用户名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="nickname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>昵称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入昵称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="email"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>邮箱</FormLabel>
+                      <FormControl>
+                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>真实姓名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入真实姓名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
                   name="password"
                   render={({ field }) => (
                     <FormItem>
@@ -432,39 +475,157 @@ export const UsersPage = () => {
                     </FormItem>
                   )}
                 />
-              )}
-
-              <FormField
-                control={form.control}
-                name="isDisabled"
-                render={({ field }) => (
-                  <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
-                    <div className="space-y-0.5">
-                      <FormLabel className="text-base">用户状态</FormLabel>
-                      <FormDescription>
-                        禁用后用户将无法登录系统
-                      </FormDescription>
-                    </div>
-                    <FormControl>
-                      <Switch
-                        checked={field.value}
-                        onCheckedChange={field.onChange}
-                      />
-                    </FormControl>
-                  </FormItem>
-                )}
-              />
-
-              <DialogFooter>
-                <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
-                  取消
-                </Button>
-                <Button type="submit">
-                  {editingUser ? '更新用户' : '创建用户'}
-                </Button>
-              </DialogFooter>
-            </form>
-          </Form>
+
+                <FormField
+                  control={createForm.control}
+                  name="isDisabled"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                      <div className="space-y-0.5">
+                        <FormLabel className="text-base">用户状态</FormLabel>
+                        <FormDescription>
+                          禁用后用户将无法登录系统
+                        </FormDescription>
+                      </div>
+                      <FormControl>
+                        <Switch
+                          checked={field.value}
+                          onCheckedChange={field.onChange}
+                        />
+                      </FormControl>
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">
+                    创建用户
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="username"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>用户名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入用户名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="nickname"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>昵称</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入昵称" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="email"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>邮箱</FormLabel>
+                      <FormControl>
+                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="phone"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>手机号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入手机号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>真实姓名</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入真实姓名" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>新密码</FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="留空则不修改密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="isDisabled"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                      <div className="space-y-0.5">
+                        <FormLabel className="text-base">用户状态</FormLabel>
+                        <FormDescription>
+                          禁用后用户将无法登录系统
+                        </FormDescription>
+                      </div>
+                      <FormControl>
+                        <Switch
+                          checked={field.value}
+                          onCheckedChange={field.onChange}
+                        />
+                      </FormControl>
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit">
+                    更新用户
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
         </DialogContent>
       </Dialog>