Просмотр исходного кода

feat: up antd to 4.0 (#5948)

* feat: use antd@4

* feat: refactoring old code

* feat: use new type

* add umi-plugin-antd-icon-config plugin

* feat: add table list page (#5742)

* feat: add table list page

* bugfix: remove unuse interface

* better demo

* fix lint warning

* update api

* update api

* support sort

* change to antd@4

* finish

* fix ts error

* dep: up @ant-design/pro-layout@5.0.0

* dep: add pro-table@2

* bugfix: fix env tag style error

* bugfix: fix typescript error

* bugfix: fix login style

close #5960

close #5958

* feat: CreateForm use pro-table
陈帅 5 лет назад
Родитель
Сommit
19606d7d6f
34 измененных файлов с 1364 добавлено и 1076 удалено
  1. 7 2
      config/config.ts
  2. 9 0
      mock/user.ts
  3. 14 10
      package.json
  4. 25 13
      src/components/GlobalHeader/AvatarDropdown.tsx
  5. 33 16
      src/components/GlobalHeader/NoticeIconView.tsx
  6. 27 25
      src/components/GlobalHeader/RightContent.tsx
  7. 7 5
      src/components/GlobalHeader/index.less
  8. 2 2
      src/components/HeaderDropdown/index.tsx
  9. 2 4
      src/components/HeaderSearch/index.less
  10. 86 128
      src/components/HeaderSearch/index.tsx
  11. 66 104
      src/components/NoticeIcon/index.tsx
  12. 1 1
      src/components/SelectLang/index.less
  13. 7 4
      src/components/SelectLang/index.tsx
  14. 21 20
      src/layouts/BasicLayout.tsx
  15. 6 7
      src/layouts/UserLayout.tsx
  16. 1 6
      src/models/login.ts
  17. 0 3
      src/pages/404.tsx
  18. 3 3
      src/pages/Admin.tsx
  19. 154 0
      src/pages/ListTableList/_mock.ts
  20. 25 0
      src/pages/ListTableList/components/CreateForm.tsx
  21. 215 0
      src/pages/ListTableList/components/UpdateForm.tsx
  22. 35 0
      src/pages/ListTableList/data.d.ts
  23. 238 0
      src/pages/ListTableList/index.tsx
  24. 38 0
      src/pages/ListTableList/service.ts
  25. 8 8
      src/pages/Welcome.tsx
  26. 98 125
      src/pages/user/login/components/Login/LoginItem.tsx
  27. 11 20
      src/pages/user/login/components/Login/LoginTab.tsx
  28. 0 4
      src/pages/user/login/components/Login/index.less
  29. 100 156
      src/pages/user/login/components/Login/index.tsx
  30. 12 5
      src/pages/user/login/components/Login/map.tsx
  31. 113 179
      src/pages/user/login/index.tsx
  32. 0 78
      src/pages/user/login/locales/en-US.ts
  33. 0 74
      src/pages/user/login/locales/zh-CN.ts
  34. 0 74
      src/pages/user/login/locales/zh-TW.ts

+ 7 - 2
config/config.ts

@@ -1,5 +1,6 @@
 import { IConfig, IPlugin } from 'umi-types';
 import defaultSettings from './defaultSettings'; // https://umijs.org/config/
+
 import slash from 'slash2';
 import themePluginConfig from './themePluginConfig';
 import proxy from './proxy';
@@ -11,7 +12,6 @@ const { pwa } = defaultSettings;
 // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
 const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, REACT_APP_ENV } = process.env;
 const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site';
-
 const plugins: IPlugin[] = [
   ['umi-plugin-antd-icon-config', {}],
   [
@@ -127,6 +127,12 @@ export default {
               ],
             },
             {
+              name: 'list.table-list',
+              icon: 'table',
+              path: '/list',
+              component: './ListTableList',
+            },
+            {
               component: './404',
             },
           ],
@@ -136,7 +142,6 @@ export default {
         },
       ],
     },
-
     {
       component: './404',
     },

+ 9 - 0
mock/user.ts

@@ -95,6 +95,15 @@ export default {
       });
       return;
     }
