BaiLuoYan 5 днів тому
батько
коміт
df7bba89ff

+ 25 - 0
src/pages/Sys/Login/components/Lang.tsx

@@ -0,0 +1,25 @@
+import { SelectLang } from '@umijs/max';
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => ({
+  lang: {
+    width: 42,
+    height: 42,
+    lineHeight: '42px',
+    position: 'fixed' as const,
+    right: 16,
+    borderRadius: token.borderRadius,
+    ':hover': {
+      backgroundColor: token.colorBgTextHover,
+    },
+  },
+}));
+
+export const Lang = () => {
+  const { styles } = useStyles();
+  return (
+    <div className={styles.lang} data-lang>
+      {SelectLang && <SelectLang />}
+    </div>
+  );
+};

+ 9 - 0
src/pages/Sys/Login/components/LoginMessage.tsx

@@ -0,0 +1,9 @@
+import { Alert } from 'antd';
+
+interface Props {
+  content: string;
+}
+
+export const LoginMessage = ({ content }: Props) => (
+  <Alert style={{ marginBottom: 24 }} message={content} type="error" showIcon />
+);

+ 67 - 0
src/pages/Sys/Login/hooks/useCapWidget.ts

@@ -0,0 +1,67 @@
+import * as loginApi from '@/services/login';
+import { useEffect, useRef, useState } from 'react';
+import type { CapWidgetElement } from '../lib/types';
+
+export const useCapWidget = () => {
+  const capWidgetRef = useRef<CapWidgetElement | null>(null);
+  const [capToken, setCapToken] = useState('');
+  const [capEndpoint, setCapEndpoint] = useState('');
+  const [capError, setCapError] = useState('');
+
+  useEffect(() => {
+    loginApi.fetchCapEndpoint().then((res) => {
+      setCapEndpoint(res.data?.data ?? '');
+    });
+  }, []);
+
+  useEffect(() => {
+    if (!capEndpoint) return;
+
+    const widget = document.querySelector('#cap') as CapWidgetElement;
+    if (!widget) return;
+
+    capWidgetRef.current = widget;
+
+    const setCaptchaWidth = () => {
+      const shadowRoot = widget.shadowRoot;
+      if (!shadowRoot) return false;
+      const captchaElement = shadowRoot.querySelector('.captcha') as HTMLElement;
+      if (!captchaElement) return false;
+      captchaElement.style.width = '100%';
+      captchaElement.style.maxWidth = '100%';
+      return true;
+    };
+
+    const intervals: number[] = [100, 300, 500, 1000, 2000];
+    const timeouts = intervals.map((delay) => setTimeout(() => setCaptchaWidth(), delay));
+
+    const observer = new MutationObserver(() => setCaptchaWidth());
+    observer.observe(widget, { childList: true, subtree: true });
+
+    setTimeout(() => {
+      if (widget.shadowRoot) {
+        observer.observe(widget.shadowRoot, { childList: true, subtree: true });
+      }
+    }, 100);
+
+    const handleSolve = (e: Event) => {
+      setCapToken((e as CustomEvent).detail.token);
+      setCapError('');
+    };
+
+    widget.addEventListener('solve', handleSolve);
+
+    return () => {
+      widget.removeEventListener('solve', handleSolve);
+      timeouts.forEach(clearTimeout);
+      observer.disconnect();
+    };
+  }, [capEndpoint]);
+
+  const resetCap = () => {
+    setCapToken('');
+    capWidgetRef.current?.reset();
+  };
+
+  return { capToken, capEndpoint, capError, setCapError, resetCap };
+};

+ 26 - 0
src/pages/Sys/Login/hooks/useCaptcha.ts

