Bläddra i källkod

✨ feat(users): 统一前后端用户表单验证逻辑

- 后端user.schema.ts添加详细的错误提示信息
- 前端直接引入后端定义的CreateUserDto和UpdateUserDto作为表单验证schema
- 移除前后端重复的表单验证逻辑,减少维护成本
- 优化用户表单数据处理,简化提交逻辑
- 修复表单输入框在值为null时的显示问题

♻️ refactor(users): 优化用户表单数据处理逻辑

- 简化创建和更新用户的数据提交过程,直接使用表单数据
- 修复开关组件与数值类型状态的映射关系
- 统一表单输入框的value处理方式,确保空值正确显示
- 移除冗余的数据转换代码,提高代码可读性和可维护性
yourname 4 månader sedan
förälder
incheckning
95f150e1b9
2 ändrade filer med 47 tillägg och 74 borttagningar
  1. 24 51
      src/client/admin-shadcn/pages/Users.tsx
  2. 23 23
      src/server/modules/users/user.schema.ts

+ 24 - 51
src/client/admin-shadcn/pages/Users.tsx

@@ -18,33 +18,16 @@ import { toast } from 'sonner';
 import { Skeleton } from '@/client/components/ui/skeleton';
 import { Switch } from '@/client/components/ui/switch';
 import { DisabledStatus } from '@/share/types';
+import { CreateUserDto, UpdateUserDto } from '@/server/modules/users/user.schema';
 
 // 使用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 - 与后端CreateUserSchema保持一致
-const createUserFormSchema = z.object({
-  username: z.string().min(3, '用户名至少3个字符'),
-  nickname: z.string().nullable().optional(),
-  email: z.string().email('请输入有效的邮箱地址').nullable().optional(),
-  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号').nullable().optional(),
-  name: z.string().nullable().optional(),
-  password: z.string().min(6, '密码至少6个字符'),
-  isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED),
-});
-
-// 更新用户表单Schema - 与后端UpdateUserSchema保持一致
-const updateUserFormSchema = z.object({
-  username: z.string().min(3, '用户名至少3个字符').optional(),
-  nickname: z.string().nullable().optional(),
-  email: z.string().email('请输入有效的邮箱地址').nullable().optional(),
-  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号').nullable().optional(),
-  name: z.string().nullable().optional(),
-  password: z.string().min(6, '密码至少6个字符').optional(),
-  isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED),
-});
+// 直接使用后端定义的 schema
+const createUserFormSchema = CreateUserDto;
+const updateUserFormSchema = UpdateUserDto;
 
 type CreateUserFormData = CreateUserRequest;
 type UpdateUserFormData = UpdateUserRequest;
