LoginPage.tsx 3.75 KB
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import { authApi } from '../../api/auth';
import type { LoginReq } from '../../api/auth';
import { BizError, isBizError } from '../../api/errors';
import { useAppDispatch } from '../../store/hooks';
import { setSession } from '../../store/slices/authSlice';
import { ERROR_MESSAGES } from './loginConstants';
import LoginForm, { LoginFormFieldErrors } from './LoginForm';
import LoginHero from './LoginHero';
import LoginFooter from './LoginFooter';

export default function LoginPage() {
  const dispatch = useAppDispatch();
  const navigate = useNavigate();

  const [loading, setLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const [fieldErrors, setFieldErrors] = useState<LoginFormFieldErrors>({});
  const [lockUntil, setLockUntil] = useState<dayjs.Dayjs | null>(null);

  // 锁定倒计时:每秒检查 lockUntil 是否过期,过期后自动允许重试
  useEffect(() => {
    if (!lockUntil) return;
    const timer = setInterval(() => {
      if (dayjs().isAfter(lockUntil)) {
        setLockUntil(null);
        setErrorMessage(null);
      }
    }, 1000);
    return () => clearInterval(timer);
  }, [lockUntil]);

  const isLocked = lockUntil != null && dayjs().isBefore(lockUntil);

  const handleSubmit = async (req: LoginReq) => {
    if (isLocked) return;
    setLoading(true);
    setErrorMessage(null);
    setFieldErrors({});

    try {
      const vo = await authApi.login(req);
      dispatch(setSession({ accessToken: vo.accessToken, userInfo: vo.userInfo }));
      navigate('/users', { replace: true });
    } catch (e) {
      if (isBizError(e)) {
        handleBizError(e);
      } else {
        setErrorMessage(ERROR_MESSAGES.UNKNOWN as string);
      }
    } finally {
      setLoading(false);
    }
  };

  const handleBizError = (e: BizError) => {
    if (e.code === 42301) {
      const data = e.data as { lockUntil?: string } | undefined;
      const lockMoment = data?.lockUntil ? dayjs(data.lockUntil) : null;
      const lockTime = lockMoment ? lockMoment.format('HH:mm') : '稍后';
      setErrorMessage((ERROR_MESSAGES[42301] as string).replace('{lockUntil}', lockTime));
      if (lockMoment) setLockUntil(lockMoment);
    } else if (e.code === 40004) {
      setFieldErrors({ companyCode: ERROR_MESSAGES[40004] as string });
    } else if (e.code === 40103) {
      setErrorMessage(ERROR_MESSAGES[40103] as string);
    } else if (e.code === 40101) {
      setErrorMessage(ERROR_MESSAGES[40101] as string);
    } else if (e.code === 40001) {
      setErrorMessage(e.message || (ERROR_MESSAGES[40001] as string));
    } else if (e.code === -1 && e.message === 'NETWORK') {
      setErrorMessage(ERROR_MESSAGES.NETWORK as string);
    } else {
      setErrorMessage(e.message || (ERROR_MESSAGES.UNKNOWN as string));
    }
  };

  return (
    <div className="login-wrap" data-testid="login-page">
      <div className="login-head">
        <span className="name">Antler ERP</span>
        <span className="sub">欢迎登录EBC平台</span>
      </div>
      <div
        className="login-body"
        style={{ display: 'flex', gap: 32, padding: 24 }}
      >
        <LoginHero />
        <div
          className="login-card"
          style={{
            width: 360,
            padding: 24,
            background: 'var(--color-bg-container)',
            borderRadius: 8,
          }}
        >
          <h3>用户登录</h3>
          <LoginForm
            onSubmit={handleSubmit}
            loading={loading}
            errorMessage={errorMessage}
            fieldErrors={fieldErrors}
            submitDisabled={isLocked}
          />
        </div>
      </div>
      <LoginFooter />
    </div>
  );
}