|
|
@@ -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 && (
|