flat:初始化
This commit is contained in:
61
src/pages/User/Center/Center.less
Normal file
61
src/pages/User/Center/Center.less
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
309
src/pages/User/Center/components/AvatarCropper/cropper.css
Normal file
309
src/pages/User/Center/components/AvatarCropper/cropper.css
Normal file
@@ -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;
|
||||
}
|
||||
BIN
src/pages/User/Center/components/AvatarCropper/images/bg.png
Normal file
BIN
src/pages/User/Center/components/AvatarCropper/images/bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 B |
10
src/pages/User/Center/components/AvatarCropper/index.less
Normal file
10
src/pages/User/Center/components/AvatarCropper/index.less
Normal file
@@ -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;
|
||||
}
|
||||
144
src/pages/User/Center/components/AvatarCropper/index.tsx
Normal file
144
src/pages/User/Center/components/AvatarCropper/index.tsx
Normal file
@@ -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;
|
||||
119
src/pages/User/Center/components/BaseInfo/index.tsx
Normal file
119
src/pages/User/Center/components/BaseInfo/index.tsx
Normal file
@@ -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;
|
||||
84
src/pages/User/Center/components/ResetPassword/index.tsx
Normal file
84
src/pages/User/Center/components/ResetPassword/index.tsx
Normal file
@@ -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;
|
||||
200
src/pages/User/Center/index.tsx
Normal file
200
src/pages/User/Center/index.tsx
Normal file
@@ -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;
|
||||
436
src/pages/User/Login/index.tsx
Normal file
436
src/pages/User/Login/index.tsx
Normal file
@@ -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;
|
||||
20
src/pages/User/Settings/index.tsx
Normal file
20
src/pages/User/Settings/index.tsx
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user