完善登录面板

This commit is contained in:
xianyi
2025-12-16 17:42:58 +08:00
parent 4f9b508e22
commit f8a7b41463

View File

@@ -1,94 +1,94 @@
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
import { getVerificationCodeImage, loginByPassword } from '../../api';
import { Button, Input } from '../ui'; import { Button, Input } from '../ui';
interface LoginModalProps { interface LoginModalProps {
onClose: () => void; onClose: () => void;
onLoginSuccess?: (phone: string) => void; onLoginSuccess?: (userName: string) => void;
} }
const APP_ID = 'b2b49e91d21446aeb14579930f732985';
export const LoginModal = ({ onClose, onLoginSuccess }: LoginModalProps) => { export const LoginModal = ({ onClose, onLoginSuccess }: LoginModalProps) => {
const [phone, setPhone] = useState(''); const [username, setUsername] = useState('');
const [code, setCode] = useState(''); const [password, setPassword] = useState('');
const [countdown, setCountdown] = useState(0); const [imgCode, setImgCode] = useState('');
const [imgCodeKey, setImgCodeKey] = useState('');
const [imgCodeUrl, setImgCodeUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [imgLoading, setImgLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
// 验证码倒计时 const fetchImageCode = async () => {
setImgLoading(true);
setError('');
try {
const res = await getVerificationCodeImage({ app_id: APP_ID });
if (res.Status === 200 && res.Data) {
setImgCodeKey(res.Data.verification_code_key || '');
// 后端返回的可能是 base64直接作为 src
setImgCodeUrl(res.Data.verification_code_image || null);
} else {
setError(res.Message || '获取图形验证码失败');
}
} catch (err) {
console.error('获取图形验证码失败', err);
setError('获取图形验证码失败,请稍后重试');
} finally {
setImgLoading(false);
}
};
useEffect(() => { useEffect(() => {
if (countdown > 0) { fetchImageCode();
const timer = setTimeout(() => setCountdown(countdown - 1), 1000); }, []);
return () => clearTimeout(timer);
}
}, [countdown]);
// 验证手机号格式
const validatePhone = (phoneNumber: string): boolean => {
return /^1[3-9]\d{9}$/.test(phoneNumber);
};
// 发送验证码
const handleSendCode = async () => {
if (!phone) {
setError('请输入手机号');
return;
}
if (!validatePhone(phone)) {
setError('请输入正确的手机号');
return;
}
setError('');
setLoading(true);
// 模拟发送验证码 API 调用
await new Promise((resolve) => setTimeout(resolve, 1000));
setLoading(false);
setCountdown(60); // 60秒倒计时
// 实际项目中,这里应该调用后端 API 发送验证码
// 开发环境可以显示验证码例如123456
// eslint-disable-next-line no-console
console.log('验证码已发送开发环境123456');
};
// 登录
const handleLogin = async () => { const handleLogin = async () => {
if (!phone) { if (!username.trim()) {
setError('请输入手机号'); setError('请输入号');
return; return;
} }
if (!validatePhone(phone)) { if (!password) {
setError('请输入正确的手机号'); setError('请输入密码');
return; return;
} }
if (!code) { if (!imgCode.trim()) {
setError('请输入验证码'); setError('请输入图形验证码');
return; return;
} }
if (code.length !== 6) { if (!imgCodeKey) {
setError('验证码应为6位数字'); setError('验证码失效,请刷新');
return; return;
} }
setError(''); setError('');
setLoading(true); setLoading(true);
try {
// 模拟登录 API 调用 const res = await loginByPassword({
await new Promise((resolve) => setTimeout(resolve, 1500)); username: username.trim(),
password,
// 开发环境:验证码为 123456 时通过 verification_code: imgCode.trim(),
if (code === '123456') { verification_code_key: imgCodeKey,
});
if (res.Status === 200 && res.Data?.access_token) {
onLoginSuccess?.(res.Data.user_name || username.trim());
onClose();
} else {
setError(res.Message || '登录失败');
// 登录失败时刷新验证码
fetchImageCode();
}
} catch (err) {
console.error('登录失败', err);
setError('登录失败,请稍后重试');
fetchImageCode();
} finally {
setLoading(false); setLoading(false);
onLoginSuccess?.(phone);
onClose();
} else {
setLoading(false);
setError('验证码错误,请重新输入');
} }
}; };
const canSendCode = countdown === 0 && !loading && phone.length === 11; const canLogin = !!(username && password && imgCode && imgCodeKey && !loading);
const canLogin = phone && code && !loading;
return ( return (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/30' onClick={onClose}> <div className='fixed inset-0 z-50 flex items-center justify-center bg-black/30' onClick={onClose}>
@@ -105,47 +105,64 @@ export const LoginModal = ({ onClose, onLoginSuccess }: LoginModalProps) => {
<div className='px-4 py-6 bg-gray-50/60 space-y-4'> <div className='px-4 py-6 bg-gray-50/60 space-y-4'>
<div className='space-y-2'> <div className='space-y-2'>
<label className='text-xs text-gray-700 font-medium'></label> <label className='text-xs text-gray-700 font-medium'></label>
<Input <Input
type='tel' type='text'
placeholder='请输入手机号' placeholder='请输入号'
value={phone} value={username}
onChange={(e) => { onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 11); setUsername(e.target.value);
setPhone(value);
setError(''); setError('');
}} }}
maxLength={11}
className='text-base' className='text-base'
/> />
</div> </div>
<div className='space-y-2'> <div className='space-y-2'>
<label className='text-xs text-gray-700 font-medium'></label> <label className='text-xs text-gray-700 font-medium'></label>
<div className='flex gap-2'> <Input
type='password'
placeholder='请输入密码'
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError('');
}}
className='text-base'
/>
</div>
<div className='space-y-2'>
<label className='text-xs text-gray-700 font-medium'></label>
<div className='flex items-center gap-2'>
<Input <Input
type='text' type='text'
placeholder='请输入6位验证码' placeholder='请输入验证码'
value={code} value={imgCode}
onChange={(e) => { onChange={(e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 6); setImgCode(e.target.value);
setCode(value);
setError(''); setError('');
}} }}
maxLength={6}
className='text-base flex-1' className='text-base flex-1'
/> />
<Button <div className='w-28 h-10 border rounded-lg bg-white flex items-center justify-center overflow-hidden'>
onClick={handleSendCode} {imgLoading ? (
disabled={!canSendCode} <span className='text-[11px] text-gray-500'></span>
className={!canSendCode ? 'opacity-50 cursor-not-allowed' : ''} ) : imgCodeUrl ? (
> <img
{countdown > 0 ? `${countdown}` : loading ? '发送中...' : '发送验证码'} src={imgCodeUrl}
</Button> alt='验证码'
</div> className='w-full h-full object-contain cursor-pointer'
<div className='text-[11px] text-gray-500'> onClick={fetchImageCode}
<span className='font-mono font-semibold'>123456</span> />
) : (
<button className='text-[11px] text-blue-600' onClick={fetchImageCode}>
</button>
)}
</div>
</div> </div>
<div className='text-[11px] text-gray-500'></div>
</div> </div>
{error && ( {error && (
@@ -156,9 +173,9 @@ export const LoginModal = ({ onClose, onLoginSuccess }: LoginModalProps) => {
<Button <Button
onClick={handleLogin} onClick={handleLogin}
disabled={!canLogin} disabled={!canLogin}
className={`w-full justify-center ${!canLogin ? 'opacity-50 cursor-not-allowed' : 'bg-gray-900 hover:bg-gray-800'}`} className={`w-full font-bold text-white justify-center ${!canLogin ? 'opacity-50 cursor-not-allowed' : 'bg-gray-900 hover:bg-gray-800'}`}
> >
{loading ? '· · ·' : '登录'} {loading ? '登录中…' : '登录'}
</Button> </Button>
</div> </div>
</div> </div>