206 lines
6.5 KiB
TypeScript
206 lines
6.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
|
||
import { getVerificationCodeImage, loginByPassword } from '../../api';
|
||
import { Button, Input } from '../ui';
|
||
|
||
interface LoginSuccessPayload {
|
||
userId: string;
|
||
userName: string;
|
||
accessToken: string;
|
||
refreshToken: string;
|
||
roles: string;
|
||
username: string;
|
||
}
|
||
|
||
interface LoginModalProps {
|
||
onClose: () => void;
|
||
onLoginSuccess?: (payload: LoginSuccessPayload) => void;
|
||
}
|
||
|
||
const APP_ID = 'b2b49e91d21446aeb14579930f732985';
|
||
|
||
export const LoginModal = ({ onClose, onLoginSuccess }: LoginModalProps) => {
|
||
const [username, setUsername] = useState('');
|
||
const [password, setPassword] = useState('');
|
||
const [imgCode, setImgCode] = useState('');
|
||
const [imgCodeKey, setImgCodeKey] = useState('');
|
||
const [imgCodeUrl, setImgCodeUrl] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [imgLoading, setImgLoading] = useState(false);
|
||
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(() => {
|
||
fetchImageCode();
|
||
}, []);
|
||
|
||
const handleLogin = async () => {
|
||
if (!username.trim()) {
|
||
setError('请输入账号');
|
||
return;
|
||
}
|
||
if (!password) {
|
||
setError('请输入密码');
|
||
return;
|
||
}
|
||
if (!imgCode.trim()) {
|
||
setError('请输入图形验证码');
|
||
return;
|
||
}
|
||
if (!imgCodeKey) {
|
||
setError('验证码失效,请刷新');
|
||
return;
|
||
}
|
||
|
||
setError('');
|
||
setLoading(true);
|
||
try {
|
||
const res = await loginByPassword({
|
||
username: username.trim(),
|
||
password,
|
||
verification_code: imgCode.trim(),
|
||
verification_code_key: imgCodeKey,
|
||
});
|
||
if (res.Status === 200 && res.Data?.access_token) {
|
||
onLoginSuccess?.({
|
||
userId: res.Data.user_id,
|
||
userName: res.Data.user_name,
|
||
accessToken: res.Data.access_token,
|
||
refreshToken: res.Data.refresh_token,
|
||
roles: res.Data.roles,
|
||
username: username.trim(),
|
||
});
|
||
onClose();
|
||
} else {
|
||
setError(res.Message || '登录失败');
|
||
// 登录失败时刷新验证码
|
||
fetchImageCode();
|
||
}
|
||
} catch (err) {
|
||
console.error('登录失败', err);
|
||
setError('登录失败,请稍后重试');
|
||
fetchImageCode();
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const canLogin = !!(username && password && imgCode && imgCodeKey && !loading);
|
||
|
||
return (
|
||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/30' onClick={onClose}>
|
||
<div
|
||
className='w-[480px] max-w-[95vw] bg-white rounded-3xl shadow-xl overflow-hidden text-sm'
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className='px-4 py-3 border-b flex items-center justify-between'>
|
||
<div className='font-semibold'>操作员登录</div>
|
||
<button className='text-xs text-gray-500 hover:text-gray-700' onClick={onClose}>
|
||
关闭
|
||
</button>
|
||
</div>
|
||
|
||
<div className='px-4 py-6 bg-gray-50/60 space-y-4'>
|
||
<div className='space-y-2'>
|
||
<label className='text-xs text-gray-700 font-medium'>账号</label>
|
||
<Input
|
||
type='text'
|
||
placeholder='请输入账号'
|
||
value={username}
|
||
onChange={(e) => {
|
||
setUsername(e.target.value);
|
||
setError('');
|
||
}}
|
||
className='text-base'
|
||
/>
|
||
</div>
|
||
|
||
<div className='space-y-2'>
|
||
<label className='text-xs text-gray-700 font-medium'>密码</label>
|
||
<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
|
||
type='text'
|
||
placeholder='请输入验证码'
|
||
value={imgCode}
|
||
onChange={(e) => {
|
||
setImgCode(e.target.value);
|
||
setError('');
|
||
}}
|
||
className='text-base flex-1'
|
||
/>
|
||
<div className='w-28 h-10 border rounded-lg bg-white flex items-center justify-center overflow-hidden'>
|
||
{imgLoading ? (
|
||
<span className='text-[11px] text-gray-500'>加载中…</span>
|
||
) : imgCodeUrl ? (
|
||
<img
|
||
src={imgCodeUrl}
|
||
alt='验证码'
|
||
className='w-full h-full object-contain cursor-pointer'
|
||
onClick={fetchImageCode}
|
||
/>
|
||
) : (
|
||
<button className='text-[11px] text-blue-600' onClick={fetchImageCode}>
|
||
刷新
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className='text-[11px] text-gray-500'>看不清?点击图片刷新</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className='px-3 py-2 rounded-xl bg-red-50 border border-red-200 text-xs text-red-600'>{error}</div>
|
||
)}
|
||
|
||
<div className='pt-2'>
|
||
<Button
|
||
onClick={handleLogin}
|
||
disabled={!canLogin}
|
||
className={`w-full font-bold justify-center ${!canLogin ? 'opacity-50 cursor-not-allowed' : 'bg-gray-900 hover:bg-gray-800'}`}
|
||
>
|
||
{loading ? <span className='text-white font-bold'>登录中...</span> : (
|
||
canLogin ? <span className='text-white font-bold'>登录</span> : <span className='text-black font-bold'>登录</span>
|
||
)}
|
||
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|