소스 검색

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
陈帅 8 년 전
부모
커밋
09c7235c6a

+ 1 - 0
.gitignore

@@ -20,3 +20,4 @@ yarn.lock
 package-lock.json
 *bak
 jsconfig.json
+.prettierrc

+ 70 - 47
.roadhogrc.mock.js

@@ -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': [{
-    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/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));

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1784 - 0
mock/geographic/city.json


+ 19 - 0
mock/geographic/geographic.js

@@ -0,0 +1,19 @@
+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,
+};

+ 138 - 0
mock/geographic/province.json

@@ -0,0 +1,138 @@
+[
+  {
+    "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"
+  }
+]

+ 15 - 0
src/common/router.js

@@ -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')),
     // },

+ 1 - 1
src/components/GlobalHeader/index.js

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

+ 4 - 0
src/layouts/BasicLayout.js

@@ -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',

+ 65 - 0
src/models/geographic.js

@@ -0,0 +1,65 @@
+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,
+      };
+    },
+  },
+};

+ 1 - 2
src/routes/Forms/StepForm/index.js

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

+ 46 - 0
src/routes/Userinfo/AccountView.js

@@ -0,0 +1,46 @@
+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>
+    );
+  }
+}

+ 147 - 0
src/routes/Userinfo/BaseView.js

@@ -0,0 +1,147 @@
+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>
+    );
+  }
+}

+ 33 - 0
src/routes/Userinfo/BaseView.less

@@ -0,0 +1,33 @@
+@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;
+    }
+  }
+}

+ 103 - 0
src/routes/Userinfo/GeographicView.js

@@ -0,0 +1,103 @@
+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>
+    );
+  }
+}

+ 78 - 0
src/routes/Userinfo/Info.js

@@ -0,0 +1,78 @@
+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>
+    );
+  }
+}

+ 73 - 0
src/routes/Userinfo/Info.less

@@ -0,0 +1,73 @@
+@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;
+  }
+}

+ 49 - 0
src/routes/Userinfo/MessageView.js

@@ -0,0 +1,49 @@
+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>
+    );
+  }
+}

+ 32 - 0
src/routes/Userinfo/PhoneView.js

@@ -0,0 +1,32 @@
+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;

+ 60 - 0
src/routes/Userinfo/SafeView.js

@@ -0,0 +1,60 @@
+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>
+    );
+  }
+}

+ 9 - 0
src/services/geographic.js

@@ -0,0 +1,9 @@
+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}`);
+}