+    if (type === 'mobile') {
+      res.send({
+        status: 'ok',
+        type,
+        currentAuthority: 'admin',
+      });
+      return;
+    }
+
     res.send({
       status: 'error',
       type,

+ 14 - 10
package.json

@@ -14,7 +14,7 @@
     "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up",
     "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro",
     "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro",
-    "fetch:blocks": "pro fetch-blocks && npm run prettier",
+    "fetch:blocks": "pro fetch-blocks --branch antd@4 && npm run prettier",
     "functions:build": "netlify-lambda build ./lambda",
     "functions:run": "cross-env NODE_ENV=dev netlify-lambda serve ./lambda",
     "gh-pages": "cp CNAME ./dist/ && gh-pages -d dist",
@@ -33,7 +33,7 @@
     "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none umi dev",
     "start:no-mock": "cross-env MOCK=none umi dev",
     "start:no-ui": "cross-env UMI_UI=none umi dev",
-    "start:pre": "cross-env REACT_APP_ENV=pre MOCK=none umi dev",
+    "start:pre": "cross-env REACT_APP_ENV=pre umi dev",
     "start:test": "cross-env REACT_APP_ENV=test MOCK=none umi dev",
     "test": "umi test",
     "test:all": "node ./tests/run-tests.js",
@@ -59,9 +59,11 @@
     "not ie <= 10"
   ],
   "dependencies": {
-    "@ant-design/pro-layout": "^4.10.13",
-    "@antv/data-set": "^0.11.0",
-    "antd": "^3.23.6",
+    "@ant-design/icons": "^4.0.0-alpha.19",
+    "@ant-design/pro-layout": "^5.0.0",
+    "@ant-design/pro-table": "^2.0.0",
+    "@antv/data-set": "^0.10.2",
+    "antd": "^4.0.0-rc.1",
     "classnames": "^2.2.6",
     "dva": "^2.6.0-beta.16",
     "lodash": "^4.17.11",
@@ -75,10 +77,12 @@
     "react-helmet": "^5.2.1",
     "redux": "^4.0.1",
     "umi": "^2.13.0",
+    "umi-plugin-antd-icon-config": "^1.0.2",
     "umi-plugin-antd-theme": "^1.0.1",
     "umi-plugin-pro-block": "^1.3.2",
-    "umi-plugin-react": "^1.9.5",
-    "umi-request": "^1.0.8"
+    "umi-plugin-react": "^1.14.10",
+    "umi-request": "^1.0.8",
+    "use-merge-value": "^1.0.1"
   },
   "devDependencies": {
     "@ant-design/pro-cli": "^1.0.18",
@@ -88,14 +92,14 @@
     "@types/jest": "^25.1.0",
     "@types/lodash": "^4.14.144",
     "@types/qs": "^6.5.3",
-    "@types/react": "^16.8.19",
+    "@types/react": "^16.9.17",
     "@types/react-dom": "^16.8.4",
     "@types/react-helmet": "^5.0.13",
     "@umijs/fabric": "^2.0.2",
     "chalk": "^3.0.0",
     "cross-env": "^7.0.0",
     "cross-port-killer": "^1.1.1",
-    "enzyme": "^3.9.0",
+    "enzyme": "^3.11.0",
     "express": "^4.17.1",
     "gh-pages": "^2.0.1",
     "husky": "^4.0.7",
@@ -112,7 +116,7 @@
     "umi-plugin-antd-icon-config": "^1.0.2",
     "umi-plugin-ga": "^1.1.3",
     "umi-plugin-pro": "^1.0.2",
-    "umi-types": "^0.5.0"
+    "umi-types": "^0.5.9"
   },
   "optionalDependencies": {
     "puppeteer": "^2.0.0"

+ 25 - 13
src/components/GlobalHeader/AvatarDropdown.tsx

@@ -1,10 +1,9 @@
-import { Avatar, Icon, Menu, Spin } from 'antd';
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { Avatar, Menu, Spin } from 'antd';
 import { ClickParam } from 'antd/es/menu';
-import { FormattedMessage } from 'umi-plugin-react/locale';
 import React from 'react';
 import { connect } from 'dva';
 import { router } from 'umi';
-
 import { ConnectProps, ConnectState } from '@/models/connect';
 import { CurrentUser } from '@/models/user';
 import HeaderDropdown from '../HeaderDropdown';
@@ -21,6 +20,7 @@ class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
 
     if (key === 'logout') {
       const { dispatch } = this.props;
+
       if (dispatch) {
         dispatch({
           type: 'login/logout',
@@ -29,35 +29,40 @@ class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
 
       return;
     }
+
     router.push(`/account/${key}`);
   };
 
   render(): React.ReactNode {
-    const { currentUser = { avatar: '', name: '' }, menu } = this.props;
-
+    const {
+      currentUser = {
+        avatar: '',
+        name: '',
+      },
+      menu,
+    } = this.props;
     const menuHeaderDropdown = (
       <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
         {menu && (
           <Menu.Item key="center">
-            <Icon type="user" />
-            <FormattedMessage id="menu.account.center" defaultMessage="account center" />
+            <UserOutlined />
+            个人中心
           </Menu.Item>
         )}
         {menu && (
           <Menu.Item key="settings">
-            <Icon type="setting" />
-            <FormattedMessage id="menu.account.settings" defaultMessage="account settings" />
+            <SettingOutlined />
+            个人设置
           </Menu.Item>
         )}
         {menu && <Menu.Divider />}
 
         <Menu.Item key="logout">
-          <Icon type="logout" />
-          <FormattedMessage id="menu.account.logout" defaultMessage="logout" />
+          <LogoutOutlined />
+          退出登录
         </Menu.Item>
       </Menu>
     );
-
     return currentUser && currentUser.name ? (
       <HeaderDropdown overlay={menuHeaderDropdown}>
         <span className={`${styles.action} ${styles.account}`}>
@@ -66,10 +71,17 @@ class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
         </span>
       </HeaderDropdown>
     ) : (
-      <Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />
+      <Spin
+        size="small"
+        style={{
+          marginLeft: 8,
+          marginRight: 8,
+        }}
+      />
     );
   }
 }
+
 export default connect(({ user }: ConnectState) => ({
   currentUser: user.currentUser,
 }))(AvatarDropdown);

+ 33 - 16
src/components/GlobalHeader/NoticeIconView.tsx

@@ -1,14 +1,12 @@
 import React, { Component } from 'react';
 import { Tag, message } from 'antd';
 import { connect } from 'dva';
-import { formatMessage } from 'umi-plugin-react/locale';
 import groupBy from 'lodash/groupBy';
 import moment from 'moment';
-
 import { NoticeItem } from '@/models/global';
-import NoticeIcon from '../NoticeIcon';
 import { CurrentUser } from '@/models/user';
 import { ConnectProps, ConnectState } from '@/models/connect';
+import NoticeIcon from '../NoticeIcon';
 import styles from './index.less';
 
 export interface GlobalHeaderRightProps extends ConnectProps {
@@ -22,6 +20,7 @@ export interface GlobalHeaderRightProps extends ConnectProps {
 class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
   componentDidMount() {
     const { dispatch } = this.props;
+
     if (dispatch) {
       dispatch({
         type: 'global/fetchNotices',
@@ -32,6 +31,7 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
   changeReadState = (clickedItem: NoticeItem): void => {
     const { id } = clickedItem;
     const { dispatch } = this.props;
+
     if (dispatch) {
       dispatch({
         type: 'global/changeNoticeReadState',
@@ -42,7 +42,8 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
 
   handleNoticeClear = (title: string, key: string) => {
     const { dispatch } = this.props;
-    message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`);
+    message.success(`${'清空了'} ${title}`);
+
     if (dispatch) {
       dispatch({
         type: 'global/clearNotices',
@@ -51,19 +52,26 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
     }
   };
 
-  getNoticeData = (): { [key: string]: NoticeItem[] } => {
+  getNoticeData = (): {
+    [key: string]: NoticeItem[];
+  } => {
     const { notices = [] } = this.props;
+
     if (notices.length === 0) {
       return {};
     }
+
     const newNotices = notices.map(notice => {
       const newNotice = { ...notice };
+
       if (newNotice.datetime) {
         newNotice.datetime = moment(notice.datetime as string).fromNow();
       }
+
       if (newNotice.id) {
         newNotice.key = newNotice.id;
       }
+
       if (newNotice.extra && newNotice.status) {
         const color = {
           todo: '',
@@ -72,23 +80,33 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
           doing: 'gold',
         }[newNotice.status];
         newNotice.extra = (
-          <Tag color={color} style={{ marginRight: 0 }}>
+          <Tag
+            color={color}
+            style={{
+              marginRight: 0,
+            }}
+          >
             {newNotice.extra}
           </Tag>
         );
       }
+
       return newNotice;
     });
     return groupBy(newNotices, 'type');
   };
 
   getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
-    const unreadMsg: { [key: string]: number } = {};
+    const unreadMsg: {
+      [key: string]: number;
+    } = {};
     Object.keys(noticeData).forEach(key => {
       const value = noticeData[key];
+
       if (!unreadMsg[key]) {
         unreadMsg[key] = 0;
       }
+
       if (Array.isArray(value)) {
         unreadMsg[key] = value.filter(item => !item.read).length;
       }
@@ -100,7 +118,6 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
     const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
     const noticeData = this.getNoticeData();
     const unreadMsg = this.getUnreadData(noticeData);
-
     return (
       <NoticeIcon
         className={styles.action}
@@ -109,8 +126,8 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
           this.changeReadState(item as NoticeItem);
         }}
         loading={fetchingNotices}
-        clearText={formatMessage({ id: 'component.noticeIcon.clear' })}
-        viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })}
+        clearText="清空"
+        viewMoreText="查看更多"
         onClear={this.handleNoticeClear}
         onPopupVisibleChange={onNoticeVisibleChange}
         onViewMore={() => message.info('Click on view more')}
@@ -120,22 +137,22 @@ class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
           tabKey="notification"
           count={unreadMsg.notification}
           list={noticeData.notification}
-          title={formatMessage({ id: 'component.globalHeader.notification' })}
-          emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })}
+          title="通知"
+          emptyText="你已查看所有通知"
           showViewMore
         />
         <NoticeIcon.Tab
           tabKey="message"
           count={unreadMsg.message}
           list={noticeData.message}
-          title={formatMessage({ id: 'component.globalHeader.message' })}
-          emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })}
+          title="消息"
+          emptyText="您已读完所有消息"
           showViewMore
         />
         <NoticeIcon.Tab
           tabKey="event"
-          title={formatMessage({ id: 'component.globalHeader.event' })}
-          emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })}
+          title="待办"
+          emptyText="你已完成所有待办"
           count={unreadMsg.event}
           list={noticeData.event}
           showViewMore

+ 27 - 25
src/components/GlobalHeader/RightContent.tsx

@@ -1,9 +1,8 @@
-import { Icon, Tooltip, Tag } from 'antd';
+import { Tooltip, Tag } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
 import React from 'react';
 import { connect } from 'dva';
-import { formatMessage } from 'umi-plugin-react/locale';
 import { ConnectProps, ConnectState } from '@/models/connect';
-
 import Avatar from './AvatarDropdown';
 import HeaderSearch from '../HeaderSearch';
 import SelectLang from '../SelectLang';
@@ -33,40 +32,43 @@ const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = props => {
     <div className={className}>
       <HeaderSearch
         className={`${styles.action} ${styles.search}`}
-        placeholder={formatMessage({
-          id: 'component.globalHeader.search',
-        })}
+        placeholder="站内搜索"
         defaultValue="umi ui"
-        dataSource={[
-          formatMessage({
-            id: 'component.globalHeader.search.example1',
-          }),
-          formatMessage({
-            id: 'component.globalHeader.search.example2',
-          }),
-          formatMessage({
-            id: 'component.globalHeader.search.example3',
-          }),
+        options={[
+          { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
+          {
+            label: <a href="next.ant.design">Ant Design</a>,
+            value: 'Ant Design',
+          },
+          {
+            label: <a href="https://protable.ant.design/">Pro Table</a>,
+            value: 'Pro Table',
+          },
+          {
+            label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
+            value: 'Pro Layout',
+          },
         ]}
-        onSearch={() => {}}
-        onPressEnter={() => {}}
+        // onSearch={value => {
+        //   //console.log('input', value);
+        // }}
       />
-      <Tooltip
-        title={formatMessage({
-          id: 'component.globalHeader.help',
-        })}
-      >
+      <Tooltip title="使用文档">
         <a
           target="_blank"
           href="https://pro.ant.design/docs/getting-started"
           rel="noopener noreferrer"
           className={styles.action}
         >
-          <Icon type="question-circle-o" />
+          <QuestionCircleOutlined />
         </a>
       </Tooltip>
       <Avatar />
-      {REACT_APP_ENV && <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>}
+      {REACT_APP_ENV && (
+        <span>
+          <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
+        </span>
+      )}
       <SelectLang className={styles.action} />
     </div>
   );

+ 7 - 5
src/components/GlobalHeader/index.less

@@ -12,17 +12,19 @@
 }
 
 .right {
+  display: flex;
   float: right;
   height: @layout-header-height;
   margin-left: auto;
   overflow: hidden;
   .action {
-    display: inline-block;
+    display: flex;
+    align-items: center;
     height: 100%;
     padding: 0 12px;
     cursor: pointer;
     transition: all 0.3s;
-    > i {
+    > span {
       color: @text-color;
       vertical-align: middle;
     }
@@ -53,7 +55,7 @@
 .dark {
   .action {
     color: rgba(255, 255, 255, 0.85);
-    > i {
+    > span {
       color: rgba(255, 255, 255, 0.85);
     }
     &:hover,
@@ -67,12 +69,12 @@
   .dark {
     .action {
       color: @text-color;
-      > i {
+      > span {
         color: @text-color;
       }
       &:hover {
         color: rgba(255, 255, 255, 0.85);
-        > i {
+        > span {
           color: rgba(255, 255, 255, 0.85);
         }
       }

+ 2 - 2
src/components/HeaderDropdown/index.tsx

@@ -6,9 +6,9 @@ import styles from './index.less';
 
 declare type OverlayFunc = () => React.ReactNode;
 
-export interface HeaderDropdownProps extends DropDownProps {
+export interface HeaderDropdownProps extends Omit<DropDownProps, 'overlay'> {
   overlayClassName?: string;
-  overlay: React.ReactNode | OverlayFunc;
+  overlay: React.ReactNode | OverlayFunc | any;
   placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
 }
 

+ 2 - 4
src/components/HeaderSearch/index.less

@@ -1,12 +1,10 @@
 @import '~antd/es/style/themes/default.less';
 
 .headerSearch {
-  :global(.anticon-search) {
-    font-size: 16px;
-    cursor: pointer;
-  }
   .input {
     width: 0;
+    min-width: 0;
+    overflow: hidden;
     background: transparent;
     border-radius: 0;
     transition: width 0.3s, margin-left 0.3s;

+ 86 - 128
src/components/HeaderSearch/index.tsx

@@ -1,147 +1,105 @@
-import { AutoComplete, Icon, Input } from 'antd';
-import { AutoCompleteProps, DataSourceItemType } from 'antd/es/auto-complete';
-import React, { Component } from 'react';
+import { SearchOutlined } from '@ant-design/icons';
+import { AutoComplete, Input } from 'antd';
+import useMergeValue from 'use-merge-value';
+import { AutoCompleteProps } from 'antd/es/auto-complete';
+import React, { useRef } from 'react';
 
 import classNames from 'classnames';
-import debounce from 'lodash/debounce';
 import styles from './index.less';
 
 export interface HeaderSearchProps {
-  onPressEnter: (value: string) => void;
-  onSearch: (value: string) => void;
-  onChange: (value: string) => void;
-  onVisibleChange: (b: boolean) => void;
-  className: string;
-  placeholder: string;
-  defaultActiveFirstOption: boolean;
-  dataSource: DataSourceItemType[];
-  defaultOpen: boolean;
+  onSearch?: (value?: string) => void;
+  onChange?: (value?: string) => void;
+  onVisibleChange?: (b: boolean) => void;
+  className?: string;
+  placeholder?: string;
+  options: AutoCompleteProps['options'];
+  defaultOpen?: boolean;
   open?: boolean;
   defaultValue?: string;
-}
-
-interface HeaderSearchState {
   value?: string;
-  searchMode: boolean;
 }
 
-export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
-  private inputRef: Input | null = null;
-
-  static defaultProps = {
-    defaultActiveFirstOption: false,
-    onPressEnter: () => {},
-    onSearch: () => {},
-    onChange: () => {},
-    className: '',
-    placeholder: '',
-    dataSource: [],
-    defaultOpen: false,
-    onVisibleChange: () => {},
-  };
-
-  static getDerivedStateFromProps(props: HeaderSearchProps) {
-    if ('open' in props) {
-      return {
-        searchMode: props.open,
-      };
-    }
-    return null;
-  }
-
-  constructor(props: HeaderSearchProps) {
-    super(props);
-    this.state = {
-      searchMode: props.defaultOpen,
-      value: props.defaultValue,
-    };
-    this.debouncePressEnter = debounce(this.debouncePressEnter, 500, {
-      leading: true,
-      trailing: false,
-    });
-  }
+const HeaderSearch: React.FC<HeaderSearchProps> = props => {
+  const {
+    className,
+    defaultValue,
+    onVisibleChange,
+    placeholder,
+    open,
+    defaultOpen,
+    ...restProps
+  } = props;
 
-  onKeyDown = (e: React.KeyboardEvent) => {
-    if (e.key === 'Enter') {
-      this.debouncePressEnter();
-    }
-  };
+  const inputRef = useRef<Input | null>(null);
 
-  onChange: AutoCompleteProps['onChange'] = value => {
-    if (typeof value === 'string') {
-      const { onSearch, onChange } = this.props;
-      this.setState({ value });
-      if (onSearch) {
-        onSearch(value);
-      }
-      if (onChange) {
-        onChange(value);
-      }
-    }
-  };
+  const [value, setValue] = useMergeValue<string | undefined>(defaultValue, {
+    value: props.value,
+    onChange: props.onChange,
+  });
 
-  enterSearchMode = () => {
-    const { onVisibleChange } = this.props;
-    onVisibleChange(true);
-    this.setState({ searchMode: true }, () => {
-      const { searchMode } = this.state;
-      if (searchMode && this.inputRef) {
-        this.inputRef.focus();
-      }
-    });
-  };
+  const [searchMode, setSearchMode] = useMergeValue(defaultOpen || false, {
+    value: props.open,
+    onChange: onVisibleChange,
+  });
 
-  leaveSearchMode = () => {
-    this.setState({
-      searchMode: false,
-    });
-  };
+  const inputClass = classNames(styles.input, {
+    [styles.show]: searchMode,
+  });
 
-  debouncePressEnter = () => {
-    const { onPressEnter } = this.props;
-    const { value } = this.state;
-    onPressEnter(value || '');
-  };
-
-  render() {
-    const { className, defaultValue, placeholder, open, ...restProps } = this.props;
-    const { searchMode, value } = this.state;
-    delete restProps.defaultOpen; // for rc-select not affected
-    const inputClass = classNames(styles.input, {
-      [styles.show]: searchMode,
-    });
-
-    return (
-      <span
-        className={classNames(className, styles.headerSearch)}
-        onClick={this.enterSearchMode}
-        onTransitionEnd={({ propertyName }) => {
-          if (propertyName === 'width' && !searchMode) {
-            const { onVisibleChange } = this.props;
+  return (
+    <div
+      className={classNames(className, styles.headerSearch)}
+      onClick={() => {
+        setSearchMode(true);
+        if (searchMode && inputRef.current) {
+          inputRef.current.focus();
+        }
+      }}
+      onTransitionEnd={({ propertyName }) => {
+        if (propertyName === 'width' && !searchMode) {
+          if (onVisibleChange) {
             onVisibleChange(searchMode);
           }
+        }
+      }}
+    >
+      <SearchOutlined
+        key="Icon"
+        style={{
+          cursor: 'pointer',
+        }}
+      />
+      <AutoComplete
+        key="AutoComplete"
+        className={inputClass}
+        value={value}
+        style={{
+          height: 28,
+          marginTop: -6,
         }}
+        options={restProps.options}
+        onChange={setValue}
       >
-        <Icon type="search" key="Icon" />
-        <AutoComplete
-          key="AutoComplete"
-          {...restProps}
-          className={inputClass}
-          value={value}
-          onChange={this.onChange}
-        >
-          <Input
-            ref={node => {
-              this.inputRef = node;
-            }}
-            defaultValue={defaultValue}
-            aria-label={placeholder}
-            placeholder={placeholder}
-            onKeyDown={this.onKeyDown}
-            onBlur={this.leaveSearchMode}
-          />
-        </AutoComplete>
-      </span>
-    );
-  }
-}
+        <Input
+          ref={inputRef}
+          defaultValue={defaultValue}
+          aria-label={placeholder}
+          placeholder={placeholder}
+          onKeyDown={e => {
+            if (e.key === 'Enter') {
+              if (restProps.onSearch) {
+                restProps.onSearch(value);
+              }
+            }
+          }}
+          onBlur={() => {
+            setSearchMode(false);
+          }}
+        />
+      </AutoComplete>
+    </div>
+  );
+};
+
+export default HeaderSearch;

+ 66 - 104
src/components/NoticeIcon/index.tsx

@@ -1,5 +1,7 @@
-import { Badge, Icon, Spin, Tabs } from 'antd';
-import React, { Component } from 'react';
+import { BellOutlined } from '@ant-design/icons';
+import { Badge, Spin, Tabs } from 'antd';
+import useMergeValue from 'use-merge-value';
+import React from 'react';
 import classNames from 'classnames';
 import NoticeList, { NoticeIconTabProps } from './NoticeList';
 
@@ -34,57 +36,24 @@ export interface NoticeIconProps {
   clearText?: string;
   viewMoreText?: string;
   clearClose?: boolean;
+  emptyImage?: string;
   children: React.ReactElement<NoticeIconTabProps>[];
 }
 
-export default class NoticeIcon extends Component<NoticeIconProps> {
-  public static Tab: typeof NoticeList = NoticeList;
-
-  static defaultProps = {
-    onItemClick: (): void => {},
-    onPopupVisibleChange: (): void => {},
-    onTabChange: (): void => {},
-    onClear: (): void => {},
-    onViewMore: (): void => {},
-    loading: false,
-    clearClose: false,
-    emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
-  };
-
-  state = {
-    visible: false,
-  };
-
-  onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps): void => {
-    const { onItemClick } = this.props;
-    if (onItemClick) {
-      onItemClick(item, tabProps);
-    }
-  };
-
-  onClear = (name: string, key: string): void => {
-    const { onClear } = this.props;
-    if (onClear) {
-      onClear(name, key);
-    }
-  };
-
-  onTabChange = (tabType: string): void => {
-    const { onTabChange } = this.props;
-    if (onTabChange) {
-      onTabChange(tabType);
-    }
-  };
-
-  onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent): void => {
-    const { onViewMore } = this.props;
-    if (onViewMore) {
-      onViewMore(tabProps, event);
-    }
-  };
-
-  getNotificationBox(): React.ReactNode {
-    const { children, loading, clearText, viewMoreText } = this.props;
+const NoticeIcon: React.FC<NoticeIconProps> & {
+  Tab: typeof NoticeList;
+} = props => {
+  const getNotificationBox = (): React.ReactNode => {
+    const {
+      children,
+      loading,
+      onClear,
+      onTabChange,
+      onItemClick,
+      onViewMore,
+      clearText,
+      viewMoreText,
+    } = props;
     if (!children) {
       return null;
     }
@@ -103,9 +72,9 @@ export default class NoticeIcon extends Component<NoticeIconProps> {
             clearText={clearText}
             viewMoreText={viewMoreText}
             data={list}
-            onClear={(): void => this.onClear(title, tabKey)}
-            onClick={(item): void => this.onItemClick(item, child.props)}
-            onViewMore={(event): void => this.onViewMore(child.props, event)}
+            onClear={(): void => onClear && onClear(title, tabKey)}
+            onClick={(item): void => onItemClick && onItemClick(item, child.props)}
+            onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
             showClear={showClear}
             showViewMore={showViewMore}
             title={title}
@@ -115,59 +84,52 @@ export default class NoticeIcon extends Component<NoticeIconProps> {
       );
     });
     return (
-      <>
-        <Spin spinning={loading} delay={300}>
-          <Tabs className={styles.tabs} onChange={this.onTabChange}>
-            {panes}
-          </Tabs>
-        </Spin>
-      </>
+      <Spin spinning={loading} delay={300}>
+        <Tabs className={styles.tabs} onChange={onTabChange}>
+          {panes}
+        </Tabs>
+      </Spin>
     );
-  }
-
-  handleVisibleChange = (visible: boolean): void => {
-    const { onPopupVisibleChange } = this.props;
-    this.setState({ visible });
-    if (onPopupVisibleChange) {
-      onPopupVisibleChange(visible);
-    }
   };
 
-  render(): React.ReactNode {
-    const { className, count, popupVisible, bell } = this.props;
-    const { visible } = this.state;
-    const noticeButtonClass = classNames(className, styles.noticeButton);
-    const notificationBox = this.getNotificationBox();
-    const NoticeBellIcon = bell || <Icon type="bell" className={styles.icon} />;
-    const trigger = (
-      <span className={classNames(noticeButtonClass, { opened: visible })}>
-        <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
-          {NoticeBellIcon}
-        </Badge>
-      </span>
-    );
-    if (!notificationBox) {
-      return trigger;
-    }
-    const popoverProps: {
-      visible?: boolean;
-    } = {};
-    if ('popupVisible' in this.props) {
-      popoverProps.visible = popupVisible;
-    }
+  const { className, count, bell } = props;
 
-    return (
-      <HeaderDropdown
-        placement="bottomRight"
-        overlay={notificationBox}
-        overlayClassName={styles.popover}
-        trigger={['click']}
-        visible={visible}
-        onVisibleChange={this.handleVisibleChange}
-        {...popoverProps}
-      >
-        {trigger}
-      </HeaderDropdown>
-    );
+  const [visible, setVisible] = useMergeValue<boolean>(false, {
+    value: props.popupVisible,
+    onChange: props.onPopupVisibleChange,
+  });
+  const noticeButtonClass = classNames(className, styles.noticeButton);
+  const notificationBox = getNotificationBox();
+  const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
+  const trigger = (
+    <span className={classNames(noticeButtonClass, { opened: visible })}>
+      <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
+        {NoticeBellIcon}
+      </Badge>
+    </span>
+  );
+  if (!notificationBox) {
+    return trigger;
   }
-}
+
+  return (
+    <HeaderDropdown
+      placement="bottomRight"
+      overlay={notificationBox}
+      overlayClassName={styles.popover}
+      trigger={['click']}
+      visible={visible}
+      onVisibleChange={setVisible}
+    >
+      {trigger}
+    </HeaderDropdown>
+  );
+};
+
+NoticeIcon.defaultProps = {
+  emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
+};
+
+NoticeIcon.Tab = NoticeList;
+
+export default NoticeIcon;

+ 1 - 1
src/components/SelectLang/index.less

@@ -13,7 +13,7 @@
   line-height: @layout-header-height;
   vertical-align: top;
   cursor: pointer;
-  > i {
+  > span {
     font-size: 16px !important;
     transform: none !important;
     svg {

+ 7 - 4
src/components/SelectLang/index.tsx

@@ -1,6 +1,6 @@
-import { Icon, Menu } from 'antd';
-import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale';
-
+import { GlobalOutlined } from '@ant-design/icons';
+import { Menu } from 'antd';
+import { getLocale, setLocale } from 'umi-plugin-react/locale';
 import { ClickParam } from 'antd/es/menu';
 import React from 'react';
 import classNames from 'classnames';
@@ -10,10 +10,13 @@ import styles from './index.less';
 interface SelectLangProps {
   className?: string;
 }
+
 const SelectLang: React.FC<SelectLangProps> = props => {
   const { className } = props;
   const selectedLang = getLocale();
+
   const changeLang = ({ key }: ClickParam): void => setLocale(key);
+
   const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
   const languageLabels = {
     'zh-CN': '简体中文',
@@ -42,7 +45,7 @@ const SelectLang: React.FC<SelectLangProps> = props => {
   return (
     <HeaderDropdown overlay={langMenu} placement="bottomRight">
       <span className={classNames(styles.dropDown, className)}>
-        <Icon type="global" title={formatMessage({ id: 'navBar.lang' })} />
+        <GlobalOutlined title="语言" />
       </span>
     </HeaderDropdown>
   );

+ 21 - 20
src/layouts/BasicLayout.tsx

@@ -3,20 +3,19 @@
  * You can view component api by:
  * https://github.com/ant-design/ant-design-pro-layout
  */
-
 import ProLayout, {
   MenuDataItem,
   BasicLayoutProps as ProLayoutProps,
   Settings,
   DefaultFooter,
 } from '@ant-design/pro-layout';
+import { formatMessage } from 'umi-plugin-react/locale';
 import React, { useEffect } from 'react';
 import { Link } from 'umi';
 import { Dispatch } from 'redux';
 import { connect } from 'dva';
-import { Icon, Result, Button } from 'antd';
-import { formatMessage } from 'umi-plugin-react/locale';
-
+import { GithubOutlined } from '@ant-design/icons';
+import { Result, Button } from 'antd';
 import Authorized from '@/utils/Authorized';
 import RightContent from '@/components/GlobalHeader/RightContent';
 import { ConnectState } from '@/models/connect';
@@ -35,7 +34,6 @@ const noMatch = (
     }
   />
 );
-
 export interface BasicLayoutProps extends ProLayoutProps {
   breadcrumbNameMap: {
     [path: string]: MenuDataItem;
@@ -51,16 +49,13 @@ export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
     [path: string]: MenuDataItem;
   };
 };
-
 /**
  * use Authorized check all menu item
  */
+
 const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
   menuList.map(item => {
-    const localItem = {
-      ...item,
-      children: item.children ? menuDataRender(item.children) : [],
-    };
+    const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
     return Authorized.check(item.authority, localItem, null) as MenuDataItem;
   });
 
@@ -76,7 +71,7 @@ const defaultFooterDom = (
       },
       {
         key: 'github',
-        title: <Icon type="github" />,
+        title: <GithubOutlined />,
         href: 'https://github.com/ant-design/ant-design-pro',
         blankTarget: true,
       },
@@ -94,6 +89,7 @@ const footerRender: BasicLayoutProps['footerRender'] = () => {
   if (!isAntDesignPro()) {
     return defaultFooterDom;
   }
+
   return (
     <>
       {defaultFooterDom}
@@ -116,7 +112,14 @@ const footerRender: BasicLayoutProps['footerRender'] = () => {
 };
 
 const BasicLayout: React.FC<BasicLayoutProps> = props => {
-  const { dispatch, children, settings, location = { pathname: '/' } } = props;
+  const {
+    dispatch,
+    children,
+    settings,
+    location = {
+      pathname: '/',
+    },
+  } = props;
   /**
    * constructor
    */
@@ -131,6 +134,7 @@ const BasicLayout: React.FC<BasicLayoutProps> = props => {
   /**
    * init variables
    */
+
   const handleMenuCollapse = (payload: boolean): void => {
     if (dispatch) {
       dispatch({
@@ -138,15 +142,15 @@ const BasicLayout: React.FC<BasicLayoutProps> = props => {
         payload,
       });
     }
-  };
-  // get children authority
+  }; // get children authority
+
   const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || {
     authority: undefined,
   };
-
   return (
     <ProLayout
       logo={logo}
+      formatMessage={formatMessage}
       menuHeaderRender={(logoDom, titleDom) => (
         <Link to="/">
           {logoDom}
@@ -158,15 +162,13 @@ const BasicLayout: React.FC<BasicLayoutProps> = props => {
         if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) {
           return defaultDom;
         }
+
         return <Link to={menuItemProps.path}>{defaultDom}</Link>;
       }}
       breadcrumbRender={(routers = []) => [
         {
           path: '/',
-          breadcrumbName: formatMessage({
-            id: 'menu.home',
-            defaultMessage: 'Home',
-          }),
+          breadcrumbName: '首页',
         },
         ...routers,
       ]}
@@ -180,7 +182,6 @@ const BasicLayout: React.FC<BasicLayoutProps> = props => {
       }}
       footerRender={footerRender}
       menuDataRender={menuDataRender}
-      formatMessage={formatMessage}
       rightContentRender={() => <RightContent />}
       {...props}
       {...settings}

+ 6 - 7
src/layouts/UserLayout.tsx

@@ -2,16 +2,17 @@ import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-des
 import { Helmet } from 'react-helmet';
 import { Link } from 'umi';
 import React from 'react';
-import { connect } from 'dva';
 import { formatMessage } from 'umi-plugin-react/locale';
-
+import { connect } from 'dva';
 import SelectLang from '@/components/SelectLang';
 import { ConnectProps, ConnectState } from '@/models/connect';
 import logo from '../assets/logo.svg';
 import styles from './UserLayout.less';
 
 export interface UserLayoutProps extends ConnectProps {
-  breadcrumbNameMap: { [path: string]: MenuDataItem };
+  breadcrumbNameMap: {
+    [path: string]: MenuDataItem;
+  };
 }
 
 const UserLayout: React.FC<UserLayoutProps> = props => {
@@ -30,8 +31,8 @@ const UserLayout: React.FC<UserLayoutProps> = props => {
   const { breadcrumb } = getMenuData(routes);
   const title = getPageTitle({
     pathname: location.pathname,
-    breadcrumb,
     formatMessage,
+    breadcrumb,
     ...props,
   });
   return (
@@ -63,6 +64,4 @@ const UserLayout: React.FC<UserLayoutProps> = props => {
   );
 };
 
-export default connect(({ settings }: ConnectState) => ({
-  ...settings,
-}))(UserLayout);
+export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout);

+ 1 - 6
src/models/login.ts

@@ -3,7 +3,7 @@ import { Effect } from 'dva';
 import { stringify } from 'querystring';
 import { router } from 'umi';
 
-import { fakeAccountLogin, getFakeCaptcha } from '@/services/login';
+import { fakeAccountLogin } from '@/services/login';
 import { setAuthority } from '@/utils/authority';
 import { getPageQuery } from '@/utils/utils';
 
@@ -18,7 +18,6 @@ export interface LoginModelType {
   state: StateType;
   effects: {
     login: Effect;
-    getCaptcha: Effect;
     logout: Effect;
   };
   reducers: {
@@ -61,10 +60,6 @@ const Model: LoginModelType = {
       }
     },
 
-    *getCaptcha({ payload }, { call }) {
-      yield call(getFakeCaptcha, payload);
-    },
-
     logout() {
       const { redirect } = getPageQuery();
       // Note: There may be security issues, please note

+ 0 - 3
src/pages/404.tsx

@@ -2,9 +2,6 @@ import { Button, Result } from 'antd';
 import React from 'react';
 import { router } from 'umi';
 
-// 这里应该使用 antd 的 404 result 组件,
-// 但是还没发布,先来个简单的。
-
 const NoFoundPage: React.FC<{}> = () => (
   <Result
     status={404}

+ 3 - 3
src/pages/Admin.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
-import { Card, Typography, Alert, Icon } from 'antd';
+import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
+import { Card, Typography, Alert } from 'antd';
 import { PageHeaderWrapper } from '@ant-design/pro-layout';
 
 export default (): React.ReactNode => (
@@ -16,8 +17,7 @@ export default (): React.ReactNode => (
         }}
       />
       <Typography.Title level={2} style={{ textAlign: 'center' }}>
-        <Icon type="smile" theme="twoTone" /> Ant Design Pro{' '}
-        <Icon type="heart" theme="twoTone" twoToneColor="#eb2f96" /> You
+        <SmileTwoTone /> Ant Design Pro <HeartTwoTone twoToneColor="#eb2f96" /> You
       </Typography.Title>
     </Card>
     <p style={{ textAlign: 'center', marginTop: 24 }}>

+ 154 - 0
src/pages/ListTableList/_mock.ts

@@ -0,0 +1,154 @@
+import { Request, Response } from 'express';
+import { parse } from 'url';
+import { TableListItem, TableListParams } from './data.d';
+
+// mock tableListDataSource
+const genList = (current: number, pageSize: number) => {
+  const tableListDataSource: TableListItem[] = [];
+
+  for (let i = 0; i < pageSize; i += 1) {
+    const index = (current - 1) * 10 + i;
+    tableListDataSource.push({
+      key: index,
+      disabled: i % 6 === 0,
+      href: 'https://ant.design',
+      avatar: [
+        'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+        'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+      ][i % 2],
+      name: `TradeCode ${index}`,
+      owner: '曲丽丽',
+      desc: '这是一段描述',
+      callNo: Math.floor(Math.random() * 1000),
+      status: Math.floor(Math.random() * 10) % 4,
+      updatedAt: new Date(),
+      createdAt: new Date(),
+      progress: Math.ceil(Math.random() * 100),
+    });
+  }
+  tableListDataSource.reverse();
+  return tableListDataSource;
+};
+
+let tableListDataSource = genList(1, 100);
+
+function getRule(req: Request, res: Response, u: string) {
+  let url = u;
+  if (!url || Object.prototype.toString.call(url) !== '[object String]') {
+    // eslint-disable-next-line prefer-destructuring
+    url = req.url;
+  }
+  const { current = 1, pageSize = 10 } = req.query;
+  const params = (parse(url, true).query as unknown) as TableListParams;
+
+  let dataSource = [...tableListDataSource].slice((current - 1) * pageSize, current * pageSize);
+  if (params.sorter) {
+    const s = params.sorter.split('_');
+    dataSource = dataSource.sort((prev, next) => {
+      if (s[1] === 'descend') {
+        return next[s[0]] - prev[s[0]];
+      }
+      return prev[s[0]] - next[s[0]];
+    });
+  }
+
+  if (params.status) {
+    const status = params.status.split(',');
+    let filterDataSource: TableListItem[] = [];
+    status.forEach((s: string) => {
+      filterDataSource = filterDataSource.concat(
+        dataSource.filter(item => {
+          if (parseInt(`${item.status}`, 10) === parseInt(s.split('')[0], 10)) {
+            return true;
+          }
+          return false;
+        }),
+      );
+    });
+    dataSource = filterDataSource;
+  }
+
+  if (params.name) {
+    dataSource = dataSource.filter(data => data.name.includes(params.name || ''));
+  }
+  const result = {
+    data: dataSource,
+    total: tableListDataSource.length,
+    success: true,
+    pageSize,
+    current: parseInt(`${params.currentPage}`, 10) || 1,
+  };
+
+  return res.json(result);
+}
+
+function postRule(req: Request, res: Response, u: string, b: Request) {
+  let url = u;
+  if (!url || Object.prototype.toString.call(url) !== '[object String]') {
+    // eslint-disable-next-line prefer-destructuring
+    url = req.url;
+  }
+
+  const body = (b && b.body) || req.body;
+  const { method, name, desc, key } = body;
+
+  switch (method) {
+    /* eslint no-case-declarations:0 */
+    case 'delete':
+      tableListDataSource = tableListDataSource.filter(item => key.indexOf(item.key) === -1);
+      break;
+    case 'post':
+      (() => {
+        const i = Math.ceil(Math.random() * 10000);
+        const newRule = {
+          key: tableListDataSource.length,
+          href: 'https://ant.design',
+          avatar: [
+            'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+            'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+          ][i % 2],
+          name,
+          owner: '曲丽丽',
+          desc,
+          callNo: Math.floor(Math.random() * 1000),
+          status: Math.floor(Math.random() * 10) % 2,
+          updatedAt: new Date(),
+          createdAt: new Date(),
+          progress: Math.ceil(Math.random() * 100),
+        };
+        tableListDataSource.unshift(newRule);
+        return res.json(newRule);
+      })();
+      return;
+
+    case 'update':
+      (() => {
+        let newRule = {};
+        tableListDataSource = tableListDataSource.map(item => {
+          if (item.key === key) {
+            newRule = { ...item, desc, name };
+            return { ...item, desc, name };
+          }
+          return item;
+        });
+        return res.json(newRule);
+      })();
+      return;
+    default:
+      break;
+  }
+
+  const result = {
+    list: tableListDataSource,
+    pagination: {
+      total: tableListDataSource.length,
+    },
+  };
+
+  res.json(result);
+}
+
+export default {
+  'GET /api/rule': getRule,
+  'POST /api/rule': postRule,
+};

+ 25 - 0
src/pages/ListTableList/components/CreateForm.tsx

@@ -0,0 +1,25 @@
+import React from 'react';
+import { Modal } from 'antd';
+
+interface CreateFormProps {
+  modalVisible: boolean;
+  onCancel: () => void;
+}
+
+const CreateForm: React.FC<CreateFormProps> = props => {
+  const { modalVisible, onCancel } = props;
+
+  return (
+    <Modal
+      destroyOnClose
+      title="新建规则"
+      visible={modalVisible}
+      onCancel={() => onCancel()}
+      footer={null}
+    >
+      {props.children}
+    </Modal>
+  );
+};
+
+export default CreateForm;

+ 215 - 0
src/pages/ListTableList/components/UpdateForm.tsx

@@ -0,0 +1,215 @@
+import React, { useState } from 'react';
+import { Form, Button, DatePicker, Input, Modal, Radio, Select, Steps } from 'antd';
+
+import { TableListItem } from '../data.d';
+
+export interface FormValueType extends Partial<TableListItem> {
+  target?: string;
+  template?: string;
+  type?: string;
+  time?: string;
+  frequency?: string;
+}
+
+export interface UpdateFormProps {
+  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+  onSubmit: (values: FormValueType) => void;
+  updateModalVisible: boolean;
+  values: Partial<TableListItem>;
+}
+const FormItem = Form.Item;
+const { Step } = Steps;
+const { TextArea } = Input;
+const { Option } = Select;
+const RadioGroup = Radio.Group;
+
+export interface UpdateFormState {
+  formVals: FormValueType;
+  currentStep: number;
+}
+
+const formLayout = {
+  labelCol: { span: 7 },
+  wrapperCol: { span: 13 },
+};
+
+const UpdateForm: React.FC<UpdateFormProps> = props => {
+  const [formVals, setFormVals] = useState<FormValueType>({
+    name: props.values.name,
+    desc: props.values.desc,
+    key: props.values.key,
+    target: '0',
+    template: '0',
+    type: '1',
+    time: '',
+    frequency: 'month',
+  });
+
+  const [currentStep, setCurrentStep] = useState<number>(0);
+
+  const [form] = Form.useForm();
+
+  const {
+    onSubmit: handleUpdate,
+    onCancel: handleUpdateModalVisible,
+    updateModalVisible,
+    values,
+  } = props;
+
+  const forward = () => setCurrentStep(currentStep + 1);
+
+  const backward = () => setCurrentStep(currentStep - 1);
+
+  const handleNext = async () => {
+    const fieldsValue = await form.validateFields();
+
+    setFormVals({ ...formVals, ...fieldsValue });
+
+    if (currentStep < 2) {
+      forward();
+    } else {
+      handleUpdate(formVals);
+    }
+  };
+
+  const renderContent = () => {
+    if (currentStep === 1) {
+      return (
+        <>
+          <FormItem name="target" label="监控对象">
+            <Select style={{ width: '100%' }}>
+              <Option value="0">表一</Option>
+              <Option value="1">表二</Option>
+            </Select>
+          </FormItem>
+          <FormItem name="template" label="规则模板">
+            <Select style={{ width: '100%' }}>
+              <Option value="0">规则模板一</Option>
+              <Option value="1">规则模板二</Option>
+            </Select>
+          </FormItem>
+          <FormItem name="type" label="规则类型">
+            <RadioGroup>
+              <Radio value="0">强</Radio>
+              <Radio value="1">弱</Radio>
+            </RadioGroup>
+          </FormItem>
+        </>
+      );
+    }
+    if (currentStep === 2) {
+      return (
+        <>
+          <FormItem
+            name="time"
+            label="开始时间"
+            rules={[{ required: true, message: '请选择开始时间!' }]}
+          >
+            <DatePicker
+              style={{ width: '100%' }}
+              showTime
+              format="YYYY-MM-DD HH:mm:ss"
+              placeholder="选择开始时间"
+            />
+          </FormItem>
+          <FormItem name="frequency" label="调度周期">
+            <Select style={{ width: '100%' }}>
+              <Option value="month">月</Option>
+              <Option value="week">周</Option>
+            </Select>
+          </FormItem>
+        </>
+      );
+    }
+    return (
+      <>
+        <FormItem
+          name="name"
+          label="规则名称"
+          rules={[{ required: true, message: '请输入规则名称!' }]}
+        >
+          <Input placeholder="请输入" />
+        </FormItem>
+        <FormItem
+          name="desc"
+          label="规则描述"
+          rules={[{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 }]}
+        >
+          <TextArea rows={4} placeholder="请输入至少五个字符" />
+        </FormItem>
+      </>
+    );
+  };
+
+  const renderFooter = () => {
+    if (currentStep === 1) {
+      return (
+        <>
+          <Button style={{ float: 'left' }} onClick={backward}>
+            上一步
+          </Button>
+          <Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
+          <Button type="primary" onClick={() => handleNext()}>
+            下一步
+          </Button>
+        </>
+      );
+    }
+    if (currentStep === 2) {
+      return (
+        <>
+          <Button style={{ float: 'left' }} onClick={backward}>
+            上一步
+          </Button>
+          <Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
+          <Button type="primary" onClick={() => handleNext()}>
+            完成
+          </Button>
+        </>
+      );
+    }
+    return (
+      <>
+        <Button onClick={() => handleUpdateModalVisible(false, values)}>取消</Button>
+        <Button type="primary" onClick={() => handleNext()}>
+          下一步
+        </Button>
+      </>
+    );
+  };
+
+  return (
+    <Modal
+      width={640}
+      bodyStyle={{ padding: '32px 40px 48px' }}
+      destroyOnClose
+      title="规则配置"
+      visible={updateModalVisible}
+      footer={renderFooter()}
+      onCancel={() => handleUpdateModalVisible(false, values)}
+      afterClose={() => handleUpdateModalVisible()}
+    >
+      <Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
+        <Step title="基本信息" />
+        <Step title="配置规则属性" />
+        <Step title="设定调度周期" />
+      </Steps>
+      <Form
+        {...formLayout}
+        form={form}
+        initialValues={{
+          target: formVals.target,
+          template: formVals.template,
+          type: formVals.type,
+          frequency: formVals.frequency,
+          name: formVals.name,
+          desc: formVals.desc,
+        }}
+      >
+        {renderContent()}
+      </Form>
+    </Modal>
+  );
+};
+
+export default UpdateForm;

+ 35 - 0
src/pages/ListTableList/data.d.ts

@@ -0,0 +1,35 @@
+export interface TableListItem {
+  key: number;
+  disabled?: boolean;
+  href: string;
+  avatar: string;
+  name: string;
+  owner: string;
+  desc: string;
+  callNo: number;
+  status: number;
+  updatedAt: Date;
+  createdAt: Date;
+  progress: number;
+}
+
+export interface TableListPagination {
+  total: number;
+  pageSize: number;
+  current: number;
+}
+
+export interface TableListData {
+  list: TableListItem[];
+  pagination: Partial<TableListPagination>;
+}
+
+export interface TableListParams {
+  sorter?: string;
+  status?: string;
+  name?: string;
+  desc?: string;
+  key?: number;
+  pageSize?: number;
+  currentPage?: number;
+}

+ 238 - 0
src/pages/ListTableList/index.tsx

@@ -0,0 +1,238 @@
+import { DownOutlined, PlusOutlined } from '@ant-design/icons';
+import { Button, Divider, Dropdown, Menu, message } from 'antd';
+import React, { useState, useRef } from 'react';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table';
+import { SorterResult } from 'antd/es/table/interface';
+
+import CreateForm from './components/CreateForm';
+import UpdateForm, { FormValueType } from './components/UpdateForm';
+import { TableListItem } from './data.d';
+import { queryRule, updateRule, addRule, removeRule } from './service';
+
+/**
+ * 添加节点
+ * @param fields
+ */
+const handleAdd = async (fields: TableListItem) => {
+  const hide = message.loading('正在添加');
+  try {
+    await addRule({ ...fields });
+    hide();
+    message.success('添加成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('添加失败请重试!');
+    return false;
+  }
+};
+
+/**
+ * 更新节点
+ * @param fields
+ */
+const handleUpdate = async (fields: FormValueType) => {
+  const hide = message.loading('正在配置');
+  try {
+    await updateRule({
+      name: fields.name,
+      desc: fields.desc,
+      key: fields.key,
+    });
+    hide();
+
+    message.success('配置成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('配置失败请重试!');
+    return false;
+  }
+};
+
+/**
+ *  删除节点
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: TableListItem[]) => {
+  const hide = message.loading('正在删除');
+  if (!selectedRows) return true;
+  try {
+    await removeRule({
+      key: selectedRows.map(row => row.key),
+    });
+    hide();
+    message.success('删除成功,即将刷新');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('删除失败,请重试');
+    return false;
+  }
+};
+
+const TableList: React.FC<{}> = () => {
+  const [sorter, setSorter] = useState<string>('');
+  const [createModalVisible, handleModalVisible] = useState<boolean>(false);
+  const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
+  const [stepFormValues, setStepFormValues] = useState({});
+  const actionRef = useRef<ActionType>();
+  const columns: ProColumns<TableListItem>[] = [
+    {
+      title: '规则名称',
+      dataIndex: 'name',
+      rules: [
+        {
+          required: true,
+          message: '规则名称为必填项',
+        },
+      ],
+    },
+    {
+      title: '描述',
+      dataIndex: 'desc',
+      valueType: 'textarea',
+    },
+    {
+      title: '服务调用次数',
+      dataIndex: 'callNo',
+      sorter: true,
+      hideInForm: true,
+      renderText: (val: string) => `${val} 万`,
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      hideInForm: true,
+      valueEnum: {
+        0: { text: '关闭', status: 'Default' },
+        1: { text: '运行中', status: 'Processing' },
+        2: { text: '已上线', status: 'Success' },
+        3: { text: '异常', status: 'Error' },
+      },
+    },
+    {
+      title: '上次调度时间',
+      dataIndex: 'updatedAt',
+      sorter: true,
+      valueType: 'dateTime',
+      hideInForm: true,
+    },
+    {
+      title: '操作',
+      dataIndex: 'option',
+      valueType: 'option',
+      render: (_, record) => (
+        <>
+          <a
+            onClick={() => {
+              handleUpdateModalVisible(true);
+              setStepFormValues(record);
+            }}
+          >
+            配置
+          </a>
+          <Divider type="vertical" />
+          <a href="">订阅警报</a>
+        </>
+      ),
+    },
+  ];
+
+  return (
+    <PageHeaderWrapper>
+      <ProTable<TableListItem>
+        headerTitle="查询表格"
+        actionRef={actionRef}
+        rowKey="key"
+        onChange={(_, _filter, _sorter) => {
+          const sorterResult = _sorter as SorterResult<TableListItem>;
+          if (sorterResult.field) {
+            setSorter(`${sorterResult.field}_${sorterResult.order}`);
+          }
+        }}
+        params={{
+          sorter,
+        }}
+        toolBarRender={(action, { selectedRows }) => [
+          <Button type="primary" onClick={() => handleModalVisible(true)}>
+            <PlusOutlined /> 新建
+          </Button>,
+          selectedRows && selectedRows.length > 0 && (
+            <Dropdown
+              overlay={
+                <Menu
+                  onClick={async e => {
+                    if (e.key === 'remove') {
+                      await handleRemove(selectedRows);
+                      action.reload();
+                    }
+                  }}
+                  selectedKeys={[]}
+                >
+                  <Menu.Item key="remove">批量删除</Menu.Item>
+                  <Menu.Item key="approval">批量审批</Menu.Item>
+                </Menu>
+              }
+            >
+              <Button>
+                批量操作 <DownOutlined />
+              </Button>
+            </Dropdown>
+          ),
+        ]}
+        tableAlertRender={(selectedRowKeys, selectedRows) => (
+          <div>
+            已选择 <a style={{ fontWeight: 600 }}>{selectedRowKeys.length}</a> 项&nbsp;&nbsp;
+            <span>
+              服务调用次数总计 {selectedRows.reduce((pre, item) => pre + item.callNo, 0)} 万
+            </span>
+          </div>
+        )}
+        request={params => queryRule(params)}
+        columns={columns}
+        rowSelection={{}}
+      />
+      <CreateForm onCancel={() => handleModalVisible(false)} modalVisible={createModalVisible}>
+        <ProTable<TableListItem, TableListItem>
+          onSubmit={async value => {
+            const success = await handleAdd(value);
+            if (success) {
+              handleModalVisible(false);
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          rowKey="key"
+          type="form"
+          columns={columns}
+          rowSelection={{}}
+        />
+      </CreateForm>
+      {stepFormValues && Object.keys(stepFormValues).length ? (
+        <UpdateForm
+          onSubmit={async value => {
+            const success = await handleUpdate(value);
+            if (success) {
+              handleModalVisible(false);
+              setStepFormValues({});
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          onCancel={() => {
+            handleUpdateModalVisible(false);
+            setStepFormValues({});
+          }}
+          updateModalVisible={updateModalVisible}
+          values={stepFormValues}
+        />
+      ) : null}
+    </PageHeaderWrapper>
+  );
+};
+
+export default TableList;

+ 38 - 0
src/pages/ListTableList/service.ts

@@ -0,0 +1,38 @@
+import request from '@/utils/request';
+import { TableListParams, TableListItem } from './data.d';
+
+export async function queryRule(params?: TableListParams) {
+  return request('/api/rule', {
+    params,
+  });
+}
+
+export async function removeRule(params: { key: number[] }) {
+  return request('/api/rule', {
+    method: 'POST',
+    data: {
+      ...params,
+      method: 'delete',
+    },
+  });
+}
+
+export async function addRule(params: TableListItem) {
+  return request('/api/rule', {
+    method: 'POST',
+    data: {
+      ...params,
+      method: 'post',
+    },
+  });
+}
+
+export async function updateRule(params: TableListParams) {
+  return request('/api/rule', {
+    method: 'POST',
+    data: {
+      ...params,
+      method: 'update',
+    },
+  });
+}

+ 8 - 8
src/pages/Welcome.tsx

@@ -1,8 +1,6 @@
 import React from 'react';
 import { PageHeaderWrapper } from '@ant-design/pro-layout';
-import { FormattedMessage } from 'umi-plugin-react/locale';
 import { Card, Typography, Alert } from 'antd';
-
 import styles from './Welcome.less';
 
 const CodePreview: React.FC<{}> = ({ children }) => (
@@ -28,10 +26,7 @@ export default (): React.ReactNode => (
       />
       <Typography.Text strong>
         <a target="_blank" rel="noopener noreferrer" href="https://pro.ant.design/docs/block">
-          <FormattedMessage
-            id="app.welcome.link.block-list"
-            defaultMessage="基于 block 开发,快速构建标准页面"
-          />
+          基于 block 开发,快速构建标准页面
         </a>
       </Typography.Text>
       <CodePreview> npm run ui</CodePreview>
@@ -46,12 +41,17 @@ export default (): React.ReactNode => (
           rel="noopener noreferrer"
           href="https://pro.ant.design/docs/available-script#npm-run-fetchblocks"
         >
-          <FormattedMessage id="app.welcome.link.fetch-blocks" defaultMessage="获取全部区块" />
+          获取全部区块
         </a>
       </Typography.Text>
       <CodePreview> npm run fetch:blocks</CodePreview>
     </Card>
-    <p style={{ textAlign: 'center', marginTop: 24 }}>
+    <p
+      style={{
+        textAlign: 'center',
+        marginTop: 24,
+      }}
+    >
       Want to add more pages? Please refer to{' '}
       <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
         use block

+ 98 - 125
src/pages/user/login/components/Login/LoginItem.tsx

@@ -1,16 +1,14 @@
-import { Button, Col, Form, Input, Row } from 'antd';
-import React, { Component } from 'react';
-import { FormComponentProps } from 'antd/es/form';
-import { GetFieldDecoratorOptions } from 'antd/es/form/Form';
-
+import { Button, Col, Input, Row, Form, message } from 'antd';
+import React, { useState, useCallback, useEffect } from 'react';
 import omit from 'omit.js';
+import { FormItemProps } from 'antd/es/form/FormItem';
+import { getFakeCaptcha } from '@/services/login';
+
 import ItemMap from './map';
 import LoginContext, { LoginContextProps } from './LoginContext';
 import styles from './index.less';
 
-type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
-
-export type WrappedLoginItemProps = Omit<LoginItemProps, 'form' | 'type' | 'updateActive'>;
+export type WrappedLoginItemProps = LoginItemProps;
 export type LoginItemKeyType = keyof typeof ItemMap;
 export interface LoginItemType {
   UserName: React.FC<WrappedLoginItemProps>;
@@ -19,168 +17,143 @@ export interface LoginItemType {
   Captcha: React.FC<WrappedLoginItemProps>;
 }
 
-export interface LoginItemProps extends GetFieldDecoratorOptions {
+export interface LoginItemProps extends Partial<FormItemProps> {
   name?: string;
   style?: React.CSSProperties;
-  onGetCaptcha?: (event?: MouseEvent) => void | Promise<boolean> | false;
   placeholder?: string;
   buttonText?: React.ReactNode;
-  onPressEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
   countDown?: number;
   getCaptchaButtonText?: string;
   getCaptchaSecondText?: string;
   updateActive?: LoginContextProps['updateActive'];
   type?: string;
   defaultValue?: string;
-  form?: FormComponentProps['form'];
   customProps?: { [key: string]: unknown };
   onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
   tabUtil?: LoginContextProps['tabUtil'];
 }
 
-interface LoginItemState {
-  count: number;
-}
-
 const FormItem = Form.Item;
 
-class WrapFormItem extends Component<LoginItemProps, LoginItemState> {
-  interval: number | undefined = undefined;
-
-  static defaultProps = {
-    getCaptchaButtonText: 'captcha',
-    getCaptchaSecondText: 'second',
+const getFormItemOptions = ({
+  onChange,
+  defaultValue,
+  customProps = {},
+  rules,
+}: LoginItemProps) => {
+  const options: {
+    rules?: LoginItemProps['rules'];
+    onChange?: LoginItemProps['onChange'];
+    initialValue?: LoginItemProps['defaultValue'];
+  } = {
+    rules: rules || (customProps.rules as LoginItemProps['rules']),
   };
-
-  constructor(props: LoginItemProps) {
-    super(props);
-    this.state = {
-      count: 0,
-    };
+  if (onChange) {
+    options.onChange = onChange;
   }
-
-  componentDidMount() {
-    const { updateActive, name = '' } = this.props;
-    if (updateActive) {
-      updateActive(name);
-    }
-  }
-
-  componentWillUnmount() {
-    clearInterval(this.interval);
+  if (defaultValue) {
+    options.initialValue = defaultValue;
   }
-
-  onGetCaptcha = () => {
-    const { onGetCaptcha } = this.props;
-    const result = onGetCaptcha ? onGetCaptcha() : null;
+  return options;
+};
+
+const LoginItem: React.FC<LoginItemProps> = props => {
+  const [count, setCount] = useState<number>(props.countDown || 0);
+  const [timing, setTiming] = useState(false);
+  // 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil
+  const {
+    onChange,
+    customProps,
+    defaultValue,
+    rules,
+    name,
+    getCaptchaButtonText,
+    getCaptchaSecondText,
+    updateActive,
+    type,
+    tabUtil,
+    ...restProps
+  } = props;
+
+  const onGetCaptcha = useCallback(async (mobile: string) => {
+    const result = await getFakeCaptcha(mobile);
     if (result === false) {
       return;
     }
-    if (result instanceof Promise) {
-      result.then(this.runGetCaptchaCountDown);
-    } else {
-      this.runGetCaptchaCountDown();
-    }
-  };
-
-  getFormItemOptions = ({ onChange, defaultValue, customProps = {}, rules }: LoginItemProps) => {
-    const options: {
-      rules?: LoginItemProps['rules'];
-      onChange?: LoginItemProps['onChange'];
-      initialValue?: LoginItemProps['defaultValue'];
-    } = {
-      rules: rules || (customProps.rules as LoginItemProps['rules']),
-    };
-    if (onChange) {
-      options.onChange = onChange;
+    message.success('获取验证码成功!验证码为:1234');
+    setTiming(true);
+  }, []);
+
+  useEffect(() => {
+    let interval: number = 0;
+    const { countDown } = props;
+    if (timing) {
+      interval = window.setInterval(() => {
+        setCount(preSecond => {
+          if (preSecond <= 1) {
+            setTiming(false);
+            clearInterval(interval);
+            // 重置秒数
+            return countDown || 60;
+          }
+          return preSecond - 1;
+        });
+      }, 1000);
     }
-    if (defaultValue) {
-      options.initialValue = defaultValue;
-    }
-    return options;
-  };
-
-  runGetCaptchaCountDown = () => {
-    const { countDown } = this.props;
-    let count = countDown || 59;
-    this.setState({ count });
-    this.interval = window.setInterval(() => {
-      count -= 1;
-      this.setState({ count });
-      if (count === 0) {
-        clearInterval(this.interval);
-      }
-    }, 1000);
-  };
-
-  render() {
-    const { count } = this.state;
-
-    // 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil
-    const {
-      onChange,
-      customProps,
-      defaultValue,
-      rules,
-      name,
-      getCaptchaButtonText,
-      getCaptchaSecondText,
-      updateActive,
-      type,
-      form,
-      tabUtil,
-      ...restProps
-    } = this.props;
-    if (!name) {
-      return null;
-    }
-    if (!form) {
-      return null;
-    }
-    const { getFieldDecorator } = form;
-    // get getFieldDecorator props
-    const options = this.getFormItemOptions(this.props);
-    const otherProps = restProps || {};
+    return () => clearInterval(interval);
+  }, [timing]);
+  if (!name) {
+    return null;
+  }
+  // get getFieldDecorator props
+  const options = getFormItemOptions(props);
+  const otherProps = restProps || {};
 
-    if (type === 'Captcha') {
-      const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
+  if (type === 'Captcha') {
+    const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
 
-      return (
-        <FormItem>
+    return (
+      <FormItem shouldUpdate>
+        {({ getFieldValue }) => (
           <Row gutter={8}>
             <Col span={16}>
-              {getFieldDecorator(name, options)(<Input {...customProps} {...inputProps} />)}
+              <FormItem name={name} {...options}>
+                <Input {...customProps} {...inputProps} />
+              </FormItem>
             </Col>
             <Col span={8}>
               <Button
-                disabled={!!count}
+                disabled={timing}
                 className={styles.getCaptcha}
                 size="large"
-                onClick={this.onGetCaptcha}
+                onClick={() => {
+                  const value = getFieldValue('mobile');
+                  onGetCaptcha(value);
+                }}
               >
-                {count ? `${count} ${getCaptchaSecondText}` : getCaptchaButtonText}
+                {timing ? `${count} 秒` : '获取验证码'}
               </Button>
             </Col>
           </Row>
-        </FormItem>
-      );
-    }
-    return (
-      <FormItem>
-        {getFieldDecorator(name, options)(<Input {...customProps} {...otherProps} />)}
+        )}
       </FormItem>
     );
   }
-}
+  return (
+    <FormItem name={name} {...options}>
+      <Input {...customProps} {...otherProps} />
+    </FormItem>
+  );
+};
 
-const LoginItem: Partial<LoginItemType> = {};
+const LoginItems: Partial<LoginItemType> = {};
 
 Object.keys(ItemMap).forEach(key => {
   const item = ItemMap[key];
-  LoginItem[key] = (props: LoginItemProps) => (
+  LoginItems[key] = (props: LoginItemProps) => (
     <LoginContext.Consumer>
       {context => (
-        <WrapFormItem
+        <LoginItem
           customProps={item.props}
           rules={item.rules}
           {...props}
@@ -193,4 +166,4 @@ Object.keys(ItemMap).forEach(key => {
   );
 });
 
-export default LoginItem as LoginItemType;
+export default LoginItems as LoginItemType;

+ 11 - 20
src/pages/user/login/components/Login/LoginTab.tsx

@@ -1,5 +1,4 @@
-import React, { Component } from 'react';
-
+import React, { useEffect } from 'react';
 import { TabPaneProps } from 'antd/es/tabs';
 import { Tabs } from 'antd';
 import LoginContext, { LoginContextProps } from './LoginContext';
@@ -16,28 +15,20 @@ const generateId = (() => {
 
 interface LoginTabProps extends TabPaneProps {
   tabUtil: LoginContextProps['tabUtil'];
+  active?: boolean;
 }
 
-class LoginTab extends Component<LoginTabProps> {
-  uniqueId: string = '';
-
-  constructor(props: LoginTabProps) {
-    super(props);
-    this.uniqueId = generateId('login-tab-');
-  }
-
-  componentDidMount() {
-    const { tabUtil } = this.props;
+const LoginTab: React.FC<LoginTabProps> = props => {
+  useEffect(() => {
+    const uniqueId = generateId('login-tab-');
+    const { tabUtil } = props;
     if (tabUtil) {
-      tabUtil.addTab(this.uniqueId);
+      tabUtil.addTab(uniqueId);
     }
-  }
-
-  render() {
-    const { children } = this.props;
-    return <TabPane {...this.props}>{children}</TabPane>;
-  }
-}
+  }, []);
+  const { children } = props;
+  return <TabPane {...props}>{props.active && children}</TabPane>;
+};
 
 const WrapContext: React.FC<TabPaneProps> & {
   typeName: string;

+ 0 - 4
src/pages/user/login/components/Login/index.less

@@ -7,10 +7,6 @@
       text-align: center;
       border-bottom: 0;
     }
-
-    .ant-form-item {
-      margin: 0 2px 24px;
-    }
   }
 
   .getCaptcha {

+ 100 - 156
src/pages/user/login/components/Login/index.tsx

@@ -1,173 +1,117 @@
-import { Form, Tabs } from 'antd';
-import React, { Component } from 'react';
-import { FormComponentProps } from 'antd/es/form';
+import { Tabs, Form } from 'antd';
+import React, { useState } from 'react';
+import useMergeValue from 'use-merge-value';
 import classNames from 'classnames';
-import LoginContext, { LoginContextProps } from './LoginContext';
-import LoginItem, { LoginItemProps, LoginItemType } from './LoginItem';
+import { FormInstance } from 'antd/es/form';
+import { LoginParamsType } from '@/services/login';
 
+import LoginContext from './LoginContext';
+import LoginItem, { LoginItemProps } from './LoginItem';
 import LoginSubmit from './LoginSubmit';
 import LoginTab from './LoginTab';
 import styles from './index.less';
-import { LoginParamsType } from '@/services/login';
 
 export interface LoginProps {
-  defaultActiveKey?: string;
+  activeKey?: string;
   onTabChange?: (key: string) => void;
   style?: React.CSSProperties;
-  onSubmit?: (error: unknown, values: LoginParamsType) => void;
+  onSubmit?: (values: LoginParamsType) => void;
   className?: string;
-  form: FormComponentProps['form'];
-  onCreate?: (form?: FormComponentProps['form']) => void;
+  from?: FormInstance;
   children: React.ReactElement<typeof LoginTab>[];
 }
 
-interface LoginState {
-  tabs?: string[];
-  type?: string;
-  active?: { [key: string]: unknown[] };
+interface LoginType extends React.FC<LoginProps> {
+  Tab: typeof LoginTab;
+  Submit: typeof LoginSubmit;
+  UserName: React.FunctionComponent<LoginItemProps>;
+  Password: React.FunctionComponent<LoginItemProps>;
+  Mobile: React.FunctionComponent<LoginItemProps>;
+  Captcha: React.FunctionComponent<LoginItemProps>;
 }
 
-class Login extends Component<LoginProps, LoginState> {
-  public static Tab = LoginTab;
-
-  public static Submit = LoginSubmit;
-
-  public static UserName: React.FunctionComponent<LoginItemProps>;
-
-  public static Password: React.FunctionComponent<LoginItemProps>;
-
-  public static Mobile: React.FunctionComponent<LoginItemProps>;
-
-  public static Captcha: React.FunctionComponent<LoginItemProps>;
-
-  static defaultProps = {
-    className: '',
-    defaultActiveKey: '',
-    onTabChange: () => {},
-    onSubmit: () => {},
-  };
-
-  constructor(props: LoginProps) {
-    super(props);
-    this.state = {
-      type: props.defaultActiveKey,
-      tabs: [],
-      active: {},
-    };
-  }
-
-  componentDidMount() {
-    const { form, onCreate } = this.props;
-    if (onCreate) {
-      onCreate(form);
-    }
-  }
-
-  onSwitch = (type: string) => {
-    this.setState(
-      {
-        type,
-      },
-      () => {
-        const { onTabChange } = this.props;
-        if (onTabChange) {
-          onTabChange(type);
-        }
-      },
-    );
-  };
-
-  getContext: () => LoginContextProps = () => {
-    const { form } = this.props;
-    const { tabs = [] } = this.state;
-    return {
-      tabUtil: {
-        addTab: id => {
-          this.setState({
-            tabs: [...tabs, id],
-          });
+const Login: LoginType = props => {
+  const { className } = props;
+  const [tabs, setTabs] = useState<string[]>([]);
+  const [active, setActive] = useState();
+  const [type, setType] = useMergeValue('', {
+    value: props.activeKey,
+    onChange: props.onTabChange,
+  });
+  const TabChildren: React.ReactComponentElement<typeof LoginTab>[] = [];
+  const otherChildren: React.ReactElement<unknown>[] = [];
+  React.Children.forEach(
+    props.children,
+    (child: React.ReactComponentElement<typeof LoginTab> | React.ReactElement<unknown>) => {
+      if (!child) {
+        return;
+      }
+      if ((child.type as { typeName: string }).typeName === 'LoginTab') {
+        TabChildren.push(child as React.ReactComponentElement<typeof LoginTab>);
+      } else {
+        otherChildren.push(child);
+      }
+    },
+  );
+  return (
+    <LoginContext.Provider
+      value={{
+        tabUtil: {
+          addTab: id => {
+            setTabs([...tabs, id]);
+          },
+          removeTab: id => {
+            setTabs(tabs.filter(currentId => currentId !== id));
+          },
         },
-        removeTab: id => {
-          this.setState({
-            tabs: tabs.filter(currentId => currentId !== id),
-          });
+        updateActive: activeItem => {
+          if (active[type]) {
+            active[type].push(activeItem);
+          } else {
+            active[type] = [activeItem];
+          }
+          setActive(active);
         },
-      },
-      form: { ...form },
-      updateActive: activeItem => {
-        const { type = '', active = {} } = this.state;
-        if (active[type]) {
-          active[type].push(activeItem);
-        } else {
-          active[type] = [activeItem];
-        }
-        this.setState({
-          active,
-        });
-      },
-    };
-  };
-
-  handleSubmit = (e: React.FormEvent) => {
-    e.preventDefault();
-    const { active = {}, type = '' } = this.state;
-    const { form, onSubmit } = this.props;
-    const activeFields = active[type] || [];
-    if (form) {
-      form.validateFields(activeFields as string[], { force: true }, (err, values) => {
-        if (onSubmit) {
-          onSubmit(err, values);
-        }
-      });
-    }
-  };
-
-  render() {
-    const { className, children } = this.props;
-    const { type, tabs = [] } = this.state;
-    const TabChildren: React.ReactComponentElement<typeof LoginTab>[] = [];
-    const otherChildren: React.ReactElement<unknown>[] = [];
-    React.Children.forEach(
-      children,
-      (child: React.ReactComponentElement<typeof LoginTab> | React.ReactElement<unknown>) => {
-        if (!child) {
-          return;
-        }
-        if ((child.type as { typeName: string }).typeName === 'LoginTab') {
-          TabChildren.push(child as React.ReactComponentElement<typeof LoginTab>);
-        } else {
-          otherChildren.push(child);
-        }
-      },
-    );
-    return (
-      <LoginContext.Provider value={this.getContext()}>
-        <div className={classNames(className, styles.login)}>
-          <Form onSubmit={this.handleSubmit}>
-            {tabs.length ? (
-              <React.Fragment>
-                <Tabs
-                  animated={false}
-                  className={styles.tabs}
-                  activeKey={type}
-                  onChange={this.onSwitch}
-                >
-                  {TabChildren}
-                </Tabs>
-                {otherChildren}
-              </React.Fragment>
-            ) : (
-              children
-            )}
-          </Form>
-        </div>
-      </LoginContext.Provider>
-    );
-  }
-}
-
-(Object.keys(LoginItem) as (keyof LoginItemType)[]).forEach(item => {
-  Login[item] = LoginItem[item];
-});
-
-export default Form.create<LoginProps>()(Login);
+      }}
+    >
+      <div className={classNames(className, styles.login)}>
+        <Form
+          form={props.from}
+          onFinish={values => {
+            if (props.onSubmit) {
+              props.onSubmit(values as LoginParamsType);
+            }
+          }}
+        >
+          {tabs.length ? (
+            <React.Fragment>
+              <Tabs
+                animated={false}
+                className={styles.tabs}
+                activeKey={type}
+                onChange={activeKey => {
+                  setType(activeKey);
+                }}
+              >
+                {TabChildren}
+              </Tabs>
+              {otherChildren}
+            </React.Fragment>
+          ) : (
+            props.children
+          )}
+        </Form>
+      </div>
+    </LoginContext.Provider>
+  );
+};
+
+Login.Tab = LoginTab;
+Login.Submit = LoginSubmit;
+
+Login.UserName = LoginItem.UserName;
+Login.Password = LoginItem.Password;
+Login.Mobile = LoginItem.Mobile;
+Login.Captcha = LoginItem.Captcha;
+
+export default Login;

+ 12 - 5
src/pages/user/login/components/Login/map.tsx

@@ -1,4 +1,4 @@
-import { Icon } from 'antd';
+import { LockTwoTone, MailTwoTone, MobileTwoTone, UserOutlined } from '@ant-design/icons';
 import React from 'react';
 import styles from './index.less';
 
@@ -7,7 +7,14 @@ export default {
     props: {
       size: 'large',
       id: 'userName',
-      prefix: <Icon type="user" className={styles.prefixIcon} />,
+      prefix: (
+        <UserOutlined
+          style={{
+            color: '#1890ff',
+          }}
+          className={styles.prefixIcon}
+        />
+      ),
       placeholder: 'admin',
     },
     rules: [
@@ -20,7 +27,7 @@ export default {
   Password: {
     props: {
       size: 'large',
-      prefix: <Icon type="lock" className={styles.prefixIcon} />,
+      prefix: <LockTwoTone className={styles.prefixIcon} />,
       type: 'password',
       id: 'password',
       placeholder: '888888',
@@ -35,7 +42,7 @@ export default {
   Mobile: {
     props: {
       size: 'large',
-      prefix: <Icon type="mobile" className={styles.prefixIcon} />,
+      prefix: <MobileTwoTone className={styles.prefixIcon} />,
       placeholder: 'mobile number',
     },
     rules: [
@@ -52,7 +59,7 @@ export default {
   Captcha: {
     props: {
       size: 'large',
-      prefix: <Icon type="mail" className={styles.prefixIcon} />,
+      prefix: <MailTwoTone className={styles.prefixIcon} />,
       placeholder: 'captcha',
     },
     rules: [

+ 113 - 179
src/pages/user/login/index.tsx

@@ -1,202 +1,136 @@
-import { Alert, Checkbox, Icon } from 'antd';
-import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
-import React, { Component } from 'react';
-
-import { CheckboxChangeEvent } from 'antd/es/checkbox';
+import { AlipayCircleOutlined, TaobaoCircleOutlined, WeiboCircleOutlined } from '@ant-design/icons';
+import { Alert, Checkbox } from 'antd';
+import React, { useState } from 'react';
 import { Dispatch, AnyAction } from 'redux';
-import { FormComponentProps } from 'antd/es/form';
 import { Link } from 'umi';
 import { connect } from 'dva';
 import { StateType } from '@/models/login';
-import LoginComponents from './components/Login';
-import styles from './style.less';
 import { LoginParamsType } from '@/services/login';
 import { ConnectState } from '@/models/connect';
+import LoginFrom from './components/Login';
 
-const { Tab, UserName, Password, Mobile, Captcha, Submit } = LoginComponents;
+import styles from './style.less';
 
+const { Tab, UserName, Password, Mobile, Captcha, Submit } = LoginFrom;
 interface LoginProps {
   dispatch: Dispatch<AnyAction>;
   userLogin: StateType;
   submitting?: boolean;
 }
-interface LoginState {
-  type: string;
-  autoLogin: boolean;
-}
 
-class Login extends Component<LoginProps, LoginState> {
-  loginForm: FormComponentProps['form'] | undefined | null = undefined;
+const LoginMessage: React.FC<{
+  content: string;
+}> = ({ content }) => (
+  <Alert
+    style={{
+      marginBottom: 24,
+    }}
+    message={content}
+    type="error"
+    showIcon
+  />
+);
 
-  state: LoginState = {
-    type: 'account',
-    autoLogin: true,
-  };
+const Login: React.FC<LoginProps> = props => {
+  const { userLogin = {}, submitting } = props;
+  const { status, type: loginType } = userLogin;
+  const [autoLogin, setAutoLogin] = useState(true);
+  const [type, setType] = useState<string>('account');
 
-  changeAutoLogin = (e: CheckboxChangeEvent) => {
-    this.setState({
-      autoLogin: e.target.checked,
+  const handleSubmit = (values: LoginParamsType) => {
+    const { dispatch } = props;
+    dispatch({
+      type: 'login/login',
+      payload: { ...values, type },
     });
   };
+  return (
+    <div className={styles.main}>
+      <LoginFrom activeKey={type} onTabChange={setType} onSubmit={handleSubmit}>
+        <Tab key="account" tab="账户密码登录">
+          {status === 'error' && loginType === 'account' && !submitting && (
+            <LoginMessage content="账户或密码错误(admin/ant.design)" />
+          )}
 
-  handleSubmit = (err: unknown, values: LoginParamsType) => {
-    const { type } = this.state;
-    if (!err) {
-      const { dispatch } = this.props;
-      dispatch({
-        type: 'login/login',
-        payload: {
-          ...values,
-          type,
-        },
-      });
-    }
-  };
-
-  onTabChange = (type: string) => {
-    this.setState({ type });
-  };
-
-  onGetCaptcha = () =>
-    new Promise<boolean>((resolve, reject) => {
-      if (!this.loginForm) {
-        return;
-      }
-      this.loginForm.validateFields(
-        ['mobile'],
-        {},
-        async (err: unknown, values: LoginParamsType) => {
-          if (err) {
-            reject(err);
-          } else {
-            const { dispatch } = this.props;
-            try {
-              const success = await ((dispatch({
-                type: 'login/getCaptcha',
-                payload: values.mobile,
-              }) as unknown) as Promise<unknown>);
-              resolve(!!success);
-            } catch (error) {
-              reject(error);
-            }
-          }
-        },
-      );
-    });
-
-  renderMessage = (content: string) => (
-    <Alert style={{ marginBottom: 24 }} message={content} type="error" showIcon />
+          <UserName
+            name="userName"
+            placeholder="用户名: admin or user"
+            rules={[
+              {
+                required: true,
+                message: '请输入用户名!',
+              },
+            ]}
+          />
+          <Password
+            name="password"
+            placeholder="密码: ant.design"
+            rules={[
+              {
+                required: true,
+                message: '请输入密码!',
+              },
+            ]}
+          />
+        </Tab>
+        <Tab key="mobile" tab="手机号登录">
+          {status === 'error' && loginType === 'mobile' && !submitting && (
+            <LoginMessage content="验证码错误" />
+          )}
+          <Mobile
+            name="mobile"
+            placeholder="手机号"
+            rules={[
+              {
+                required: true,
+                message: '请输入手机号!',
+              },
+              {
+                pattern: /^1\d{10}$/,
+                message: '手机号格式错误!',
+              },
+            ]}
+          />
+          <Captcha
+            name="captcha"
+            placeholder="验证码"
+            countDown={120}
+            getCaptchaButtonText=""
+            getCaptchaSecondText="秒"
+            rules={[
+              {
+                required: true,
+                message: '请输入验证码!',
+              },
+            ]}
+          />
+        </Tab>
+        <div>
+          <Checkbox checked={autoLogin} onChange={e => setAutoLogin(e.target.checked)}>
+            自动登录
+          </Checkbox>
+          <a
+            style={{
+              float: 'right',
+            }}
+          >
+            忘记密码
+          </a>
+        </div>
+        <Submit loading={submitting}>登录</Submit>
+        <div className={styles.other}>
+          其他登录方式
+          <AlipayCircleOutlined className={styles.icon} />
+          <TaobaoCircleOutlined className={styles.icon} />
+          <WeiboCircleOutlined className={styles.icon} />
+          <Link className={styles.register} to="/user/register">
+            注册账户
+          </Link>
+        </div>
+      </LoginFrom>
+    </div>
   );
-
-  render() {
-    const { userLogin = {}, submitting } = this.props;
-    const { status, type: loginType } = userLogin;
-    const { type, autoLogin } = this.state;
-    return (
-      <div className={styles.main}>
-        <LoginComponents
-          defaultActiveKey={type}
-          onTabChange={this.onTabChange}
-          onSubmit={this.handleSubmit}
-          onCreate={(form?: FormComponentProps['form']) => {
-            this.loginForm = form;
-          }}
-        >
-          <Tab key="account" tab={formatMessage({ id: 'user-login.login.tab-login-credentials' })}>
-            {status === 'error' &&
-              loginType === 'account' &&
-              !submitting &&
-              this.renderMessage(
-                formatMessage({ id: 'user-login.login.message-invalid-credentials' }),
-              )}
-            <UserName
-              name="userName"
-              placeholder={`${formatMessage({ id: 'user-login.login.userName' })}: admin or user`}
-              rules={[
-                {
-                  required: true,
-                  message: formatMessage({ id: 'user-login.userName.required' }),
-                },
-              ]}
-            />
-            <Password
-              name="password"
-              placeholder={`${formatMessage({ id: 'user-login.login.password' })}: ant.design`}
-              rules={[
-                {
-                  required: true,
-                  message: formatMessage({ id: 'user-login.password.required' }),
-                },
-              ]}
-              onPressEnter={e => {
-                e.preventDefault();
-                if (this.loginForm) {
-                  this.loginForm.validateFields(this.handleSubmit);
-                }
-              }}
-            />
-          </Tab>
-          <Tab key="mobile" tab={formatMessage({ id: 'user-login.login.tab-login-mobile' })}>
-            {status === 'error' &&
-              loginType === 'mobile' &&
-              !submitting &&
-              this.renderMessage(
-                formatMessage({ id: 'user-login.login.message-invalid-verification-code' }),
-              )}
-            <Mobile
-              name="mobile"
-              placeholder={formatMessage({ id: 'user-login.phone-number.placeholder' })}
-              rules={[
-                {
-                  required: true,
-                  message: formatMessage({ id: 'user-login.phone-number.required' }),
-                },
-                {
-                  pattern: /^1\d{10}$/,
-                  message: formatMessage({ id: 'user-login.phone-number.wrong-format' }),
-                },
-              ]}
-            />
-            <Captcha
-              name="captcha"
-              placeholder={formatMessage({ id: 'user-login.verification-code.placeholder' })}
-              countDown={120}
-              onGetCaptcha={this.onGetCaptcha}
-              getCaptchaButtonText={formatMessage({ id: 'user-login.form.get-captcha' })}
-              getCaptchaSecondText={formatMessage({ id: 'user-login.captcha.second' })}
-              rules={[
-                {
-                  required: true,
-                  message: formatMessage({ id: 'user-login.verification-code.required' }),
-                },
-              ]}
-            />
-          </Tab>
-          <div>
-            <Checkbox checked={autoLogin} onChange={this.changeAutoLogin}>
-              <FormattedMessage id="user-login.login.remember-me" />
-            </Checkbox>
-            <a style={{ float: 'right' }} href="">
-              <FormattedMessage id="user-login.login.forgot-password" />
-            </a>
-          </div>
-          <Submit loading={submitting}>
-            <FormattedMessage id="user-login.login.login" />
-          </Submit>
-          <div className={styles.other}>
-            <FormattedMessage id="user-login.login.sign-in-with" />
-            <Icon type="alipay-circle" className={styles.icon} theme="outlined" />
-            <Icon type="taobao-circle" className={styles.icon} theme="outlined" />
-            <Icon type="weibo-circle" className={styles.icon} theme="outlined" />
-            <Link className={styles.register} to="/user/register">
-              <FormattedMessage id="user-login.login.signup" />
-            </Link>
-          </div>
-        </LoginComponents>
-      </div>
-    );
-  }
-}
+};
 
 export default connect(({ login, loading }: ConnectState) => ({
   userLogin: login,

+ 0 - 78
src/pages/user/login/locales/en-US.ts

@@ -1,78 +0,0 @@
-export default {
-  'user-login.login.userName': 'userName',
-  'user-login.login.password': 'password',
-  'user-login.login.message-invalid-credentials':
-    'Invalid username or password(admin/ant.design)',
-  'user-login.login.message-invalid-verification-code': 'Invalid verification code',
-  'user-login.login.tab-login-credentials': 'Credentials',
-  'user-login.login.tab-login-mobile': 'Mobile number',
-  'user-login.login.remember-me': 'Remember me',
-  'user-login.login.forgot-password': 'Forgot your password?',
-  'user-login.login.sign-in-with': 'Sign in with',
-  'user-login.login.signup': 'Sign up',
-  'user-login.login.login': 'Login',
-  'user-login.register.register': 'Register',
-  'user-login.register.get-verification-code': 'Get code',
-  'user-login.register.sign-in': 'Already have an account?',
-  'user-login.register-result.msg': 'Account:registered at {email}',
-  'user-login.register-result.activation-email':
-    'The activation email has been sent to your email address and is valid for 24 hours. Please log in to the email in time and click on the link in the email to activate the account.',
-  'user-login.register-result.back-home': 'Back to home',
-  'user-login.register-result.view-mailbox': 'View mailbox',
-  'user-login.email.required': 'Please enter your email!',
-  'user-login.email.wrong-format': 'The email address is in the wrong format!',
-  'user-login.userName.required': 'Please enter your userName!',
-  'user-login.password.required': 'Please enter your password!',
-  'user-login.password.twice': 'The passwords entered twice do not match!',
-  'user-login.strength.msg':
-    "Please enter at least 6 characters and don't use passwords that are easy to guess.",
-  'user-login.strength.strong': 'Strength: strong',
-  'user-login.strength.medium': 'Strength: medium',
-  'user-login.strength.short': 'Strength: too short',
-  'user-login.confirm-password.required': 'Please confirm your password!',
-  'user-login.phone-number.required': 'Please enter your phone number!',
-  'user-login.phone-number.wrong-format': 'Malformed phone number!',
-  'user-login.verification-code.required': 'Please enter the verification code!',
-  'user-login.title.required': 'Please enter a title',
-  'user-login.date.required': 'Please select the start and end date',
-  'user-login.goal.required': 'Please enter a description of the goal',
-  'user-login.standard.required': 'Please enter a metric',
-  'user-login.form.get-captcha': 'Get Captcha',
-  'user-login.captcha.second': 'sec',
-  'user-login.form.optional': ' (optional) ',
-  'user-login.form.submit': 'Submit',
-  'user-login.form.save': 'Save',
-  'user-login.email.placeholder': 'Email',
-  'user-login.password.placeholder': 'Password',
-  'user-login.confirm-password.placeholder': 'Confirm password',
-  'user-login.phone-number.placeholder': 'Phone number',
-  'user-login.verification-code.placeholder': 'Verification code',
-  'user-login.title.label': 'Title',
-  'user-login.title.placeholder': 'Give the target a name',
-  'user-login.date.label': 'Start and end date',
-  'user-login.placeholder.start': 'Start date',
-  'user-login.placeholder.end': 'End date',
-  'user-login.goal.label': 'Goal description',
-  'user-login.goal.placeholder': 'Please enter your work goals',
-  'user-login.standard.label': 'Metrics',
-  'user-login.standard.placeholder': 'Please enter a metric',
-  'user-login.client.label': 'Client',
-  'user-login.label.tooltip': 'Target service object',
-  'user-login.client.placeholder':
-    'Please describe your customer service, internal customers directly @ Name / job number',
-  'user-login.invites.label': 'Inviting critics',
-  'user-login.invites.placeholder':
-    'Please direct @ Name / job number, you can invite up to 5 people',
-  'user-login.weight.label': 'Weight',
-  'user-login.weight.placeholder': 'Please enter weight',
-  'user-login.public.label': 'Target disclosure',
-  'user-login.label.help': 'Customers and invitees are shared by default',
-  'user-login.radio.public': 'Public',
-  'user-login.radio.partially-public': 'Partially public',
-  'user-login.radio.private': 'Private',
-  'user-login.publicUsers.placeholder': 'Open to',
-  'user-login.option.A': 'Colleague A',
-  'user-login.option.B': 'Colleague B',
-  'user-login.option.C': 'Colleague C',
-  'user-login.navBar.lang': 'Languages',
-};

+ 0 - 74
src/pages/user/login/locales/zh-CN.ts

@@ -1,74 +0,0 @@
-export default {
-  'user-login.login.userName': '用户名',
-  'user-login.login.password': '密码',
-  'user-login.login.message-invalid-credentials': '账户或密码错误(admin/ant.design)',
-  'user-login.login.message-invalid-verification-code': '验证码错误',
-  'user-login.login.tab-login-credentials': '账户密码登录',
-  'user-login.login.tab-login-mobile': '手机号登录',
-  'user-login.login.remember-me': '自动登录',
-  'user-login.login.forgot-password': '忘记密码',
-  'user-login.login.sign-in-with': '其他登录方式',
-  'user-login.login.signup': '注册账户',
-  'user-login.login.login': '登录',
-  'user-login.register.register': '注册',
-  'user-login.register.get-verification-code': '获取验证码',
-  'user-login.register.sign-in': '使用已有账户登录',
-  'user-login.register-result.msg': '你的账户:{email} 注册成功',
-  'user-login.register-result.activation-email':
-    '激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。',
-  'user-login.register-result.back-home': '返回首页',
-  'user-login.register-result.view-mailbox': '查看邮箱',
-  'user-login.email.required': '请输入邮箱地址!',
-  'user-login.email.wrong-format': '邮箱地址格式错误!',
-  'user-login.userName.required': '请输入用户名!',
-  'user-login.password.required': '请输入密码!',
-  'user-login.password.twice': '两次输入的密码不匹配!',
-  'user-login.strength.msg': '请至少输入 6 个字符。请不要使用容易被猜到的密码。',
-  'user-login.strength.strong': '强度:强',
-  'user-login.strength.medium': '强度:中',
-  'user-login.strength.short': '强度:太短',
-  'user-login.confirm-password.required': '请确认密码!',
-  'user-login.phone-number.required': '请输入手机号!',
-  'user-login.phone-number.wrong-format': '手机号格式错误!',
-  'user-login.verification-code.required': '请输入验证码!',
-  'user-login.title.required': '请输入标题',
-  'user-login.date.required': '请选择起止日期',
-  'user-login.goal.required': '请输入目标描述',
-  'user-login.standard.required': '请输入衡量标准',
-  'user-login.form.get-captcha': '获取验证码',
-  'user-login.captcha.second': '秒',
-  'user-login.form.optional': '(选填)',
-  'user-login.form.submit': '提交',
-  'user-login.form.save': '保存',
-  'user-login.email.placeholder': '邮箱',
-  'user-login.password.placeholder': '至少6位密码,区分大小写',
-  'user-login.confirm-password.placeholder': '确认密码',
-  'user-login.phone-number.placeholder': '手机号',
-  'user-login.verification-code.placeholder': '验证码',
-  'user-login.title.label': '标题',
-  'user-login.title.placeholder': '给目标起个名字',
-  'user-login.date.label': '起止日期',
-  'user-login.placeholder.start': '开始日期',
-  'user-login.placeholder.end': '结束日期',
-  'user-login.goal.label': '目标描述',
-  'user-login.goal.placeholder': '请输入你的阶段性工作目标',
-  'user-login.standard.label': '衡量标准',
-  'user-login.standard.placeholder': '请输入衡量标准',
-  'user-login.client.label': '客户',
-  'user-login.label.tooltip': '目标的服务对象',
-  'user-login.client.placeholder': '请描述你服务的客户,内部客户直接 @姓名/工号',
-  'user-login.invites.label': '邀评人',
-  'user-login.invites.placeholder': '请直接 @姓名/工号,最多可邀请 5 人',
-  'user-login.weight.label': '权重',
-  'user-login.weight.placeholder': '请输入',
-  'user-login.public.label': '目标公开',
-  'user-login.label.help': '客户、邀评人默认被分享',
-  'user-login.radio.public': '公开',
-  'user-login.radio.partially-public': '部分公开',
-  'user-login.radio.private': '不公开',
-  'user-login.publicUsers.placeholder': '公开给',
-  'user-login.option.A': '同事甲',
-  'user-login.option.B': '同事乙',
-  'user-login.option.C': '同事丙',
-  'user-login.navBar.lang': '语言',
-};

+ 0 - 74
src/pages/user/login/locales/zh-TW.ts

@@ -1,74 +0,0 @@
-export default {
-  'user-login.login.userName': '賬戶',
-  'user-login.login.password': '密碼',
-  'user-login.login.message-invalid-credentials': '賬戶或密碼錯誤(admin/ant.design)',
-  'user-login.login.message-invalid-verification-code': '驗證碼錯誤',
-  'user-login.login.tab-login-credentials': '賬戶密碼登錄',
-  'user-login.login.tab-login-mobile': '手機號登錄',
-  'user-login.login.remember-me': '自動登錄',
-  'user-login.login.forgot-password': '忘記密碼',
-  'user-login.login.sign-in-with': '其他登錄方式',
-  'user-login.login.signup': '註冊賬戶',
-  'user-login.login.login': '登錄',
-  'user-login.register.register': '註冊',
-  'user-login.register.get-verification-code': '獲取驗證碼',
-  'user-login.register.sign-in': '使用已有賬戶登錄',
-  'user-login.register-result.msg': '妳的賬戶:{email} 註冊成功',
-  'user-login.register-result.activation-email':
-    '激活郵件已發送到妳的郵箱中,郵件有效期為24小時。請及時登錄郵箱,點擊郵件中的鏈接激活帳戶。',
-  'user-login.register-result.back-home': '返回首頁',
-  'user-login.register-result.view-mailbox': '查看郵箱',
-  'user-login.email.required': '請輸入郵箱地址!',
-  'user-login.email.wrong-format': '郵箱地址格式錯誤!',
-  'user-login.userName.required': '請輸入賬戶!',
-  'user-login.password.required': '請輸入密碼!',
-  'user-login.password.twice': '兩次輸入的密碼不匹配!',
-  'user-login.strength.msg': '請至少輸入 6 個字符。請不要使用容易被猜到的密碼。',
-  'user-login.strength.strong': '強度:強',
-  'user-login.strength.medium': '強度:中',
-  'user-login.strength.short': '強度:太短',
-  'user-login.confirm-password.required': '請確認密碼!',
-  'user-login.phone-number.required': '請輸入手機號!',
-  'user-login.phone-number.wrong-format': '手機號格式錯誤!',
-  'user-login.verification-code.required': '請輸入驗證碼!',
-  'user-login.title.required': '請輸入標題',
-  'user-login.date.required': '請選擇起止日期',
-  'user-login.goal.required': '請輸入目標描述',
-  'user-login.standard.required': '請輸入衡量標淮',
-  'user-login.form.get-captcha': '獲取驗證碼',
-  'user-login.captcha.second': '秒',
-  'user-login.form.optional': '(選填)',
-  'user-login.form.submit': '提交',
-  'user-login.form.save': '保存',
-  'user-login.email.placeholder': '郵箱',
-  'user-login.password.placeholder': '至少6位密碼,區分大小寫',
-  'user-login.confirm-password.placeholder': '確認密碼',
-  'user-login.phone-number.placeholder': '手機號',
-  'user-login.verification-code.placeholder': '驗證碼',
-  'user-login.title.label': '標題',
-  'user-login.title.placeholder': '給目標起個名字',
-  'user-login.date.label': '起止日期',
-  'user-login.placeholder.start': '開始日期',
-  'user-login.placeholder.end': '結束日期',
-  'user-login.goal.label': '目標描述',
-  'user-login.goal.placeholder': '請輸入妳的階段性工作目標',
-  'user-login.standard.label': '衡量標淮',
-  'user-login.standard.placeholder': '請輸入衡量標淮',
-  'user-login.client.label': '客戶',
-  'user-login.label.tooltip': '目標的服務對象',
-  'user-login.client.placeholder': '請描述妳服務的客戶,內部客戶直接 @姓名/工號',
-  'user-login.invites.label': '邀評人',
-  'user-login.invites.placeholder': '請直接 @姓名/工號,最多可邀請 5 人',
-  'user-login.weight.label': '權重',
-  'user-login.weight.placeholder': '請輸入',
-  'user-login.public.label': '目標公開',
-  'user-login.label.help': '客戶、邀評人默認被分享',
-  'user-login.radio.public': '公開',
-  'user-login.radio.partially-public': '部分公開',
-  'user-login.radio.private': '不公開',
-  'user-login.publicUsers.placeholder': '公開給',
-  'user-login.option.A': '同事甲',
-  'user-login.option.B': '同事乙',
-  'user-login.option.C': '同事丙',
-  'user-login.navBar.lang': '語言',
-};