浏览代码

✨ feat(login): 重构登录表单为表单组件并实现验证功能

- 引入react-hook-form和zod实现表单管理和验证
- 实现用户名和密码的输入验证规则:用户名3-20字符且无空格,密码6-20字符
- 重构表单UI为组件化结构,使用Form组件包装输入项
- 优化登录按钮状态,表单验证通过前禁用提交
- 移除冗余的输入处理函数,使用表单组件内置处理
- 优化错误提示显示方式,统一使用FormMessage组件

♻️ refactor(login): 优化代码结构和用户体验

- 移除旧的useState状态管理,改用表单组件的状态管理
- 优化登录按钮加载状态显示,添加加载动画
- 统一代码格式和缩进,修复排版不一致问题
- 优化导航链接跳转代码格式
- 移除多余的输入处理函数,简化代码逻辑
yourname 4 月之前
父节点
当前提交
f59268f5c5
共有 1 个文件被更改,包括 107 次插入118 次删除
  1. 107 118
      mini/src/pages/login/index.tsx

+ 107 - 118
mini/src/pages/login/index.tsx

@@ -6,14 +6,38 @@ import { cn } from '@/utils/cn'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import Navbar from '@/components/ui/navbar'
+import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
+import { z } from 'zod'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm } from 'react-hook-form'
 import './index.css'
 
