Commit 09c7235c authored by 陈帅's avatar 陈帅 Committed by ddcat1115

features add userinfo page (#880)

* features add userinfo page

* Delete unnecessary objects

* Modify the title style

* Meun using antd

* userinfo/base/:page -> userinfo/:page

* itemview -> list

* userinfo add upload

* meun -> menu

* use getRoutes & rename files
parent 28d3327d
......@@ -20,3 +20,4 @@ yarn.lock
package-lock.json
*bak
jsconfig.json
.prettierrc
......@@ -7,6 +7,7 @@ import { getProfileBasicData } from './mock/profile';
import { getProfileAdvancedData } from './mock/profile';
import { getNotices } from './mock/notices';
import { format, delay } from 'roadhog-api-doc';
import { getProvince, getCity, getArea } from './mock/geographic/geographic';
// 是否禁用代理
const noProxy = process.env.NO_PROXY === 'true';
......@@ -15,7 +16,7 @@ const noProxy = process.env.NO_PROXY === 'true';
const proxy = {
// 支持值为 Object 和 Array
'GET /api/currentUser': {
$desc: "获取当前用户接口",
$desc: '获取当前用户接口',
$params: {
pageSize: {
desc: '分页',
......@@ -24,28 +25,48 @@ const proxy = {
},
$body: {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
profile: '简单的介绍下自己',
notifyCount: 12,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
},
// GET POST 可省略
'GET /api/users': [{
'GET /api/users': [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
}, {
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
}, {
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
}],
},
],
'GET /api/project/notice': getNotice,
'GET /api/activities': getActivities,
'GET /api/rule': getRule,
......@@ -62,7 +83,7 @@ const proxy = {
res.send({ message: 'Ok' });
},
'GET /api/tags': mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }]
'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }],
}),
'GET /api/fake_list': getFakeList,
'POST /api/fake_list': postFakeList,
......@@ -71,26 +92,26 @@ const proxy = {
'GET /api/profile/advanced': getProfileAdvancedData,
'POST /api/login/account': (req, res) => {
const { password, userName, type } = req.body;
if(password === '888888' && userName === 'admin'){
if (password === '888888' && userName === 'admin') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin'
currentAuthority: 'admin',
});
return ;
return;
}
if(password === '123456' && userName === 'user'){
if (password === '123456' && userName === 'user') {
res.send({
status: 'ok',
type,
currentAuthority: 'user'
currentAuthority: 'user',
});
return ;
return;
}
res.send({
status: 'error',
type,
currentAuthority: 'guest'
currentAuthority: 'guest',
});
},
'POST /api/register': (req, res) => {
......@@ -99,40 +120,42 @@ const proxy = {
'GET /api/notices': getNotices,
'GET /api/500': (req, res) => {
res.status(500).send({
"timestamp": 1513932555104,
"status": 500,
"error": "error",
"message": "error",
"path": "/base/category/list"
timestamp: 1513932555104,
status: 500,
error: 'error',
message: 'error',
path: '/base/category/list',
});
},
'GET /api/404': (req, res) => {
res.status(404).send({
"timestamp": 1513932643431,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/base/category/list/2121212"
timestamp: 1513932643431,
status: 404,
error: 'Not Found',
message: 'No message available',
path: '/base/category/list/2121212',
});
},
'GET /api/403': (req, res) => {
res.status(403).send({
"timestamp": 1513932555104,
"status": 403,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/base/category/list"
timestamp: 1513932555104,
status: 403,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/401': (req, res) => {
res.status(401).send({
"timestamp": 1513932555104,
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/base/category/list"
timestamp: 1513932555104,
status: 401,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/geographic/province': getProvince,
'GET /api/geographic/city/:province': getCity,
};
export default noProxy ? {} : delay(proxy, 1000);
export default (noProxy ? {} : delay(proxy, 1000));
This diff is collapsed.
const fs = require('fs');
function getJson(infoType) {
const json = fs.readFileSync(`${__dirname}/${infoType}.json`, 'utf8');
return JSON.parse(json);
}
export function getProvince(req, res) {
res.json(getJson('province'));
}
export function getCity(req, res) {
res.json(getJson('city')[req.params.province]);
}
export default {
getProvince,
getCity,
};
[
{
"name": "北京市",
"id": "110000"
},
{
"name": "天津市",
"id": "120000"
},
{
"name": "河北省",
"id": "130000"
},
{
"name": "山西省",
"id": "140000"
},
{
"name": "内蒙古自治区",
"id": "150000"
},
{
"name": "辽宁省",
"id": "210000"
},
{
"name": "吉林省",
"id": "220000"
},
{
"name": "黑龙江省",
"id": "230000"
},
{
"name": "上海市",
"id": "310000"
},
{
"name": "江苏省",
"id": "320000"
},
{
"name": "浙江省",
"id": "330000"
},
{
"name": "安徽省",
"id": "340000"
},
{
"name": "福建省",
"id": "350000"
},
{
"name": "江西省",
"id": "360000"
},
{
"name": "山东省",
"id": "370000"
},
{
"name": "河南省",
"id": "410000"
},
{
"name": "湖北省",
"id": "420000"
},
{
"name": "湖南省",
"id": "430000"
},
{
"name": "广东省",
"id": "440000"
},
{
"name": "广西壮族自治区",
"id": "450000"
},
{
"name": "海南省",
"id": "460000"
},
{
"name": "重庆市",
"id": "500000"
},
{
"name": "四川省",
"id": "510000"
},
{
"name": "贵州省",
"id": "520000"
},
{
"name": "云南省",
"id": "530000"
},
{
"name": "西藏自治区",
"id": "540000"
},
{
"name": "陕西省",
"id": "610000"
},
{
"name": "甘肃省",
"id": "620000"
},
{
"name": "青海省",
"id": "630000"
},
{
"name": "宁夏回族自治区",
"id": "640000"
},
{
"name": "新疆维吾尔自治区",
"id": "650000"
},
{
"name": "台湾省",
"id": "710000"
},
{
"name": "香港特别行政区",
"id": "810000"
},
{
"name": "澳门特别行政区",
"id": "820000"
}
]
......@@ -160,6 +160,21 @@ export const getRouterData = (app) => {
'/user/register-result': {
component: dynamicWrapper(app, [], () => import('../routes/User/RegisterResult')),
},
'/userinfo': {
component: dynamicWrapper(app, ['geographic'], () => import('../routes/Userinfo/Info')),
},
'/userinfo/base': {
component: dynamicWrapper(app, ['geographic'], () => import('../routes/Userinfo/BaseView')),
},
'/userinfo/safe': {
component: dynamicWrapper(app, ['geographic'], () => import('../routes/Userinfo/SafeView')),
},
'/userinfo/account': {
component: dynamicWrapper(app, ['geographic'], () => import('../routes/Userinfo/AccountView')),
},
'/userinfo/message': {
component: dynamicWrapper(app, ['geographic'], () => import('../routes/Userinfo/MessageView')),
},
// '/user/:id': {
// component: dynamicWrapper(app, [], () => import('../routes/User/SomeComponent')),
// },
......
......@@ -58,7 +58,7 @@ export default class GlobalHeader extends PureComponent {
const menu = (
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
<Menu.Item disabled><Icon type="user" />个人中心</Menu.Item>
<Menu.Item disabled><Icon type="setting" />设置</Menu.Item>
<Menu.Item key="userinfo"><Icon type="setting" />设置</Menu.Item>
<Menu.Item key="triggerError"><Icon type="close-circle" />触发报错</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
......
......@@ -131,6 +131,10 @@ class BasicLayout extends React.PureComponent {
this.props.dispatch(routerRedux.push('/exception/trigger'));
return;
}
if (key === 'userinfo') {
this.props.dispatch(routerRedux.push('/userinfo/base'));
return;
}
if (key === 'logout') {
this.props.dispatch({
type: 'login/logout',
......
import { queryProvince, queryCity } from '../services/geographic';
export default {
namespace: 'geographic',
state: {
province: [],
city: [],
isLoading: false,
},
effects: {
*fetchProvince(_, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryProvince);
yield put({
type: 'setProvince',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
*fetchCity({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryCity, payload);
yield put({
type: 'setCity',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
},
reducers: {
setProvince(state, action) {
return {
...state,
province: action.payload,
};
},
setCity(state, action) {
return {
...state,
city: action.payload,
};
},
changeLoading(state, action) {
return {
...state,
isLoading: action.payload,
};
},
},
};
......@@ -2,7 +2,6 @@ import React, { PureComponent } from 'react';
import { Route, Redirect, Switch } from 'dva/router';
import { Card, Steps } from 'antd';
import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
import NotFound from '../../Exception/404';
import { getRoutes } from '../../../utils/utils';
import styles from '../style.less';
......@@ -43,7 +42,7 @@ export default class StepForm extends PureComponent {
))
}
<Redirect exact from="/form/step-form" to="/form/step-form/info" />
<Route render={NotFound} />
<Redirect to="/exception/404" />
</Switch>
</div>
</Card>
......
import React, { Component, Fragment } from 'react';
import { Icon, List } from 'antd';
export default class AccountView extends Component {
getData = () => {
return [
{
title: '绑定淘宝',
description: '当前未绑定淘宝账号',
actions: [<a>绑定</a>],
avatar: <Icon type="taobao" className="taobao" />,
},
{
title: '绑定支付宝',
description: '当前未绑定支付宝账号',
actions: [<a>绑定</a>],
avatar: <Icon type="alipay" className="alipay" />,
},
{
title: '绑定钉钉',
description: '当前未绑定钉钉账号',
actions: [<a>绑定</a>],
avatar: <Icon type="dingding" className="dingding" />,
},
];
};
render() {
return (
<Fragment>
<List
itemLayout="horizontal"
dataSource={this.getData()}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta
avatar={item.avatar}
title={item.title}
description={item.description}
/>
</List.Item>
)}
/>
</Fragment>
);
}
}
import React, { Component, Fragment } from 'react';
import { Form, Input, Upload, Select, Button } from 'antd';
import styles from './BaseView.less';
import GeographicView from './GeographicView';
import PhoneView from './PhoneView';
const FormItem = Form.Item;
const { Option } = Select;
// 头像组件 方便以后独立,增加裁剪之类的功能
const AvatarView = ({ avatar }) => (
<Fragment>
<div className={styles.avatar_title}>头像</div>
<div className={styles.avatar}>
<img src={avatar} alt="avatar" />
</div>
<Upload fileList={[]}>
<div className={styles.button_view}>
<Button icon="upload">更换头像</Button>
</div>
</Upload>
</Fragment>
);
const validatorGeographic = (rule, value, callback) => {
const { province, city } = value;
if (!province.key) {
callback('Please input your province!');
}
if (!city.key) {
callback('Please input your city!');
}
callback();
};
const validatorPhone = (rule, value, callback) => {
const values = value.split('-');
if (!values[0]) {
callback('Please input your area code!');
}
if (!values[1]) {
callback('Please input your phone number!');
}
callback();
};
@Form.create()
export default class BaseView extends Component {
componentDidMount() {
this.setBaseInfo();
}
setBaseInfo = () => {
const { currentUser } = this.props;
Object.keys(this.props.form.getFieldsValue()).forEach((key) => {
const obj = {};
obj[key] = currentUser[key] || null;
this.props.form.setFieldsValue(obj);
});
};
getAvatarURL() {
if (this.props.currentUser.avatar) {
return this.props.currentUser.avatar;
}
const url =
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
return url;
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<div className={styles.baseView}>
<div className={styles.left}>
<Form layout="vertical" onSubmit={this.handleSubmit} hideRequiredMark>
<FormItem label="邮箱">
{getFieldDecorator('email', {
rules: [
{ required: true, message: 'Please input your email!' },
],
})(<Input />)}
</FormItem>
<FormItem label="昵称">
{getFieldDecorator('name', {
rules: [
{ required: true, message: 'Please input your nick name!' },
],
})(<Input />)}
</FormItem>
<FormItem label="个人简介">
{getFieldDecorator('profile', {
rules: [
{ required: true, message: 'Please input personal profile!' },
],
})(<Input.TextArea rows={4} />)}
</FormItem>
<FormItem label="国家/地区">
{getFieldDecorator('country', {
rules: [
{ required: true, message: 'Please input your country!' },
],
})(
<Select style={{ width: 220 }}>
<Option value="China">中国</Option>
<Option value="USA">美国</Option>
<Option value="France">法国</Option>
<Option value="Russian">俄罗斯</Option>
<Option value="UK">英国</Option>
</Select>,
)}
</FormItem>
<FormItem label="所在省市">
{getFieldDecorator('geographic', {
rules: [
{
required: true,
message: 'Please input your geographic info!',
},
{
validator: validatorGeographic,
},
],
})(<GeographicView />)}
</FormItem>
<FormItem label="街道地址">
{getFieldDecorator('address', {
rules: [
{ required: true, message: 'Please input your address!' },
],
})(<Input />)}
</FormItem>
<FormItem label="联系电话">
{getFieldDecorator('phone', {
rules: [
{ required: true, message: 'Please input your phone!' },
{ validator: validatorPhone },
],
})(<PhoneView />)}
</FormItem>
<Button type="primary">更新信息</Button>
</Form>
</div>
<div className={styles.right}>
<AvatarView avatar={this.getAvatarURL()} />
</div>
</div>
);
}
}
@import '~antd/lib/style/themes/default.less';
.baseView {
display: flex;
.left {
width: 448px;
}
.right {
flex: 1;
padding-left: 104px;
.avatar_title {
height: 22px;
width: 28px;
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
line-height: 22px;
margin-bottom: 8px;
}
.avatar {
width: 144px;
height: 144px;
margin-bottom: 12px;
overflow: hidden;
img {
width: 100%;
}
}
.button_view {
width: 144px;
text-align: center;
}
}
}
import React, { PureComponent } from 'react';
import { Select, Spin } from 'antd';
import { connect } from 'dva';
const { Option } = Select;
const nullSlectItem = {
label: '',
key: '',
};
@connect(({ geographic }) => {
const { province, isLoading, city } = geographic;
return {
province,
city,
isLoading,
};
})
export default class ProvinceSelect extends PureComponent {
componentDidMount = () => {
this.props.dispatch({
type: 'geographic/fetchProvince',
});
};
getProvinceOption() {
return this.getOption(this.props.province);
}
getCityOption = () => {
return this.getOption(this.props.city);
};
getOption = (list) => {
if (!list || list.length < 1) {
return (
<Option key={0} value={0}>
没有找到选项
</Option>
);
}
return list.map((item) => {
return (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
);
});
};
selectProvinceItem = (item) => {
this.props.dispatch({
type: 'geographic/fetchCity',
payload: item.key,
});
this.props.onChange({
province: item,
city: nullSlectItem,
});
};
selectCityItem = (item) => {
this.props.onChange({
province: this.props.value.province,
city: item,
});
};
conversionObject() {
const { value } = this.props;
if (!value) {
return {
province: nullSlectItem,
city: nullSlectItem,
};
}
const { province, city } = value;
return {
province: province || nullSlectItem,
city: city || nullSlectItem,
};
}
render() {
const { province, city } = this.conversionObject();
return (
<Spin spinning={this.props.isLoading}>
<Select
value={province}
labelInValue
showSearch
onSelect={this.selectProvinceItem}
style={{ width: 220, marginRight: 8 }}
>
{this.getProvinceOption()}
</Select>
<Select
value={city}
labelInValue
showSearch
onSelect={this.selectCityItem}
style={{ width: 220 }}
>
{this.getCityOption()}
</Select>
</Spin>
);
}
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Route, routerRedux, Switch, Redirect } from 'dva/router';
import { Menu } from 'antd';
import styles from './Info.less';
import { getRoutes } from '../../utils/utils';
const { Item } = Menu;
const menuMap = {
base: '基本设置',
safe: '安全设置',
account: '账号绑定',
message: '新消息通知',
};
@connect(({ user }) => ({
currentUser: user.currentUser,
}))
export default class Info extends PureComponent {
constructor(props) {
super(props);
const { match, location } = props;
let key = location.pathname.replace(`${match.path}/`, '');
key = menuMap[key] ? key : 'base';
this.state = {
selectKey: key,
};
}
getmenu = () => {
return Object.keys(menuMap).map(item => <Item key={item}>{menuMap[item]}</Item>);
};
getRightTitle = () => {
return menuMap[this.state.selectKey];
};
selectKey = ({ key }) => {
this.props.dispatch(routerRedux.push(`/userinfo/${key}`));
this.setState({
selectKey: key,
});
};
render() {
const { match, routerData, currentUser } = this.props;
if (!currentUser.userid) {
return '';
}
return (
<div className={styles.main}>
<div className={styles.leftmenu}>
<Menu
mode="inline"
selectedKeys={[this.state.selectKey]}
onClick={this.selectKey}
>
{this.getmenu()}
</Menu>
</div>
<div className={styles.right}>
<div className={styles.title}>{this.getRightTitle()}</div>
<Switch>
{
getRoutes(match.path, routerData).map(item => (
<Route
key={item.key}
path={item.path}
render={props => <item.component {...props} currentUser={currentUser} />}
exact={item.exact}
/>
))
}
<Redirect exact from="/userinfo" to="/userinfo/base" />
<Redirect to="/exception/404" />
</Switch>
</div>
</div>
);
}
}
@import '~antd/lib/style/themes/default.less';
.main {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
padding-top: 15px;
padding-bottom: 15px;
.leftmenu {
width: 224px;
border-right: 1px solid #e8e8e8;
:global {
.ant-menu-inline {
border: none;
}
}
}
.right {
flex: 1;
padding-left: 40px;
padding-right: 40px;
padding-top: 9px;
padding-bottom: 9px;
.title {
font-size: 20px;
color: rgba(0, 0, 0, 0.85);
line-height: 28px;
width: 100px;
height: 28px;
font-weight: bold;
margin-bottom: 24px;
}
}
}
:global {
.ant-list-item-meta {
// 账号绑定图标
.taobao {
color: #ff4000;
display: block;
font-size: 48px;
line-height: 48px;
border-radius: 4px;
}
.dingding {
background-color: #2eabff;
color: #fff;
font-size: 32px;
line-height: 32px;
padding: 6px;
margin: 2px;
border-radius: 4px;
}
.alipay {
color: #2eabff;
font-size: 48px;
line-height: 48px;
border-radius: 4px;
}
}
// 密码强度
font.strong {
color: #52c41a;
}
font.medium {
color: yellow;
}
font.weak {
color: red;
}
}
import React, { Component, Fragment } from 'react';
import { Switch, List } from 'antd';
const Action = <Switch defaultChecked />;
export default class MessageView extends Component {
getData = () => {
return [
{
title: '账户密码',
description: '其他用户的消息将以站内信的形式通知',
actions: [Action],
},
{
title: '消息通知',
description: '已绑定手机:138****8293',
actions: [Action],
},
{
title: '系统消息',
description: '系统消息将以站内信的形式通知',
actions: [Action],
},
{
title: '待办通知',
description: '待办事项将以站内信的形式通知',
actions: [Action],
},
];
};
render() {
return (
<Fragment>
<List
itemLayout="horizontal"
dataSource={this.getData()}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta
title={item.title}
description={item.description}
/>
</List.Item>
)}
/>
</Fragment>
);
}
}
import React, { Fragment, PureComponent } from 'react';
import { Input } from 'antd';
class PhoneView extends PureComponent {
render() {
const { value, onChange } = this.props;
let values = ['', ''];
if (value) {
values = value.split('-');
}
return (
<Fragment>
<Input
value={values[0]}
onChange={(e) => {
onChange(`${e.target.value}-${values[1]}`);
}}
style={{ width: 128, marginRight: 8 }}
/>
<Input
onChange={(e) => {
onChange(`${values[0]}-${e.target.value}`);
}}
value={values[1]}
style={{ width: 312 }}
/>
</Fragment>
);
}
}
export default PhoneView;
import React, { Component, Fragment } from 'react';
import { List } from 'antd';
const passwordStrength = {
strong: <font className="strong"></font>,
medium: <font className="medium">中文</font>,
weak: <font className="weak"></font>,
};
export default class SafeView extends Component {
getData = () => {
return [
{
title: '账户密码',
description: (
<Fragment> 当前密码强度{passwordStrength.strong}</Fragment>
),
actions: [<a>修改</a>],
},
{
title: '密保手机',
description: '已绑定手机:138****8293',
actions: [<a>修改</a>],
},
{
title: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
actions: [<a>设置</a>],
},
{
title: '备用邮箱',
description: '已绑定邮箱:ant***sign.com',
actions: [<a>修改</a>],
},
{
title: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
actions: [<a>绑定</a>],
},
];
};
render() {
return (
<Fragment>
<List
itemLayout="horizontal"
dataSource={this.getData()}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta
title={item.title}
description={item.description}
/>
</List.Item>
)}
/>
</Fragment>
);
}
}
import request from '../utils/request';
export async function queryProvince() {
return request('/api/geographic/province');
}
export async function queryCity(province) {
return request(`/api/geographic/city/${province}`);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment