Files
kevisual-home/packages/user-login/src/user/login/Login.tsx
2025-10-27 01:40:38 +08:00

343 lines
11 KiB
TypeScript

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]);
};