完善登录面板
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user