+const loginSchema = z.object({
+  username: z
+    .string()
+    .min(3, '用户名至少3个字符')
+    .max(20, '用户名最多20个字符')
+    .regex(/^\S+$/, '用户名不能包含空格'),
+  password: z
+    .string()
+    .min(6, '密码至少6位')
+    .max(20, '密码最多20位'),
+})
+
+type LoginFormData = z.infer<typeof loginSchema>
+
 export default function Login() {
-  const [username, setUsername] = useState('')
-  const [password, setPassword] = useState('')
   const [showPassword, setShowPassword] = useState(false)
   const { login, isLoading } = useAuth()
 
+  const form = useForm<LoginFormData>({
+    resolver: zodResolver(loginSchema),
+    defaultValues: {
+      username: '',
+      password: '',
+    },
+  })
+
   // 设置导航栏标题
   useEffect(() => {
     Taro.setNavigationBarTitle({
@@ -21,44 +45,7 @@ export default function Login() {
     })
   }, [])
 
-  const handleLogin = async () => {
-    // 输入验证
-    if (!username.trim()) {
-      Taro.showToast({
-        title: '请输入用户名',
-        icon: 'none',
-        duration: 2000
-      })
-      return
-    }
-
-    if (!password.trim()) {
-      Taro.showToast({
-        title: '请输入密码',
-        icon: 'none',
-        duration: 2000
-      })
-      return
-    }
-
-    if (username.trim().length < 3) {
-      Taro.showToast({
-        title: '用户名至少3个字符',
-        icon: 'none',
-        duration: 2000
-      })
-      return
-    }
-
-    if (password.trim().length < 6) {
-      Taro.showToast({
-        title: '密码至少6位',
-        icon: 'none',
-        duration: 2000
-      })
-      return
-    }
-    
+  const onSubmit = async (data: LoginFormData) => {
     try {
       Taro.showLoading({
         title: '登录中...',
@@ -66,8 +53,8 @@ export default function Login() {
       })
 
       await login({
-        username: username.trim(),
-        password: password.trim()
+        username: data.username.trim(),
+        password: data.password.trim()
       })
       
       Taro.hideLoading()
@@ -110,25 +97,11 @@ export default function Login() {
   }
 
   const goToRegister = () => {
-    Taro.navigateTo({ 
+    Taro.navigateTo({
       url: '/pages/register/index'
     })
   }
 
-  const handleUsernameInput = (e: any) => {
-    const value = e.detail.value
-    if (value.length <= 20) {
-      setUsername(value.replace(/\s+/g, ''))
-    }
-  }
-
-  const handlePasswordInput = (e: any) => {
-    const value = e.detail.value
-    if (value.length <= 20) {
-      setPassword(value)
-    }
-  }
-
   return (
     <View className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
       <Navbar
@@ -149,76 +122,92 @@ export default function Login() {
 
         {/* 登录表单 */}
         <View className="bg-white rounded-2xl shadow-sm p-6">
-          <View className="space-y-5">
-            {/* 用户名输入框 */}
-            <View className="space-y-2">
-              <Text className="text-sm font-medium text-gray-700">用户名</Text>
-              <Input
-                leftIcon="i-heroicons-user-20-solid"
-                placeholder="请输入用户名"
-                value={username}
-                onInput={handleUsernameInput}
-                maxlength={20}
-                type="text"
-                confirmType="next"
-                size="lg"
-                variant="filled"
+          <Form {...form}>
+            <View className="space-y-5">
+              <FormField
+                control={form.control}
+                name="username"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>用户名</FormLabel>
+                    <FormControl>
+                      <Input
+                        leftIcon="i-heroicons-user-20-solid"
+                        placeholder="请输入用户名"
+                        maxlength={20}
+                        type="text"
+                        confirmType="next"
+                        size="lg"
+                        variant="filled"
+                        {...field}
+                      />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
               />
-            </View>
 
-            {/* 密码输入框 */}
-            <View className="space-y-2">
-              <Text className="text-sm font-medium text-gray-700">密码</Text>
-              <Input
-                leftIcon="i-heroicons-lock-closed-20-solid"
-                rightIcon={showPassword ? "i-heroicons-eye-20-solid" : "i-heroicons-eye-slash-20-solid"}
-                placeholder="请输入密码"
-                password={!showPassword}
-                value={password}
-                onInput={handlePasswordInput}
-                maxlength={20}
-                confirmType="done"
-                size="lg"
-                variant="filled"
-                onRightIconClick={() => setShowPassword(!showPassword)}
+              <FormField
+                control={form.control}
+                name="password"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>密码</FormLabel>
+                    <FormControl>
+                      <Input
+                        leftIcon="i-heroicons-lock-closed-20-solid"
+                        rightIcon={showPassword ? "i-heroicons-eye-20-solid" : "i-heroicons-eye-slash-20-solid"}
+                        placeholder="请输入密码"
+                        password={!showPassword}
+                        maxlength={20}
+                        confirmType="done"
+                        size="lg"
+                        variant="filled"
+                        onRightIconClick={() => setShowPassword(!showPassword)}
+                        {...field}
+                      />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
               />
-            </View>
 
-            {/* 忘记密码 */}
-            <View className="flex justify-end">
-              <Text className="text-sm text-blue-500 hover:text-blue-600">忘记密码?</Text>
+              {/* 忘记密码 */}
+              <View className="flex justify-end">
+                <Text className="text-sm text-blue-500 hover:text-blue-600">忘记密码?</Text>
+              </View>
+
+              {/* 登录按钮 */}
+              <Button
+                className={cn(
+                  "w-full",
+                  !form.formState.isValid || isLoading
+                    ? "bg-gray-300"
+                    : "bg-blue-500 hover:bg-blue-600"
+                )}
+                size="lg"
+                variant="default"
+                onClick={form.handleSubmit(onSubmit)}
+                disabled={!form.formState.isValid || isLoading}
+              >
+                {isLoading ? (
+                  <View className="flex items-center justify-center">
+                    <View className="i-heroicons-arrow-path-20-solid animate-spin w-5 h-5 mr-2" />
+                    登录中...
+                  </View>
+                ) : (
+                  '安全登录'
+                )}
+              </Button>
             </View>
-
-            {/* 登录按钮 */}
-            <Button
-              className={cn(
-                "w-full",
-                isLoading || !username || !password
-                  ? "bg-gray-300"
-                  : "bg-blue-500 hover:bg-blue-600"
-              )}
-              size="lg"
-              variant="default"
-              onClick={handleLogin}
-              disabled={isLoading || !username || !password}
-            >
-              {isLoading ? (
-                <View className="flex items-center justify-center">
-                  <View className="i-heroicons-arrow-path-20-solid animate-spin w-5 h-5 mr-2" />
-                  登录中...
-                </View>
-              ) : (
-                '安全登录'
-              )}
-            </Button>
-          </View>
+          </Form>
 
           {/* 注册链接 */}
           <View className="mt-6 text-center">
             <Text className="text-sm text-gray-600">
-              还没有账号? 
-              <Text 
-                className="text-blue-500 font-medium hover:text-blue-600" 
+              还没有账号?
+              <Text
+                className="text-blue-500 font-medium hover:text-blue-600"
                 onClick={goToRegister}
               >
                 立即注册