'init_again'
Change-Id: Ib7ecdb9f5baeab1e4681152a57b936edf7475b35
diff --git a/src/pages/User/Center/Center.less b/src/pages/User/Center/Center.less
new file mode 100644
index 0000000..430d88e
--- /dev/null
+++ b/src/pages/User/Center/Center.less
@@ -0,0 +1,61 @@
+
+.avatarHolder {
+ margin-bottom: 16px;
+ text-align: center;
+ position: relative;
+ display: inline-block;
+ height: 120px;
+
+ & > img {
+ width: 120px;
+ height: 120px;
+ margin-bottom: 20px;
+ border-radius: 50%;
+ }
+ &:hover:after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ color: #eee;
+ font-size: 24px;
+ font-style: normal;
+ line-height: 110px;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ cursor: pointer;
+ content: '+';
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+}
+
+.teamTitle {
+ margin-bottom: 12px;
+ color: @heading-color;
+ font-weight: 500;
+}
+
+.team {
+ :global {
+ .ant-avatar {
+ margin-right: 12px;
+ }
+ }
+
+ a {
+ display: block;
+ margin-bottom: 24px;
+ overflow: hidden;
+ color: @text-color;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ transition: color 0.3s;
+
+ &:hover {
+ color: @primary-color;
+ }
+ }
+}
diff --git a/src/pages/User/Center/components/AvatarCropper/cropper.css b/src/pages/User/Center/components/AvatarCropper/cropper.css
new file mode 100644
index 0000000..7f2f350
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/cropper.css
@@ -0,0 +1,309 @@
+/*!
+ * Cropper.js v1.5.13
+ * https://fengyuanchen.github.io/cropperjs
+ *
+ * Copyright 2015-present Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2022-11-20T05:30:43.444Z
+ */
+
+.cropper-container {
+ direction: ltr;
+ font-size: 0;
+ line-height: 0;
+ position: relative;
+ -ms-touch-action: none;
+ touch-action: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.cropper-container img {
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ display: block;
+ height: 100%;
+ image-orientation: 0deg;
+ max-height: none !important;
+ max-width: none !important;
+ min-height: 0 !important;
+ min-width: 0 !important;
+ width: 100%;
+ }
+
+.cropper-wrap-box,
+.cropper-canvas,
+.cropper-drag-box,
+.cropper-crop-box,
+.cropper-modal {
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.cropper-wrap-box,
+.cropper-canvas {
+ overflow: hidden;
+}
+
+.cropper-drag-box {
+ background-color: #fff;
+ opacity: 0;
+}
+
+.cropper-modal {
+ background-color: #000;
+ opacity: 0.5;
+}
+
+.cropper-view-box {
+ display: block;
+ height: 100%;
+ outline: 1px solid #39f;
+ outline-color: rgba(51, 153, 255, 75%);
+ overflow: hidden;
+ width: 100%;
+}
+
+.cropper-dashed {
+ border: 0 dashed #eee;
+ display: block;
+ opacity: 0.5;
+ position: absolute;
+}
+
+.cropper-dashed.dashed-h {
+ border-bottom-width: 1px;
+ border-top-width: 1px;
+ height: calc(100% / 3);
+ left: 0;
+ top: calc(100% / 3);
+ width: 100%;
+ }
+
+.cropper-dashed.dashed-v {
+ border-left-width: 1px;
+ border-right-width: 1px;
+ height: 100%;
+ left: calc(100% / 3);
+ top: 0;
+ width: calc(100% / 3);
+ }
+
+.cropper-center {
+ display: block;
+ height: 0;
+ left: 50%;
+ opacity: 0.75;
+ position: absolute;
+ top: 50%;
+ width: 0;
+}
+
+.cropper-center::before,
+ .cropper-center::after {
+ background-color: #eee;
+ content: " ";
+ display: block;
+ position: absolute;
+ }
+
+.cropper-center::before {
+ height: 1px;
+ left: -3px;
+ top: 0;
+ width: 7px;
+ }
+
+.cropper-center::after {
+ height: 7px;
+ left: 0;
+ top: -3px;
+ width: 1px;
+ }
+
+.cropper-face,
+.cropper-line,
+.cropper-point {
+ display: block;
+ height: 100%;
+ opacity: 0.1;
+ position: absolute;
+ width: 100%;
+}
+
+.cropper-face {
+ background-color: #fff;
+ left: 0;
+ top: 0;
+}
+
+.cropper-line {
+ background-color: #39f;
+}
+
+.cropper-line.line-e {
+ cursor: ew-resize;
+ right: -3px;
+ top: 0;
+ width: 5px;
+ }
+
+.cropper-line.line-n {
+ cursor: ns-resize;
+ height: 5px;
+ left: 0;
+ top: -3px;
+ }
+
+.cropper-line.line-w {
+ cursor: ew-resize;
+ left: -3px;
+ top: 0;
+ width: 5px;
+ }
+
+.cropper-line.line-s {
+ bottom: -3px;
+ cursor: ns-resize;
+ height: 5px;
+ left: 0;
+ }
+
+.cropper-point {
+ background-color: #39f;
+ height: 5px;
+ opacity: 0.75;
+ width: 5px;
+}
+
+.cropper-point.point-e {
+ cursor: ew-resize;
+ margin-top: -3px;
+ right: -3px;
+ top: 50%;
+ }
+
+.cropper-point.point-n {
+ cursor: ns-resize;
+ left: 50%;
+ margin-left: -3px;
+ top: -3px;
+ }
+
+.cropper-point.point-w {
+ cursor: ew-resize;
+ left: -3px;
+ margin-top: -3px;
+ top: 50%;
+ }
+
+.cropper-point.point-s {
+ bottom: -3px;
+ cursor: s-resize;
+ left: 50%;
+ margin-left: -3px;
+ }
+
+.cropper-point.point-ne {
+ cursor: nesw-resize;
+ right: -3px;
+ top: -3px;
+ }
+
+.cropper-point.point-nw {
+ cursor: nwse-resize;
+ left: -3px;
+ top: -3px;
+ }
+
+.cropper-point.point-sw {
+ bottom: -3px;
+ cursor: nesw-resize;
+ left: -3px;
+ }
+
+.cropper-point.point-se {
+ bottom: -3px;
+ cursor: nwse-resize;
+ height: 20px;
+ opacity: 1;
+ right: -3px;
+ width: 20px;
+ }
+
+@media (min-width: 768px) {
+
+.cropper-point.point-se {
+ height: 15px;
+ width: 15px;
+ }
+ }
+
+@media (min-width: 992px) {
+
+.cropper-point.point-se {
+ height: 10px;
+ width: 10px;
+ }
+ }
+
+@media (min-width: 1200px) {
+
+.cropper-point.point-se {
+ height: 5px;
+ opacity: 0.75;
+ width: 5px;
+ }
+ }
+
+.cropper-point.point-se::before {
+ background-color: #39f;
+ bottom: -50%;
+ content: " ";
+ display: block;
+ height: 200%;
+ opacity: 0;
+ position: absolute;
+ right: -50%;
+ width: 200%;
+ }
+
+.cropper-invisible {
+ opacity: 0;
+}
+
+.cropper-bg {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC");
+}
+
+.cropper-hide {
+ display: block;
+ height: 0;
+ position: absolute;
+ width: 0;
+}
+
+.cropper-hidden {
+ display: none !important;
+}
+
+.cropper-move {
+ cursor: move;
+}
+
+.cropper-crop {
+ cursor: crosshair;
+}
+
+.cropper-disabled .cropper-drag-box,
+.cropper-disabled .cropper-face,
+.cropper-disabled .cropper-line,
+.cropper-disabled .cropper-point {
+ cursor: not-allowed;
+}
diff --git a/src/pages/User/Center/components/AvatarCropper/images/bg.png b/src/pages/User/Center/components/AvatarCropper/images/bg.png
new file mode 100644
index 0000000..3c7056b
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/images/bg.png
Binary files differ
diff --git a/src/pages/User/Center/components/AvatarCropper/index.less b/src/pages/User/Center/components/AvatarCropper/index.less
new file mode 100644
index 0000000..dc3fecf
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/index.less
@@ -0,0 +1,10 @@
+.avatarPreview {
+ position: absolute;
+ top: 50%;
+ transform: translate(50%, -50%);
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #ccc;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/src/pages/User/Center/components/AvatarCropper/index.tsx b/src/pages/User/Center/components/AvatarCropper/index.tsx
new file mode 100644
index 0000000..83d0bcf
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/index.tsx
@@ -0,0 +1,144 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Modal, Row, Col, Button, Space, Upload, message } from 'antd';
+import { useIntl } from '@umijs/max';
+import { uploadAvatar } from '@/services/system/user';
+import { Cropper } from 'react-cropper';
+import './cropper.css';
+import styles from './index.less';
+import {
+ MinusOutlined,
+ PlusOutlined,
+ RedoOutlined,
+ UndoOutlined,
+ UploadOutlined,
+} from '@ant-design/icons';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2022/02/24
+ *
+ * */
+
+export type AvatarCropperProps = {
+ onFinished: (isSuccess: boolean) => void;
+ open: boolean;
+ data: any;
+};
+
+const AvatarCropperForm: React.FC<AvatarCropperProps> = (props) => {
+ const cropperRef = useRef<HTMLImageElement>(null);
+ const [avatarData, setAvatarData] = useState<any>();
+ const [previewData, setPreviewData] = useState();
+
+ useEffect(() => {
+ setAvatarData(props.data);
+ }, [props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.getCroppedCanvas().toBlob((blob: Blob) => {
+ const formData = new FormData();
+ formData.append('avatarfile', blob);
+ uploadAvatar(formData).then((res) => {
+ if (res.code === 200) {
+ message.success(res.msg);
+ props.onFinished(true);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }, 'image/png');
+ };
+ const handleCancel = () => {
+ props.onFinished(false);
+ };
+ const onCrop = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ setPreviewData(cropper.getCroppedCanvas().toDataURL());
+ };
+ const onRotateRight = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.rotate(90);
+ };
+ const onRotateLeft = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.rotate(-90);
+ };
+ const onZoomIn = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.zoom(0.1);
+ };
+ const onZoomOut = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.zoom(-0.1);
+ };
+ const beforeUpload = (file: any) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ setAvatarData(reader.result);
+ };
+ };
+ return (
+ <Modal
+ width={800}
+ title={intl.formatMessage({
+ id: 'system.user.modify_avatar',
+ defaultMessage: '修改头像',
+ })}
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <Cropper
+ ref={cropperRef}
+ src={avatarData}
+ style={{ height: 350, width: '100%', marginBottom: '16px' }}
+ initialAspectRatio={1}
+ guides={false}
+ crop={onCrop}
+ zoomable={true}
+ zoomOnWheel={true}
+ rotatable={true}
+ />
+ </Col>
+ <Col span={12} order={2}>
+ <div className={styles.avatarPreview}>
+ <img src={previewData} style={{ height: '100%', width: '100%' }} />
+ </div>
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={6}>
+ <Upload beforeUpload={beforeUpload} maxCount={1}>
+ <Button>
+ <UploadOutlined />
+ 上传
+ </Button>
+ </Upload>
+ </Col>
+ <Col>
+ <Space>
+ <Button icon={<RedoOutlined />} onClick={onRotateRight} />
+ <Button icon={<UndoOutlined />} onClick={onRotateLeft} />
+ <Button icon={<PlusOutlined />} onClick={onZoomIn} />
+ <Button icon={<MinusOutlined />} onClick={onZoomOut} />
+ </Space>
+ </Col>
+ </Row>
+ </Modal>
+ );
+};
+
+export default AvatarCropperForm;
diff --git a/src/pages/User/Center/components/BaseInfo/index.tsx b/src/pages/User/Center/components/BaseInfo/index.tsx
new file mode 100644
index 0000000..5cdca5f
--- /dev/null
+++ b/src/pages/User/Center/components/BaseInfo/index.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { Form, message, Row } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { ProForm, ProFormRadio, ProFormText } from '@ant-design/pro-components';
+import { updateUserProfile } from '@/services/system/user';
+
+
+export type BaseInfoProps = {
+ values: Partial<API.CurrentUser> | undefined;
+};
+
+const BaseInfo: React.FC<BaseInfoProps> = (props) => {
+ const [form] = Form.useForm();
+ const intl = useIntl();
+
+ const handleFinish = async (values: Record<string, any>) => {
+ const data = { ...props.values, ...values } as API.CurrentUser;
+ const resp = await updateUserProfile(data);
+ if (resp.code === 200) {
+ message.success('修改成功');
+ } else {
+ message.warning(resp.msg);
+ }
+ };
+
+ return (
+ <>
+ <ProForm form={form} onFinish={handleFinish} initialValues={props.values}>
+ <Row>
+ <ProFormText
+ name="nickName"
+ label={intl.formatMessage({
+ id: 'system.user.nick_name',
+ defaultMessage: '用户昵称',
+ })}
+ width="xl"
+ placeholder="请输入用户昵称"
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage id="请输入用户昵称!" defaultMessage="请输入用户昵称!" />
+ ),
+ },
+ ]}
+ />
+ </Row>
+ <Row>
+ <ProFormText
+ name="phonenumber"
+ label={intl.formatMessage({
+ id: 'system.user.phonenumber',
+ defaultMessage: '手机号码',
+ })}
+ width="xl"
+ placeholder="请输入手机号码"
+ rules={[
+ {
+ required: false,
+ message: (
+ <FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />
+ ),
+ },
+ ]}
+ />
+ </Row>
+ <Row>
+ <ProFormText
+ name="email"
+ label={intl.formatMessage({
+ id: 'system.user.email',
+ defaultMessage: '邮箱',
+ })}
+ width="xl"
+ placeholder="请输入邮箱"
+ rules={[
+ {
+ type: 'email',
+ message: '无效的邮箱地址!',
+ },
+ {
+ required: false,
+ message: <FormattedMessage id="请输入邮箱!" defaultMessage="请输入邮箱!" />,
+ },
+ ]}
+ />
+ </Row>
+ <Row>
+ <ProFormRadio.Group
+ options={[
+ {
+ label: '男',
+ value: '0',
+ },
+ {
+ label: '女',
+ value: '1',
+ },
+ ]}
+ name="sex"
+ label={intl.formatMessage({
+ id: 'system.user.sex',
+ defaultMessage: 'sex',
+ })}
+ width="xl"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入性别!" defaultMessage="请输入性别!" />,
+ },
+ ]}
+ />
+ </Row>
+ </ProForm>
+ </>
+ );
+};
+
+export default BaseInfo;
diff --git a/src/pages/User/Center/components/ResetPassword/index.tsx b/src/pages/User/Center/components/ResetPassword/index.tsx
new file mode 100644
index 0000000..26825d4
--- /dev/null
+++ b/src/pages/User/Center/components/ResetPassword/index.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { Form, message } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { updateUserPwd } from '@/services/system/user';
+import { ProForm, ProFormText } from '@ant-design/pro-components';
+
+const ResetPassword: React.FC = () => {
+ const [form] = Form.useForm();
+ const intl = useIntl();
+
+ const handleFinish = async (values: Record<string, any>) => {
+ const resp = await updateUserPwd(values.oldPassword, values.newPassword);
+ if (resp.code === 200) {
+ message.success('密码重置成功。');
+ } else {
+ message.warning(resp.msg);
+ }
+ };
+
+ const checkPassword = (rule: any, value: string) => {
+ const login_password = form.getFieldValue('newPassword');
+ if (value === login_password) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('两次密码输入不一致'));
+ };
+
+ return (
+ <>
+ <ProForm form={form} onFinish={handleFinish}>
+ <ProFormText.Password
+ name="oldPassword"
+ label={intl.formatMessage({
+ id: 'system.user.old_password',
+ defaultMessage: '旧密码',
+ })}
+ width="xl"
+ placeholder="请输入旧密码"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入旧密码!" defaultMessage="请输入旧密码!" />,
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="newPassword"
+ label={intl.formatMessage({
+ id: 'system.user.new_password',
+ defaultMessage: '新密码',
+ })}
+ width="xl"
+ placeholder="请输入新密码"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入新密码!" defaultMessage="请输入新密码!" />,
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="confirmPassword"
+ label={intl.formatMessage({
+ id: 'system.user.confirm_password',
+ defaultMessage: '确认密码',
+ })}
+ width="xl"
+ placeholder="请输入确认密码"
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage id="请输入确认密码!" defaultMessage="请输入确认密码!" />
+ ),
+ },
+ { validator: checkPassword },
+ ]}
+ />
+ </ProForm>
+ </>
+ );
+};
+
+export default ResetPassword;
diff --git a/src/pages/User/Center/index.tsx b/src/pages/User/Center/index.tsx
new file mode 100644
index 0000000..2ce308c
--- /dev/null
+++ b/src/pages/User/Center/index.tsx
@@ -0,0 +1,200 @@
+import {
+ ClusterOutlined,
+ MailOutlined,
+ TeamOutlined,
+ UserOutlined,
+ MobileOutlined,
+ ManOutlined,
+} from '@ant-design/icons';
+import { Card, Col, Divider, List, Row } from 'antd';
+import React, { useState } from 'react';
+import styles from './Center.less';
+import BaseInfo from './components/BaseInfo';
+import ResetPassword from './components/ResetPassword';
+import AvatarCropper from './components/AvatarCropper';
+import { useRequest } from '@umijs/max';
+import { getUserInfo } from '@/services/session';
+import { PageLoading } from '@ant-design/pro-components';
+
+const operationTabList = [
+ {
+ key: 'base',
+ tab: (
+ <span>
+ 基本资料
+ </span>
+ ),
+ },
+ {
+ key: 'password',
+ tab: (
+ <span>
+ 重置密码
+ </span>
+ ),
+ },
+];
+
+export type tabKeyType = 'base' | 'password';
+
+const Center: React.FC = () => {
+
+ const [tabKey, setTabKey] = useState<tabKeyType>('base');
+
+ const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
+
+ // 获取用户信息
+ const { data: userInfo, loading } = useRequest(async () => {
+ return { data: await getUserInfo()};
+ });
+ if (loading) {
+ return <div>loading...</div>;
+ }
+
+ const currentUser = userInfo?.user;
+
+ // 渲染用户信息
+ const renderUserInfo = ({
+ userName,
+ phonenumber,
+ email,
+ sex,
+ dept,
+ }: Partial<API.CurrentUser>) => {
+ return (
+ <List>
+ <List.Item>
+ <div>
+ <UserOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 用户名
+ </div>
+ <div>{userName}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <ManOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 性别
+ </div>
+ <div>{sex === '1' ? '女' : '男'}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MobileOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 电话
+ </div>
+ <div>{phonenumber}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 邮箱
+ </div>
+ <div>{email}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <ClusterOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 部门
+ </div>
+ <div>{dept?.deptName}</div>
+ </List.Item>
+ </List>
+ );
+ };
+
+ // 渲染tab切换
+ const renderChildrenByTabKey = (tabValue: tabKeyType) => {
+ if (tabValue === 'base') {
+ return <BaseInfo values={currentUser} />;
+ }
+ if (tabValue === 'password') {
+ return <ResetPassword />;
+ }
+ return null;
+ };
+
+ if (!currentUser) {
+ return <PageLoading />;
+ }
+
+ return (
+ <div>
+ <Row gutter={[16, 24]}>
+ <Col lg={8} md={24}>
+ <Card
+ title="个人信息"
+ bordered={false}
+ loading={loading}
+ >
+ {!loading && (
+ <div style={{ textAlign: "center"}}>
+ <div className={styles.avatarHolder} onClick={()=>{setCropperModalOpen(true)}}>
+ <img alt="" src={currentUser.avatar} />
+ </div>
+ {renderUserInfo(currentUser)}
+ <Divider dashed />
+ <div className={styles.team}>
+ <div className={styles.teamTitle}>角色</div>
+ <Row gutter={36}>
+ {currentUser.roles &&
+ currentUser.roles.map((item: any) => (
+ <Col key={item.roleId} lg={24} xl={12}>
+ <TeamOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ {item.roleName}
+ </Col>
+ ))}
+ </Row>
+ </div>
+ </div>
+ )}
+ </Card>
+ </Col>
+ <Col lg={16} md={24}>
+ <Card
+ bordered={false}
+ tabList={operationTabList}
+ activeTabKey={tabKey}
+ onTabChange={(_tabKey: string) => {
+ setTabKey(_tabKey as tabKeyType);
+ }}
+ >
+ {renderChildrenByTabKey(tabKey)}
+ </Card>
+ </Col>
+ </Row>
+ <AvatarCropper
+ onFinished={() => {
+ setCropperModalOpen(false);
+ }}
+ open={cropperModalOpen}
+ data={currentUser.avatar}
+ />
+ </div>
+ );
+};
+
+export default Center;
diff --git a/src/pages/User/Login/index.tsx b/src/pages/User/Login/index.tsx
new file mode 100644
index 0000000..a18f785
--- /dev/null
+++ b/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;
diff --git a/src/pages/User/Settings/index.tsx b/src/pages/User/Settings/index.tsx
new file mode 100644
index 0000000..f29d0b9
--- /dev/null
+++ b/src/pages/User/Settings/index.tsx
@@ -0,0 +1,20 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { Card } from 'antd';
+import React from 'react';
+
+/**
+ *
+ * @author whiteshader@163.com
+ *
+ * */
+
+
+const Settings: React.FC = () => {
+ return (
+ <PageContainer>
+ <Card title="Developing" />
+ </PageContainer>
+ );
+};
+
+export default Settings;