feat: 初始化项目并完成基础功能开发
- 完成项目初始化
- 实现用户注册、登录功能
- 完成用户管理与权限管理模块
- 开发后端 Tracker 服务器项目管理接口
- 实现日志管理接口
Change-Id: Ia4bde1c9ff600352a7ff0caca0cc50b02cad1af7
diff --git a/react-ui/src/pages/User/Login/index.tsx b/react-ui/src/pages/User/Login/index.tsx
new file mode 100644
index 0000000..a18f785
--- /dev/null
+++ b/react-ui/src/pages/User/Login/index.tsx
@@ -0,0 +1,436 @@
+import Footer from '@/components/Footer';
+import { getCaptchaImg, login } from '@/services/system/auth';
+import { getFakeCaptcha } from '@/services/ant-design-pro/login';
+import {
+ AlipayCircleOutlined,
+ LockOutlined,
+ MobileOutlined,
+ TaobaoCircleOutlined,
+ UserOutlined,
+ WeiboCircleOutlined,
+} from '@ant-design/icons';
+import {
+ LoginForm,
+ ProFormCaptcha,
+ ProFormCheckbox,
+ ProFormText,
+} from '@ant-design/pro-components';
+import { useEmotionCss } from '@ant-design/use-emotion-css';
+import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
+import { Alert, Col, message, Row, Tabs, Image } from 'antd';
+import Settings from '../../../../config/defaultSettings';
+import React, { useEffect, useState } from 'react';
+import { flushSync } from 'react-dom';
+import { clearSessionToken, setSessionToken } from '@/access';
+
+const ActionIcons = () => {
+ const langClassName = useEmotionCss(({ token }) => {
+ return {
+ marginLeft: '8px',
+ color: 'rgba(0, 0, 0, 0.2)',
+ fontSize: '24px',
+ verticalAlign: 'middle',
+ cursor: 'pointer',
+ transition: 'color 0.3s',
+ '&:hover': {
+ color: token.colorPrimaryActive,
+ },
+ };
+ });
+
+ return (
+ <>
+ <AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
+ <TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
+ <WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
+ </>
+ );
+};
+
+const Lang = () => {
+ const langClassName = useEmotionCss(({ token }) => {
+ return {
+ width: 42,
+ height: 42,
+ lineHeight: '42px',
+ position: 'fixed',
+ right: 16,
+ borderRadius: token.borderRadius,
+ ':hover': {
+ backgroundColor: token.colorBgTextHover,
+ },
+ };
+ });
+
+ return (
+ <div className={langClassName} data-lang>
+ {SelectLang && <SelectLang />}
+ </div>
+ );
+};
+
+const LoginMessage: React.FC<{
+ content: string;
+}> = ({ content }) => {
+ return (
+ <Alert
+ style={{
+ marginBottom: 24,
+ }}
+ message={content}
+ type="error"
+ showIcon
+ />
+ );
+};
+
+const Login: React.FC = () => {
+ const [userLoginState, setUserLoginState] = useState<API.LoginResult>({code: 200});
+ const [type, setType] = useState<string>('account');
+ const { initialState, setInitialState } = useModel('@@initialState');
+ const [captchaCode, setCaptchaCode] = useState<string>('');
+ const [uuid, setUuid] = useState<string>('');
+
+ const containerClassName = useEmotionCss(() => {
+ return {
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100vh',
+ overflow: 'auto',
+ backgroundImage:
+ "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
+ backgroundSize: '100% 100%',
+ };
+ });
+
+ const intl = useIntl();
+
+ const getCaptchaCode = async () => {
+ const response = await getCaptchaImg();
+ const imgdata = `data:image/png;base64,${response.img}`;
+ setCaptchaCode(imgdata);
+ setUuid(response.uuid);
+ };
+
+ const fetchUserInfo = async () => {
+ const userInfo = await initialState?.fetchUserInfo?.();
+ if (userInfo) {
+ flushSync(() => {
+ setInitialState((s) => ({
+ ...s,
+ currentUser: userInfo,
+ }));
+ });
+ }
+ };
+
+ const handleSubmit = async (values: API.LoginParams) => {
+ try {
+ // 登录
+ const response = await login({ ...values, uuid });
+ if (response.code === 200) {
+ const defaultLoginSuccessMessage = intl.formatMessage({
+ id: 'pages.login.success',
+ defaultMessage: '登录成功!',
+ });
+ const current = new Date();
+ const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
+ console.log('login response: ', response);
+ setSessionToken(response?.token, response?.token, expireTime);
+ message.success(defaultLoginSuccessMessage);
+ await fetchUserInfo();
+ console.log('login ok');
+ const urlParams = new URL(window.location.href).searchParams;
+ history.push(urlParams.get('redirect') || '/');
+ return;
+ } else {
+ console.log(response.msg);
+ clearSessionToken();
+ // 如果失败去设置用户错误信息
+ setUserLoginState({ ...response, type });
+ getCaptchaCode();
+ }
+ } catch (error) {
+ const defaultLoginFailureMessage = intl.formatMessage({
+ id: 'pages.login.failure',
+ defaultMessage: '登录失败,请重试!',
+ });
+ console.log(error);
+ message.error(defaultLoginFailureMessage);
+ }
+ };
+ const { code } = userLoginState;
+ const loginType = type;
+
+ useEffect(() => {
+ getCaptchaCode();
+ }, []);
+
+ return (
+ <div className={containerClassName}>
+ <Helmet>
+ <title>
+ {intl.formatMessage({
+ id: 'menu.login',
+ defaultMessage: '登录页',
+ })}
+ - {Settings.title}
+ </title>
+ </Helmet>
+ <Lang />
+ <div
+ style={{
+ flex: '1',
+ padding: '32px 0',
+ }}
+ >
+ <LoginForm
+ contentStyle={{
+ minWidth: 280,
+ maxWidth: '75vw',
+ }}
+ logo={<img alt="logo" src="/logo.svg" />}
+ title="Ant Design"
+ subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
+ initialValues={{
+ autoLogin: true,
+ }}
+ actions={[
+ <FormattedMessage
+ key="loginWith"
+ id="pages.login.loginWith"
+ defaultMessage="其他登录方式"
+ />,
+ <ActionIcons key="icons" />,
+ ]}
+ onFinish={async (values) => {
+ await handleSubmit(values as API.LoginParams);
+ }}
+ >
+ <Tabs
+ activeKey={type}
+ onChange={setType}
+ centered
+ items={[
+ {
+ key: 'account',
+ label: intl.formatMessage({
+ id: 'pages.login.accountLogin.tab',
+ defaultMessage: '账户密码登录',
+ }),
+ },
+ {
+ key: 'mobile',
+ label: intl.formatMessage({
+ id: 'pages.login.phoneLogin.tab',
+ defaultMessage: '手机号登录',
+ }),
+ },
+ ]}
+ />
+
+ {code !== 200 && loginType === 'account' && (
+ <LoginMessage
+ content={intl.formatMessage({
+ id: 'pages.login.accountLogin.errorMessage',
+ defaultMessage: '账户或密码错误(admin/admin123)',
+ })}
+ />
+ )}
+ {type === 'account' && (
+ <>
+ <ProFormText
+ name="username"
+ initialValue="admin"
+ fieldProps={{
+ size: 'large',
+ prefix: <UserOutlined />,
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.username.placeholder',
+ defaultMessage: '用户名: admin',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.username.required"
+ defaultMessage="请输入用户名!"
+ />
+ ),
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="password"
+ initialValue="admin123"
+ fieldProps={{
+ size: 'large',
+ prefix: <LockOutlined />,
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.password.placeholder',
+ defaultMessage: '密码: admin123',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.password.required"
+ defaultMessage="请输入密码!"
+ />
+ ),
+ },
+ ]}
+ />
+ <Row>
+ <Col flex={3}>
+ <ProFormText
+ style={{
+ float: 'right',
+ }}
+ name="code"
+ placeholder={intl.formatMessage({
+ id: 'pages.login.captcha.placeholder',
+ defaultMessage: '请输入验证',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.searchTable.updateForm.ruleName.nameRules"
+ defaultMessage="请输入验证啊"
+ />
+ ),
+ },
+ ]}
+ />
+ </Col>
+ <Col flex={2}>
+ <Image
+ src={captchaCode}
+ alt="验证码"
+ style={{
+ display: 'inline-block',
+ verticalAlign: 'top',
+ cursor: 'pointer',
+ paddingLeft: '10px',
+ width: '100px',
+ }}
+ preview={false}
+ onClick={() => getCaptchaCode()}
+ />
+ </Col>
+ </Row>
+ </>
+ )}
+
+ {code !== 200 && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
+ {type === 'mobile' && (
+ <>
+ <ProFormText
+ fieldProps={{
+ size: 'large',
+ prefix: <MobileOutlined />,
+ }}
+ name="mobile"
+ placeholder={intl.formatMessage({
+ id: 'pages.login.phoneNumber.placeholder',
+ defaultMessage: '手机号',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.phoneNumber.required"
+ defaultMessage="请输入手机号!"
+ />
+ ),
+ },
+ {
+ pattern: /^1\d{10}$/,
+ message: (
+ <FormattedMessage
+ id="pages.login.phoneNumber.invalid"
+ defaultMessage="手机号格式错误!"
+ />
+ ),
+ },
+ ]}
+ />
+ <ProFormCaptcha
+ fieldProps={{
+ size: 'large',
+ prefix: <LockOutlined />,
+ }}
+ captchaProps={{
+ size: 'large',
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.captcha.placeholder',
+ defaultMessage: '请输入验证码',
+ })}
+ captchaTextRender={(timing, count) => {
+ if (timing) {
+ return `${count} ${intl.formatMessage({
+ id: 'pages.getCaptchaSecondText',
+ defaultMessage: '获取验证码',
+ })}`;
+ }
+ return intl.formatMessage({
+ id: 'pages.login.phoneLogin.getVerificationCode',
+ defaultMessage: '获取验证码',
+ });
+ }}
+ name="captcha"
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.captcha.required"
+ defaultMessage="请输入验证码!"
+ />
+ ),
+ },
+ ]}
+ onGetCaptcha={async (phone) => {
+ const result = await getFakeCaptcha({
+ phone,
+ });
+ if (!result) {
+ return;
+ }
+ message.success('获取验证码成功!验证码为:1234');
+ }}
+ />
+ </>
+ )}
+ <div
+ style={{
+ marginBottom: 24,
+ }}
+ >
+ <ProFormCheckbox noStyle name="autoLogin">
+ <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
+ </ProFormCheckbox>
+ <a
+ style={{
+ float: 'right',
+ }}
+ >
+ <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
+ </a>
+ </div>
+ </LoginForm>
+ </div>
+ <Footer />
+ </div>
+ );
+};
+
+export default Login;