@@ -125,7 +108,7 @@ export const UsersPage = () => {
     setIsCreateForm(true);
     createForm.reset({
       username: '',
-      nickname: undefined,
+      nickname: null,
       email: null,
       phone: null,
       name: null,
@@ -141,10 +124,10 @@ export const UsersPage = () => {
     setIsCreateForm(false);
     updateForm.reset({
       username: user.username,
-      nickname: user.nickname || undefined,
-      email: user.email || null,
-      phone: user.phone || null,
-      name: user.name || null,
+      nickname: user.nickname,
+      email: user.email,
+      phone: user.phone,
+      name: user.name,
       isDisabled: user.isDisabled,
     });
     setIsModalOpen(true);
@@ -153,13 +136,8 @@ export const UsersPage = () => {
   // 处理创建表单提交
   const handleCreateSubmit = async (data: CreateUserFormData) => {
     try {
-      const submitData: CreateUserRequest = {
-        ...data,
-        isDisabled: data.isDisabled ? 1 : 0,
-      };
-
       const res = await userClient.$post({
-        json: submitData
+        json: data
       });
       if (res.status !== 201) {
         throw new Error('创建用户失败');
@@ -178,14 +156,9 @@ export const UsersPage = () => {
     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
+        json: data
       });
       if (res.status !== 200) {
         throw new Error('更新用户失败');
@@ -414,7 +387,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>昵称</FormLabel>
                       <FormControl>
-                        <Input placeholder="请输入昵称" {...field} />
+                        <Input placeholder="请输入昵称" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -428,7 +401,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>邮箱</FormLabel>
                       <FormControl>
-                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                        <Input type="email" placeholder="请输入邮箱" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -442,7 +415,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>手机号</FormLabel>
                       <FormControl>
-                        <Input placeholder="请输入手机号" {...field} />
+                        <Input placeholder="请输入手机号" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -456,7 +429,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>真实姓名</FormLabel>
                       <FormControl>
-                        <Input placeholder="请输入真实姓名" {...field} />
+                        <Input placeholder="请输入真实姓名" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -490,8 +463,8 @@ export const UsersPage = () => {
                       </div>
                       <FormControl>
                         <Switch
-                          checked={field.value}
-                          onCheckedChange={field.onChange}
+                          checked={field.value === 1}
+                          onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
                         />
                       </FormControl>
                     </FormItem>
@@ -518,7 +491,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>用户名</FormLabel>
                       <FormControl>
-                        <Input placeholder="请输入用户名" {...field} />
+                        <Input placeholder="请输入用户名" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -532,7 +505,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>昵称</FormLabel>
                       <FormControl>
-                        <Input placeholder="请输入昵称" {...field} />
+                        <Input placeholder="请输入昵称" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -546,7 +519,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>邮箱</FormLabel>
                       <FormControl>
-                        <Input type="email" placeholder="请输入邮箱" {...field} />
+                        <Input type="email" placeholder="请输入邮箱" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -560,7 +533,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>手机号</FormLabel>
                       <FormControl>
-                        <Input placeholder="请输入手机号" {...field} />
+                        <Input placeholder="请输入手机号" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -574,7 +547,7 @@ export const UsersPage = () => {
                     <FormItem>
                       <FormLabel>真实姓名</FormLabel>
                       <FormControl>
-                        <Input placeholder="请输入真实姓名" {...field} />
+                        <Input placeholder="请输入真实姓名" {...field} value={field.value || ''} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
@@ -608,8 +581,8 @@ export const UsersPage = () => {
                       </div>
                       <FormControl>
                         <Switch
-                          checked={field.value}
-                          onCheckedChange={field.onChange}
+                          checked={field.value === 1}
+                          onCheckedChange={(checked) => field.onChange(checked ? 1 : 0)}
                         />
                       </FormControl>
                     </FormItem>

+ 23 - 23
src/server/modules/users/user.schema.ts

@@ -5,31 +5,31 @@ import { RoleSchema } from './role.schema';
 // 基础用户 schema(包含所有字段)
 export const UserSchema = z.object({
   id: z.number().int().positive().openapi({ description: '用户ID' }),
-  username: z.string().min(3).max(255).openapi({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
     example: 'admin',
     description: '用户名,3-255个字符'
   }),
-  password: z.string().min(6).max(255).openapi({
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
     example: 'password123',
     description: '密码,最少6位'
   }),
-  phone: z.string().max(255).nullable().openapi({
+  phone: z.string().max(255, '手机号最多255个字符').nullable().openapi({
     example: '13800138000',
     description: '手机号'
   }),
-  email: z.string().email().max(255).nullable().openapi({
+  email: z.string().email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().openapi({
     example: 'user@example.com',
     description: '邮箱'
   }),
-  nickname: z.string().max(255).nullable().openapi({
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().openapi({
     example: '昵称',
     description: '用户昵称'
   }),
-  name: z.string().max(255).nullable().openapi({
+  name: z.string().max(255, '姓名最多255个字符').nullable().openapi({
     example: '张三',
     description: '真实姓名'
   }),
-  avatar: z.string().max(255).nullable().openapi({
+  avatar: z.string().max(255, '头像地址最多255个字符').nullable().openapi({
     example: 'https://example.com/avatar.jpg',
     description: '用户头像'
   }),
@@ -60,35 +60,35 @@ export const UserSchema = z.object({
 
 // 创建用户请求 schema
 export const CreateUserDto = z.object({
-  username: z.string().min(3).max(255).openapi({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
     example: 'admin',
     description: '用户名,3-255个字符'
   }),
-  password: z.string().min(6).max(255).openapi({
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
     example: 'password123',
     description: '密码,最少6位'
   }),
-  phone: z.string().max(255).nullable().optional().openapi({
+  phone: z.string().max(255, '手机号最多255个字符').nullable().optional().openapi({
     example: '13800138000',
     description: '手机号'
   }),
-  email: z.string().email().max(255).nullable().optional().openapi({
+  email: z.string().email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().optional().openapi({
     example: 'user@example.com',
     description: '邮箱'
   }),
-  nickname: z.string().max(255).nullable().optional().openapi({
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
     example: '昵称',
     description: '用户昵称'
   }),
-  name: z.string().max(255).nullable().optional().openapi({
+  name: z.string().max(255, '姓名最多255个字符').nullable().optional().openapi({
     example: '张三',
     description: '真实姓名'
   }),
-  avatar: z.string().max(255).nullable().optional().openapi({
+  avatar: z.string().max(255, '头像地址最多255个字符').nullable().optional().openapi({
     example: 'https://example.com/avatar.jpg',
     description: '用户头像'
   }),
-  isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED).optional().openapi({
+  isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').default(DisabledStatus.ENABLED).optional().openapi({
     example: DisabledStatus.ENABLED,
     description: '是否禁用(0:启用,1:禁用)'
   })
@@ -96,35 +96,35 @@ export const CreateUserDto = z.object({
 
 // 更新用户请求 schema
 export const UpdateUserDto = z.object({
-  username: z.string().min(3).max(255).optional().openapi({
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').optional().openapi({
     example: 'admin',
     description: '用户名,3-255个字符'
   }),
-  password: z.string().min(6).max(255).optional().openapi({
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
     example: 'password123',
     description: '密码,最少6位'
   }),
-  phone: z.string().max(255).nullable().optional().openapi({
+  phone: z.string().max(255, '手机号最多255个字符').nullable().optional().openapi({
     example: '13800138000',
     description: '手机号'
   }),
-  email: z.string().email().max(255).nullable().optional().openapi({
+  email: z.string().email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().optional().openapi({
     example: 'user@example.com',
     description: '邮箱'
   }),
-  nickname: z.string().max(255).nullable().optional().openapi({
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
     example: '昵称',
     description: '用户昵称'
   }),
-  name: z.string().max(255).nullable().optional().openapi({
+  name: z.string().max(255, '姓名最多255个字符').nullable().optional().openapi({
     example: '张三',
     description: '真实姓名'
   }),
-  avatar: z.string().max(255).nullable().optional().openapi({
+  avatar: z.string().max(255, '头像地址最多255个字符').nullable().optional().openapi({
     example: 'https://example.com/avatar.jpg',
     description: '用户头像'
   }),
-  isDisabled: z.number().int().min(0).max(1).optional().openapi({
+  isDisabled: z.number().int().min(0, '状态值只能是0或1').max(1, '状态值只能是0或1').optional().openapi({
     example: DisabledStatus.ENABLED,
     description: '是否禁用(0:启用,1:禁用)'
   })