@@ -0,0 +1,26 @@
+import * as loginApi from '@/services/login';
+import { useCallback, useEffect, useState } from 'react';
+
+export const useCaptcha = () => {
+  const [captchaInfo, setCaptchaInfo] = useState<API.CaptchaInfo | null>(null);
+  const [captchaLoading, setCaptchaLoading] = useState(false);
+
+  const refreshCaptcha = useCallback(() => {
+    setCaptchaLoading(true);
+    loginApi
+      .fetchCaptcha()
+      .then((res) => setCaptchaInfo(res.data ?? null))
+      .finally(() => setCaptchaLoading(false));
+  }, []);
+
+  useEffect(() => {
+    refreshCaptcha();
+  }, [refreshCaptcha]);
+
+  const onClickedCaptcha = () => {
+    if (captchaLoading) return;
+    refreshCaptcha();
+  };
+
+  return { captchaInfo, captchaLoading, refreshCaptcha, onClickedCaptcha };
+};

+ 104 - 0
src/pages/Sys/Login/hooks/useLoginForm.ts

@@ -0,0 +1,104 @@
+import globalConfig from '@/config';
+import * as loginApi from '@/services/login';
+import { message } from '@/utils/antdAppInstance';
+import { setToken } from '@/utils/authUtils';
+import { history, useIntl, useModel } from '@umijs/max';
+import { useState } from 'react';
+import { flushSync } from 'react-dom';
+import type { LoginFormValues } from '../lib/types';
+
+interface UseLoginFormParams {
+  captchaInfo: API.CaptchaInfo | null;
+  capToken: string;
+  setCapError: (error: string) => void;
+  resetCap: () => void;
+  refreshCaptcha: () => void;
+}
+
+export const useLoginForm = ({
+  captchaInfo,
+  capToken,
+  setCapError,
+  resetCap,
+  refreshCaptcha,
+}: UseLoginFormParams) => {
+  const { initialState, setInitialState } = useModel('@@initialState');
+  const intl = useIntl();
+  const [loginState, setLoginState] = useState({ errorCode: 0, errorMessage: '' });
+  const { managementKey } = globalConfig.security;
+
+  const fetchUserInfo = async () => {
+    const userInfo = await initialState?.fetchUserInfo?.();
+    if (!userInfo) return;
+    flushSync(() => {
+      setInitialState((s) => ({ ...s, currentUser: userInfo }));
+    });
+  };
+
+  const handleLoginSuccess = async (res: API.LoginResult) => {
+    setToken(res.data!);
+    message.success(intl.formatMessage({ id: 'pages.login.success' }));
+    await fetchUserInfo();
+    setTimeout(() => {
+      const urlParams = new URL(window.location.href).searchParams;
+      history.push(urlParams.get('redirect') || '/');
+    }, 1000);
+  };
+
+  const handleLoginError = (err: any) => {
+    if (err.name === 'BizError') {
+      setLoginState(err.info ?? {});
+    } else if (err.response) {
+      setLoginState({ errorMessage: err.message, errorCode: err.response.status });
+    } else if (err.request) {
+      setLoginState({ errorMessage: 'None response! Please retry.', errorCode: 503 });
+    } else {
+      setLoginState({ errorMessage: 'Request error, please retry.', errorCode: 50000 });
+    }
+    message.error(intl.formatMessage({ id: 'pages.login.failure' }));
+  };
+
+  /** cap.js 验证登录 */
+  const handleSubmit = (values: LoginFormValues) => {
+    if (!capToken) {
+      setCapError(
+        intl.formatMessage(
+          { id: 'pages.login.capWidget.required' },
+          { defaultMessage: '请完成人机验证' },
+        ),
+      );
+      return;
+    }
+    loginApi
+      .fetchAdminLoginByCap(
+        { username: values.username, password: values.password, managementKey, capToken },
+        { skipErrorHandler: true },
+      )
+      .then(handleLoginSuccess, (err) => {
+        handleLoginError(err);
+        resetCap();
+      });
+  };
+
+  /** 图片验证码登录 */
+  const handleSubmit2 = (values: LoginFormValues) => {
+    const { id } = captchaInfo!;
+    loginApi
+      .fetchAdminLogin(
+        {
+          username: values.username,
+          password: values.password,
+          managementKey,
+          captchaId: id,
+          captchaCode: values.captchaCode!,
+        },
+        { skipErrorHandler: true },
+      )
+      .then(handleLoginSuccess, (err) => {
+        handleLoginError(err);
+        refreshCaptcha();
+      });
+  };
+
+  return { loginState, handleSubmit, handleSubmit2 };
+};

