update
This commit is contained in:
342
packages/user-login/src/user/login/Login.tsx
Normal file
342
packages/user-login/src/user/login/Login.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { useUserStore } from '../store';
|
||||
import { setWxerwma, wxId } from '@/wx/ws-login.ts';
|
||||
import { checkCaptcha } from '@/wx/tencent-captcha.ts';
|
||||
import { dynimicLoadTcapTcha } from '@/wx/load-js.ts';
|
||||
import { message } from '@/modules/message';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { WeChatMpLogin } from './modules/WeChatMpLogin';
|
||||
const WeChatLogin: React.FC = () => {
|
||||
const userStore = useUserStore(
|
||||
useShallow((state) => {
|
||||
return { config: state.config! };
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
setWxerwma({
|
||||
...userStore.config.wxLogin,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <div id={wxId} className='max-w-sm mx-auto bg-white rounded-lg text-center'></div>;
|
||||
};
|
||||
|
||||
type VerificationCodeInputProps = {
|
||||
onGetCode: () => void;
|
||||
verificationCode: string;
|
||||
setVerificationCode: (value: string) => void;
|
||||
};
|
||||
const VerificationCodeInput = forwardRef(({ onGetCode, verificationCode, setVerificationCode }: VerificationCodeInputProps, ref) => {
|
||||
// const [verificationCode, setVerificationCode] = useState('')
|
||||
const [isCounting, setIsCounting] = useState(false);
|
||||
const [countdown, setCountdown] = useState(60);
|
||||
useImperativeHandle(ref, () => ({
|
||||
isCounting,
|
||||
setIsCounting,
|
||||
setCountdown,
|
||||
}));
|
||||
const handleGetCode = () => {
|
||||
if (!isCounting) {
|
||||
// setIsCounting(true)
|
||||
// setCountdown(60)
|
||||
onGetCode(); // 调用父组件传入的获取验证码逻辑
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (isCounting) {
|
||||
timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
setIsCounting(false);
|
||||
clearInterval(timer);
|
||||
return 60;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [isCounting]);
|
||||
|
||||
return (
|
||||
<div className='mb-4 items-center'>
|
||||
<label className='block text-[#F39800] py-1 mb-1'>验证码</label>
|
||||
<div className='flex'>
|
||||
<input
|
||||
type='text'
|
||||
className='border-[#FBBF24] rounded-lg p-2 border focus:outline-none focus:ring-2 focus:ring-[#F39800]'
|
||||
placeholder='请输入验证码'
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={`ml-2 px-4 py-2 w-[120px] rounded-md text-white ${isCounting ? 'bg-gray-400 cursor-not-allowed' : 'bg-[#F39800] hover:bg-yellow-400'}`}
|
||||
onClick={handleGetCode}
|
||||
disabled={isCounting}>
|
||||
{isCounting ? `${countdown}s 后重试` : '获取验证码'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function PhoneNumberValidation({ phoneNumber, setPhoneNumber }) {
|
||||
// const [phoneNumber, setPhoneNumber] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const validatePhoneNumber = (number) => {
|
||||
// 假设手机号的格式为中国的11位数字
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(number)) {
|
||||
setErrorMessage('请输入有效的手机号');
|
||||
} else {
|
||||
setErrorMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setPhoneNumber(value);
|
||||
validatePhoneNumber(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className=''>
|
||||
<label className='block text-[#F39800] py-1 mb-1'>手机号</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`w-full border rounded-lg p-2 focus:outline-none focus:ring-2 ${
|
||||
errorMessage ? 'border-red-500 focus:ring-red-500' : 'border-[#FBBF24] focus:ring-[#F39800]'
|
||||
}`}
|
||||
placeholder='请输入手机号'
|
||||
value={phoneNumber}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{errorMessage && <p className='text-red-500 text-xs mt-1'>{errorMessage}</p>}
|
||||
{!errorMessage && <p className='text-gray-500 text-xs mt-1 invisible'>请输入11位手机号</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountLogin({ accountName, setAccountName, password, setPassword }) {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const validateAccountName = (name) => {
|
||||
if (name.length < 3) {
|
||||
setErrorMessage('账户名至少需要3个字符');
|
||||
} else {
|
||||
setErrorMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setAccountName(value);
|
||||
validateAccountName(value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
const onTestAccountLogin = () => {
|
||||
setAccountName('demo');
|
||||
setPassword('123456');
|
||||
};
|
||||
return (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<label className='block text-[#F39800] py-1 mb-1'>账户名</label>
|
||||
<input
|
||||
type='text'
|
||||
className={`w-full border rounded-lg p-2 focus:outline-none focus:ring-2 ${
|
||||
errorMessage ? 'border-red-500 focus:ring-red-500' : 'border-[#FBBF24] focus:ring-[#F39800]'
|
||||
}`}
|
||||
placeholder='请输入账户名'
|
||||
value={accountName}
|
||||
onChange={handleAccountChange}
|
||||
/>
|
||||
{errorMessage && <p className='text-red-500 text-xs mt-1'>{errorMessage}</p>}
|
||||
{!errorMessage && <p className='text-gray-500 text-xs mt-1 invisible'>账户名至少需要3个字符</p>}
|
||||
|
||||
<label className='block text-[#F39800] py-1 mb-1 mt-2'>密码</label>
|
||||
<input
|
||||
type='password'
|
||||
className='w-full border-[#FBBF24] rounded-lg p-2 border focus:outline-none focus:ring-2 focus:ring-[#F39800]'
|
||||
placeholder='请输入密码'
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
|
||||
<div
|
||||
className='text-xs text-gray-400/60 mt-2 hover:text-gray-500 cursor-pointer'
|
||||
onClick={() => {
|
||||
onTestAccountLogin();
|
||||
}}>
|
||||
试用账号登录
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LoginForm: React.FC = () => {
|
||||
const [phoneNumber, setPhoneNumber] = useState('');
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [accountName, setAccountName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'phone' | 'wechat' | 'wechat-mp' | 'account'>('phone');
|
||||
const userStore = useUserStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
config: state.config! || {},
|
||||
getCode: state.getCode,
|
||||
login: state.login,
|
||||
loginByAccount: state.loginByAccount,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const ref = useRef<any>(null);
|
||||
const handleGetCode = async () => {
|
||||
const loaded = await dynimicLoadTcapTcha();
|
||||
if (!loaded) {
|
||||
message.error('验证码加载失败');
|
||||
return;
|
||||
}
|
||||
const captcha = await checkCaptcha(userStore.config.captchaAppId);
|
||||
if (captcha.ret !== 0) {
|
||||
message.error('验证码发送失败');
|
||||
return;
|
||||
}
|
||||
ref.current.setIsCounting(true);
|
||||
ref.current.setCountdown(60);
|
||||
userStore.getCode(phoneNumber, captcha);
|
||||
};
|
||||
useEffect(() => {
|
||||
dynimicLoadTcapTcha();
|
||||
if (userStore.config.loginWay?.length > 0) {
|
||||
setActiveTab(userStore.config.loginWay[0]);
|
||||
}
|
||||
}, [userStore.config.loginWay]);
|
||||
const handleLogin = () => {
|
||||
// alert(`登录中:手机号: ${phoneNumber}, 验证码: ${verificationCode}`)
|
||||
userStore.login(phoneNumber, verificationCode);
|
||||
};
|
||||
const inLoginWay = (way: string) => {
|
||||
const loginWay = userStore.config?.loginWay || [];
|
||||
return loginWay.includes(way);
|
||||
};
|
||||
const handleAccountLogin = () => {
|
||||
if (!accountName || !password) {
|
||||
message.error('请输入账户名和密码');
|
||||
return;
|
||||
}
|
||||
userStore.loginByAccount(accountName, password);
|
||||
};
|
||||
useListenEnter({ active: activeTab === 'phone', handleLogin });
|
||||
useListenEnter({ active: activeTab === 'account', handleLogin: handleAccountLogin });
|
||||
const tab = useMemo(() => {
|
||||
const phoneCom = (
|
||||
<button
|
||||
key='phone'
|
||||
className={`flex-1 py-2 font-medium ${activeTab === 'phone' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
|
||||
onClick={() => setActiveTab('phone')}>
|
||||
手机号登录
|
||||
</button>
|
||||
);
|
||||
const wechatCom = (
|
||||
<button
|
||||
key='wechat'
|
||||
className={`flex-1 py-2 font-medium ${activeTab === 'wechat' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
|
||||
onClick={() => setActiveTab('wechat')}>
|
||||
微信登录
|
||||
</button>
|
||||
);
|
||||
const wechatMpCom = (
|
||||
<button
|
||||
key='wechat-mp'
|
||||
className={`flex-1 py-2 font-medium ${activeTab === 'wechat-mp' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
|
||||
onClick={() => setActiveTab('wechat-mp')}>
|
||||
微信公众号登录
|
||||
</button>
|
||||
);
|
||||
const accountCom = (
|
||||
<button
|
||||
key='account'
|
||||
className={`flex-1 py-2 font-medium ${activeTab === 'account' ? 'border-[#F39800] text-[#F39800] border-b-2' : ''}`}
|
||||
onClick={() => setActiveTab('account')}>
|
||||
账号登录
|
||||
</button>
|
||||
);
|
||||
const coms: React.ReactNode[] = [];
|
||||
for (const way of userStore.config.loginWay) {
|
||||
if (way === 'phone') {
|
||||
coms.push(phoneCom);
|
||||
} else if (way === 'wechat') {
|
||||
coms.push(wechatCom);
|
||||
} else if (way === 'account') {
|
||||
coms.push(accountCom);
|
||||
} else if (way === 'wechat-mp') {
|
||||
coms.push(wechatMpCom);
|
||||
}
|
||||
}
|
||||
return coms;
|
||||
}, [userStore.config.loginWay, activeTab]);
|
||||
return (
|
||||
<div className='max-w-sm mx-auto p-6 bg-white rounded-lg flex flex-col items-center justify-center'>
|
||||
{/* Tabs */}
|
||||
<div className='flex text-gray-400 min-w-[360px]'>{tab}</div>
|
||||
|
||||
<div className='mt-4 min-h-[300px] w-full relative'>
|
||||
{/* Phone Login Form */}
|
||||
{activeTab === 'phone' && inLoginWay('phone') && (
|
||||
<div className='mt-4 pt-4 '>
|
||||
<PhoneNumberValidation phoneNumber={phoneNumber} setPhoneNumber={setPhoneNumber} />
|
||||
<VerificationCodeInput ref={ref} onGetCode={handleGetCode} verificationCode={verificationCode} setVerificationCode={setVerificationCode} />
|
||||
<button className='w-full mt-3 py-2 bg-[#F39800] text-white rounded-lg hover:bg-yellow-400' onClick={handleLogin}>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* WeChat Login Placeholder */}
|
||||
{activeTab === 'wechat' && inLoginWay('wechat') && (
|
||||
<div className='-mt-2 w-[310px] ml-[12px] flex flex-col justify-center text-center text-gray-500 absolute top-0 left-0 z-index-10'>
|
||||
<WeChatLogin />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'wechat-mp' && inLoginWay('wechat-mp') && (
|
||||
<div className='mt-2 w-[310px] ml-[12px] flex flex-col justify-center text-center '>
|
||||
<WeChatMpLogin />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'account' && inLoginWay('account') && (
|
||||
<div className='mt-4 pt-4 w-full '>
|
||||
<AccountLogin accountName={accountName} setAccountName={setAccountName} password={password} setPassword={setPassword} />
|
||||
<button className='w-full mt-3 py-2 bg-[#F39800] text-white rounded-lg hover:bg-yellow-400' onClick={handleAccountLogin}>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
|
||||
export const useListenEnter = (opts?: { active: boolean; handleLogin: () => void }) => {
|
||||
useEffect(() => {
|
||||
if (!opts?.active) {
|
||||
return;
|
||||
}
|
||||
const handleEnter = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
opts?.handleLogin?.();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleEnter);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleEnter);
|
||||
};
|
||||
}, [opts?.active, opts?.handleLogin]);
|
||||
};
|
||||
Reference in New Issue
Block a user