+ 33 - 251
src/pages/Sys/Login/index.tsx

@@ -1,262 +1,53 @@
 import bgLogin from '@/assets/images/login-bg.png';
 import { Footer, Icon } from '@/components';
-import * as loginApi from '@/services/login';
-import { message } from '@/utils/antdAppInstance';
-import { setToken } from '@/utils/authUtils';
 import { LoadingOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
 import { LoginForm, ProFormText } from '@ant-design/pro-components';
 import '@cap.js/widget';
 import mcLogoSvg from '@svgs/multi-color/mc-logo.svg';
-import { FormattedMessage, Helmet, SelectLang, history, useIntl, useModel } from '@umijs/max';
-import { Alert, Spin } from 'antd';
+import { FormattedMessage, Helmet, useIntl } from '@umijs/max';
+import { Spin } from 'antd';
 import { createStyles } from 'antd-style';
 import dayjs from 'dayjs';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { flushSync } from 'react-dom';
+import React from 'react';
 import Settings from '../../../../config/defaultSettings';
-
-type CapWidgetElement = HTMLElement & {
-  verify: () => Promise<{ token: string }>;
-  reset: () => void;
-};
-
-interface LoginFormValues {
-  username: string;
-  password: string;
-  captchaCode?: string;
-}
+import { Lang } from './components/Lang';
+import { LoginMessage } from './components/LoginMessage';
+import { useCapWidget } from './hooks/useCapWidget';
+import { useCaptcha } from './hooks/useCaptcha';
+import { useLoginForm } from './hooks/useLoginForm';
+import type { LoginFormValues } from './lib/types';
 
 const defSettings = {
   ...Settings,
   title: process.env.REACT_APP_NAME,
 };
 
-const useStyles = createStyles(({ token }) => {
-  return {
-    lang: {
-      width: 42,
-      height: 42,
-      lineHeight: '42px',
-      position: 'fixed',
-      right: 16,
-      borderRadius: token.borderRadius,
-      ':hover': {
-        backgroundColor: token.colorBgTextHover,
-      },
-    },
-    container: {
-      display: 'flex',
-      flexDirection: 'column',
-      height: '100vh',
-      overflow: 'auto',
-      backgroundImage: `url('${bgLogin}')`,
-      backgroundSize: '100% 100%',
-    },
-  };
-});
-
-const Lang = () => {
-  const { styles } = useStyles();
-
-  return (
-    <div className={styles.lang} data-lang>
-      {SelectLang && <SelectLang />}
-    </div>
-  );
-};
-
-const LoginMessage: React.FC<{
-  content: string;
-}> = ({ content }) => {
-  return <Alert style={{ marginBottom: 24 }} message={content} type="error" showIcon />;
-};
+const useStyles = createStyles(() => ({
+  container: {
+    display: 'flex',
+    flexDirection: 'column' as const,
+    height: '100vh',
+    overflow: 'auto',
+    backgroundImage: `url('${bgLogin}')`,
+    backgroundSize: '100% 100%',
+  },
+}));
 
 const Login: React.FC = () => {
-  const [captchaInfo, setCaptchaInfo] = useState<API.CaptchaInfo | null>(null);
-  const [captchaLoading, setCaptchaLoading] = useState(false);
-
-  const refreshCaptcha = useCallback(() => {
-    setCaptchaLoading(true);
-    loginApi
-      .fetchCaptcha()
-      .then((res) => setCaptchaInfo(res.data ?? null))
-      .finally(() => setCaptchaLoading(false));
-  }, []);
-
-  useEffect(() => {
-    refreshCaptcha();
-  }, [refreshCaptcha]);
-
-  const onClickedCaptcha = () => {
-    if (captchaLoading) return;
-    refreshCaptcha();
-  };
-
-  const capWidgetRef = useRef<CapWidgetElement | null>(null);
-
-  const { initialState, setInitialState } = useModel('@@initialState');
   const { styles } = useStyles();
   const intl = useIntl();
 
-  const [capToken, setCapToken] = useState<string>('');
-  const [capEndpoint, setCapEndpoint] = useState<string>('');
-  const [capError, setCapError] = useState<string>('');
-
-  useEffect(() => {
-    loginApi.fetchCapEndpoint().then((res) => {
-      if (res.data?.data) {
-        setCapEndpoint(res.data.data);
-      } else {
-        setCapEndpoint('');
-      }
-    });
-  }, []);
-
-  useEffect(() => {
-    if (!capEndpoint) return;
-
-    const widget = document.querySelector('#cap') as CapWidgetElement;
-    if (!widget) return;
-
-    capWidgetRef.current = widget;
-
-    const setCaptchaWidth = () => {
-      const shadowRoot = widget.shadowRoot;
-      if (shadowRoot) {
-        const captchaElement = shadowRoot.querySelector('.captcha') as HTMLElement;
-        if (captchaElement) {
-          captchaElement.style.width = '100%';
-          captchaElement.style.maxWidth = '100%';
-          return true;
-        }
-      }
-      return false;
-    };
-
-    const intervals: number[] = [100, 300, 500, 1000, 2000];
-    const timeouts = intervals.map((delay) => setTimeout(() => setCaptchaWidth(), delay));
-
-    const observer = new MutationObserver(() => {
-      setCaptchaWidth();
-    });
-
-    observer.observe(widget, { childList: true, subtree: true });
-
-    setTimeout(() => {
-      if (widget.shadowRoot) {
-        observer.observe(widget.shadowRoot, { childList: true, subtree: true });
-      }
-    }, 100);
-
-    const handleSolve = (e: any) => {
-      setCapToken(e.detail.token);
-      setCapError('');
-    };
-
-    widget.addEventListener('solve', handleSolve);
-
-    return () => {
-      widget.removeEventListener('solve', handleSolve);
-      timeouts.forEach(clearTimeout);
-      observer.disconnect();
-    };
-  }, [capEndpoint]);
-
-  const [loginState, setLoginState] = useState<{
-    errorCode: number;
-    errorMessage: string;
-  }>({ errorCode: 0, errorMessage: '' });
-  const { errorMessage, errorCode } = loginState;
-
-  const fetchUserInfo = async () => {
-    const userInfo = await initialState?.fetchUserInfo?.();
-    if (userInfo) {
-      flushSync(() => {
-        setInitialState((s) => ({
-          ...s,
-          currentUser: userInfo,
-        }));
-      });
-    }
-  };
-
-  const handleLoginSuccess = async (res: API.LoginResult) => {
-    setToken(res.data!);
-    message.success(intl.formatMessage({ id: 'pages.login.success' }));
-    await fetchUserInfo();
-    setTimeout(() => {
-      const urlParams = new URL(window.location.href).searchParams;
-      history.push(urlParams.get('redirect') || '/');
-    }, 1000);
-  };
-
-  const handleLoginError = (err: any) => {
-    if (err.name === 'BizError') {
-      setLoginState(err.info ?? {});
-    } else if (err.response) {
-      setLoginState({
-        errorMessage: err.message,
-        errorCode: err.response.status,
-      });
-    } else if (err.request) {
-      setLoginState({
-        errorMessage: 'None response! Please retry.',
-        errorCode: 503,
-      });
-    } else {
-      setLoginState({
-        errorMessage: 'Request error, please retry.',
-        errorCode: 50000,
-      });
-    }
-    message.error(intl.formatMessage({ id: 'pages.login.failure' }));
-  };
-
-  const managementKey = process.env.REACT_APP_MANAGEMENT_KEY ?? '';
-
-  /** cap.js 验证登录 */
-  const handleSubmit = async (values: LoginFormValues) => {
-    if (!capToken) {
-      setCapError(
-        intl.formatMessage(
-          { id: 'pages.login.capWidget.required' },
-          { defaultMessage: '请完成人机验证' },
-        ),
-      );
-      return;
-    }
-
-    loginApi
-      .fetchAdminLoginByCap(
-        { username: values.username, password: values.password, managementKey, capToken },
-        { skipErrorHandler: true },
-      )
-      .then(handleLoginSuccess, (err) => {
-        handleLoginError(err);
-        setCapToken('');
-        capWidgetRef.current?.reset();
-      });
-  };
+  const { captchaInfo, captchaLoading, refreshCaptcha, onClickedCaptcha } = useCaptcha();
+  const { capToken, capEndpoint, capError, setCapError, resetCap } = useCapWidget();
+  const { loginState, handleSubmit, handleSubmit2 } = useLoginForm({
+    captchaInfo,
+    capToken,
+    setCapError,
+    resetCap,
+    refreshCaptcha,
+  });
 
-  /** 图片验证码登录 */
-  const handleSubmit2 = (values: LoginFormValues) => {
-    const { id } = captchaInfo!;
-    loginApi
-      .fetchAdminLogin(
-        {
-          username: values.username,
-          password: values.password,
-          managementKey,
-          captchaId: id,
-          captchaCode: values.captchaCode!,
-        },
-        { skipErrorHandler: true },
-      )
-      .then(handleLoginSuccess, (err) => {
-        handleLoginError(err);
-        refreshCaptcha();
-      });
-  };
+  const { errorCode, errorMessage } = loginState;
 
   return (
     <div className={styles.container}>
@@ -278,7 +69,7 @@ const Login: React.FC = () => {
           )}
           onFinish={async (values) => {
             if (capEndpoint !== '') {
-              await handleSubmit(values);
+              handleSubmit(values);
             } else {
               handleSubmit2(values);
             }
@@ -287,16 +78,10 @@ const Login: React.FC = () => {
           {errorCode !== 0 && <LoginMessage content={errorMessage} />}
           <ProFormText
             name="username"
-            fieldProps={{
-              size: 'large',
-              prefix: <UserOutlined />,
-            }}
+            fieldProps={{ size: 'large', prefix: <UserOutlined /> }}
             placeholder={intl.formatMessage({ id: 'pages.login.username.placeholder' })}
             rules={[
-              {
-                required: true,
-                message: <FormattedMessage id="pages.login.username.required" />,
-              },
+              { required: true, message: <FormattedMessage id="pages.login.username.required" /> },
             ]}
           />
           <ProFormText.Password
@@ -304,10 +89,7 @@ const Login: React.FC = () => {
             fieldProps={{ size: 'large', prefix: <LockOutlined /> }}
             placeholder={intl.formatMessage({ id: 'pages.login.password.placeholder' })}
             rules={[
-              {
-                required: true,
-                message: <FormattedMessage id="pages.login.password.required" />,
-              },
+              { required: true, message: <FormattedMessage id="pages.login.password.required" /> },
             ]}
           />
           {capEndpoint && (

+ 10 - 0
src/pages/Sys/Login/lib/types.ts

@@ -0,0 +1,10 @@
+export type CapWidgetElement = HTMLElement & {
+  verify: () => Promise<{ token: string }>;
+  reset: () => void;
+};
+
+export interface LoginFormValues {
+  username: string;
+  password: string;
+  captchaCode?: string;
+}