ddcat1115 7 лет назад
Родитель
Сommit
92293e4445

+ 4 - 0
appveyor.yml

@@ -2,6 +2,10 @@
 environment:
   nodejs_version: "8"
 
+# this is how to allow failing jobs in the matrix
+matrix:
+  fast_finish: true     # set this flag to immediately finish build once one of the jobs fails.
+
 # Install scripts. (runs after repo cloning)
 install:
   # Get the latest stable version of Node.js or io.js

+ 1 - 1
package.json

@@ -45,6 +45,7 @@
     "react-document-title": "^2.0.3",
     "react-dom": "^16.2.0",
     "react-fittext": "^1.0.0",
+    "rollbar": "^2.3.4",
     "url-polyfill": "^1.0.10"
   },
   "devDependencies": {
@@ -72,7 +73,6 @@
     "regenerator-runtime": "^0.11.1",
     "roadhog": "^2.1.0",
     "roadhog-api-doc": "^0.3.4",
-    "rollbar": "^2.3.4",
     "stylelint": "^8.4.0",
     "stylelint-config-standard": "^18.0.0"
   },

+ 1 - 1
src/common/menu.js

@@ -115,7 +115,7 @@ const menuData = [{
   }],
 }];
 
-function formatter(data, parentPath = '', parentAuthority) {
+function formatter(data, parentPath = '/', parentAuthority) {
   return data.map((item) => {
     let { path } = item;
     if (!isUrl(path)) {

+ 4 - 1
src/common/router.js

@@ -92,12 +92,15 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm')),
     },
     '/form/step-form/info': {
+      name: '分步表单(填写转账信息)',
       component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step1')),
     },
     '/form/step-form/confirm': {
+      name: '分步表单(确认转账信息)',
       component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step2')),
     },
     '/form/step-form/result': {
+      name: '分步表单(完成)',
       component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step3')),
     },
     '/form/advanced-form': {
@@ -193,7 +196,7 @@ export const getRouterData = (app) => {
     // Regular match item name
     // eg.  router /user/:id === /user/chen
     const pathRegexp = pathToRegexp(path);
-    const menuKey = Object.keys(menuData).find(key => pathRegexp.test(`/${key}`));
+    const menuKey = Object.keys(menuData).find(key => pathRegexp.test(`${key}`));
     let menuItem = {};
     // If menuKey is not empty
     if (menuKey) {

+ 11 - 3
src/components/Authorized/PromiseRender.js

@@ -6,9 +6,17 @@ export default class PromiseRender extends React.PureComponent {
     component: null,
   };
   componentDidMount() {
-    const ok = this.checkIsInstantiation(this.props.ok);
-    const error = this.checkIsInstantiation(this.props.error);
-    this.props.promise
+    this.setRenderComponent(this.props);
+  }
+  componentWillReceiveProps(nextProps) {
+    // new Props enter
+    this.setRenderComponent(nextProps);
+  }
+  // set render Component : ok or error
+  setRenderComponent(props) {
+    const ok = this.checkIsInstantiation(props.ok);
+    const error = this.checkIsInstantiation(props.error);
+    props.promise
       .then(() => {
         this.setState({
           component: ok,

+ 6 - 8
src/components/CountDown/index.js

@@ -51,9 +51,8 @@ class CountDown extends Component {
     }
 
     lastTime = targetTime - new Date().getTime();
-
     return {
-      lastTime,
+      lastTime: lastTime < 0 ? 0 : lastTime,
     };
   }
   // defaultFormat = time => (
@@ -63,11 +62,11 @@ class CountDown extends Component {
     const hours = 60 * 60 * 1000;
     const minutes = 60 * 1000;
 
-    const h = fixedZero(Math.floor(time / hours));
-    const m = fixedZero(Math.floor((time - (h * hours)) / minutes));
-    const s = fixedZero(Math.floor((time - (h * hours) - (m * minutes)) / 1000));
+    const h = Math.floor(time / hours);
+    const m = Math.floor((time - (h * hours)) / minutes);
+    const s = Math.floor((time - (h * hours) - (m * minutes)) / 1000);
     return (
-      <span>{h}:{m}:{s}</span>
+      <span>{fixedZero(h)}:{fixedZero(m)}:{fixedZero(s)}</span>
     );
   }
   tick = () => {
@@ -96,9 +95,8 @@ class CountDown extends Component {
   }
 
   render() {
-    const { format = this.defaultFormat, ...rest } = this.props;
+    const { format = this.defaultFormat, onEnd, ...rest } = this.props;
     const { lastTime } = this.state;
-
     const result = format(lastTime);
 
     return (<span {...rest}>{result}</span>);

+ 67 - 50
src/components/PageHeader/index.js

@@ -4,7 +4,7 @@ import pathToRegexp from 'path-to-regexp';
 import { Breadcrumb, Tabs } from 'antd';
 import classNames from 'classnames';
 import styles from './index.less';
-
+import { urlToList } from '../utils/pathTools';
 
 const { TabPane } = Tabs;
 export function getBreadcrumb(breadcrumbNameMap, url) {
@@ -19,14 +19,6 @@ export function getBreadcrumb(breadcrumbNameMap, url) {
   return breadcrumb || {};
 }
 
-// /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
-export function urlToList(url) {
-  const urllist = url.split('/').filter(i => i);
-  return urllist.map((urlItem, index) => {
-    return `/${urllist.slice(0, index + 1).join('/')}`;
-  });
-}
-
 export default class PageHeader extends PureComponent {
   static contextTypes = {
     routes: PropTypes.array,
@@ -44,29 +36,35 @@ export default class PageHeader extends PureComponent {
       routes: this.props.routes || this.context.routes,
       params: this.props.params || this.context.params,
       routerLocation: this.props.location || this.context.location,
-      breadcrumbNameMap: this.props.breadcrumbNameMap || this.context.breadcrumbNameMap,
+      breadcrumbNameMap:
+        this.props.breadcrumbNameMap || this.context.breadcrumbNameMap,
     };
   };
   // Generated according to props
-  conversionFromProps= () => {
+  conversionFromProps = () => {
     const {
-      breadcrumbList, breadcrumbSeparator, linkElement = 'a',
+      breadcrumbList,
+      breadcrumbSeparator,
+      linkElement = 'a',
     } = this.props;
     return (
-      <Breadcrumb
-        className={styles.breadcrumb}
-        separator={breadcrumbSeparator}
-      >
+      <Breadcrumb className={styles.breadcrumb} separator={breadcrumbSeparator}>
         {breadcrumbList.map(item => (
           <Breadcrumb.Item key={item.title}>
-            {item.href ? (createElement(linkElement, {
-          [linkElement === 'a' ? 'href' : 'to']: item.href,
-        }, item.title)) : item.title}
+            {item.href
+              ? createElement(
+                  linkElement,
+                  {
+                    [linkElement === 'a' ? 'href' : 'to']: item.href,
+                  },
+                  item.title,
+                )
+              : item.title}
           </Breadcrumb.Item>
-      ))}
+        ))}
       </Breadcrumb>
     );
-  }
+  };
   conversionFromLocation = (routerLocation, breadcrumbNameMap) => {
     const { breadcrumbSeparator, linkElement = 'a' } = this.props;
     // Convert the url to an array
@@ -74,7 +72,8 @@ export default class PageHeader extends PureComponent {
     // Loop data mosaic routing
     const extraBreadcrumbItems = pathSnippets.map((url, index) => {
       const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url);
-      const isLinkable = (index !== pathSnippets.length - 1) && currentBreadcrumb.component;
+      const isLinkable =
+        index !== pathSnippets.length - 1 && currentBreadcrumb.component;
       return currentBreadcrumb.name && !currentBreadcrumb.hideInBreadcrumb ? (
         <Breadcrumb.Item key={url}>
           {createElement(
@@ -88,26 +87,33 @@ export default class PageHeader extends PureComponent {
     // Add home breadcrumbs to your head
     extraBreadcrumbItems.unshift(
       <Breadcrumb.Item key="home">
-        {createElement(linkElement, {
-        [linkElement === 'a' ? 'href' : 'to']: '/' }, '首页')}
-      </Breadcrumb.Item>
+        {createElement(
+          linkElement,
+          {
+            [linkElement === 'a' ? 'href' : 'to']: '/',
+          },
+          '首页',
+        )}
+      </Breadcrumb.Item>,
     );
     return (
-      <Breadcrumb
-        className={styles.breadcrumb}
-        separator={breadcrumbSeparator}
-      >
+      <Breadcrumb className={styles.breadcrumb} separator={breadcrumbSeparator}>
         {extraBreadcrumbItems}
       </Breadcrumb>
     );
-  }
+  };
   /**
    * 将参数转化为面包屑
    * Convert parameters into breadcrumbs
    */
   conversionBreadcrumbList = () => {
     const { breadcrumbList, breadcrumbSeparator } = this.props;
-    const { routes, params, routerLocation, breadcrumbNameMap } = this.getBreadcrumbProps();
+    const {
+      routes,
+      params,
+      routerLocation,
+      breadcrumbNameMap,
+    } = this.getBreadcrumbProps();
     if (breadcrumbList && breadcrumbList.length) {
       return this.conversionFromProps();
     }
@@ -126,28 +132,41 @@ export default class PageHeader extends PureComponent {
     }
     // 根据 location 生成 面包屑
     // Generate breadcrumbs based on location
-    if (location && location.pathname) {
+    if (routerLocation && routerLocation.pathname) {
       return this.conversionFromLocation(routerLocation, breadcrumbNameMap);
     }
     return null;
-  }
+  };
   // 渲染Breadcrumb 子节点
   // Render the Breadcrumb child node
   itemRender = (route, params, routes, paths) => {
     const { linkElement = 'a' } = this.props;
     const last = routes.indexOf(route) === routes.length - 1;
-    return (last || !route.component)
-      ? <span>{route.breadcrumbName}</span>
-      : createElement(linkElement, {
-        href: paths.join('/') || '/',
-        to: paths.join('/') || '/',
-      }, route.breadcrumbName);
-  }
+    return last || !route.component ? (
+      <span>{route.breadcrumbName}</span>
+    ) : (
+      createElement(
+        linkElement,
+        {
+          href: paths.join('/') || '/',
+          to: paths.join('/') || '/',
+        },
+        route.breadcrumbName,
+      )
+    );
+  };
 
   render() {
     const {
-      title, logo, action, content, extraContent,
-      tabList, className, tabActiveKey, tabBarExtraContent,
+      title,
+      logo,
+      action,
+      content,
+      extraContent,
+      tabList,
+      className,
+      tabActiveKey,
+      tabBarExtraContent,
     } = this.props;
     const clsString = classNames(styles.pageHeader, className);
 
@@ -175,12 +194,13 @@ export default class PageHeader extends PureComponent {
             </div>
             <div className={styles.row}>
               {content && <div className={styles.content}>{content}</div>}
-              {extraContent && <div className={styles.extraContent}>{extraContent}</div>}
+              {extraContent && (
+                <div className={styles.extraContent}>{extraContent}</div>
+              )}
             </div>
           </div>
         </div>
-        {
-          tabList &&
+        {tabList &&
           tabList.length && (
             <Tabs
               className={styles.tabs}
@@ -188,12 +208,9 @@ export default class PageHeader extends PureComponent {
               onChange={this.onChange}
               tabBarExtraContent={tabBarExtraContent}
             >
-              {
-                tabList.map(item => <TabPane tab={item.tab} key={item.key} />)
-              }
+              {tabList.map(item => <TabPane tab={item.tab} key={item.key} />)}
             </Tabs>
-          )
-        }
+          )}
       </div>
     );
   }

+ 5 - 23
src/components/PageHeader/index.test.js

@@ -1,23 +1,5 @@
-import { getBreadcrumb, urlToList } from './index';
-
-describe('test urlToList', () => {
-  it('A path', () => {
-    expect(urlToList('/userinfo')).toEqual(['/userinfo']);
-  });
-  it('Secondary path', () => {
-    expect(urlToList('/userinfo/2144')).toEqual([
-      '/userinfo',
-      '/userinfo/2144',
-    ]);
-  });
-  it('Three paths', () => {
-    expect(urlToList('/userinfo/2144/addr')).toEqual([
-      '/userinfo',
-      '/userinfo/2144',
-      '/userinfo/2144/addr',
-    ]);
-  });
-});
+import { getBreadcrumb } from './index';
+import { urlToList } from '../utils/pathTools';
 
 const routerData = {
   '/dashboard/analysis': {
@@ -36,17 +18,17 @@ const routerData = {
 describe('test getBreadcrumb', () => {
   it('Simple url', () => {
     expect(getBreadcrumb(routerData, '/dashboard/analysis').name).toEqual(
-      '分析页'
+      '分析页',
     );
   });
   it('Parameters url', () => {
     expect(getBreadcrumb(routerData, '/userinfo/2144').name).toEqual(
-      '用户信息'
+      '用户信息',
     );
   });
   it('The middle parameter url', () => {
     expect(getBreadcrumb(routerData, '/userinfo/2144/addr').name).toEqual(
-      '收货订单'
+      '收货订单',
     );
   });
   it('Loop through the parameters', () => {

+ 68 - 72
src/components/SiderMenu/SiderMenu.js

@@ -3,6 +3,7 @@ import { Layout, Menu, Icon } from 'antd';
 import pathToRegexp from 'path-to-regexp';
 import { Link } from 'dva/router';
 import styles from './index.less';
+import { urlToList } from '../utils/pathTools';
 
 const { Sider } = Layout;
 const { SubMenu } = Menu;
@@ -21,10 +22,17 @@ const getIcon = (icon) => {
   return icon;
 };
 
+export const getMeunMatcheys = (flatMenuKeys, path) => {
+  return flatMenuKeys.filter((item) => {
+    return pathToRegexp(item).test(path);
+  });
+};
+
 export default class SiderMenu extends PureComponent {
   constructor(props) {
     super(props);
     this.menus = props.menuData;
+    this.flatMenuKeys = this.getFlatMenuKeys(props.menuData);
     this.state = {
       openKeys: this.getDefaultCollapsedSubMenus(props),
     };
@@ -43,30 +51,11 @@ export default class SiderMenu extends PureComponent {
    */
   getDefaultCollapsedSubMenus(props) {
     const { location: { pathname } } = props || this.props;
-    // eg. /list/search/articles = > ['','list','search','articles']
-    let snippets = pathname.split('/');
-    // Delete the end
-    // eg.  delete 'articles'
-    snippets.pop();
-    // Delete the head
-    // eg. delete ''
-    snippets.shift();
-    // eg. After the operation is completed, the array should be ['list','search']
-    // eg. Forward the array as ['list','list/search']
-    snippets = snippets.map((item, index) => {
-      // If the array length > 1
-      if (index > 0) {
-        // eg. search => ['list','search'].join('/')
-        return snippets.slice(0, index + 1).join('/');
-      }
-      // index 0 to not do anything
-      return item;
-    });
-    snippets = snippets.map((item) => {
-      return this.getSelectedMenuKeys(`/${item}`)[0];
-    });
-    // eg. ['list','list/search']
-    return snippets;
+    return urlToList(pathname)
+      .map((item) => {
+        return getMeunMatcheys(this.flatMenuKeys, item)[0];
+      })
+      .filter(item => item);
   }
   /**
    * Recursively flatten the data
@@ -77,29 +66,17 @@ export default class SiderMenu extends PureComponent {
     let keys = [];
     menus.forEach((item) => {
       if (item.children) {
-        keys.push(item.path);
         keys = keys.concat(this.getFlatMenuKeys(item.children));
-      } else {
-        keys.push(item.path);
       }
+      keys.push(item.path);
     });
     return keys;
   }
   /**
-   * Get selected child nodes
-   * /user/chen => ['user','/user/:id']
+   * 判断是否是http链接.返回 Link 或 a
+   * Judge whether it is http link.return a or Link
+   * @memberof SiderMenu
    */
-  getSelectedMenuKeys = (path) => {
-    const flatMenuKeys = this.getFlatMenuKeys(this.menus);
-    return flatMenuKeys.filter((item) => {
-      return pathToRegexp(`/${item}(.*)`).test(path);
-    });
-  }
-  /**
-  * 判断是否是http链接.返回 Link 或 a
-  * Judge whether it is http link.return a or Link
-  * @memberof SiderMenu
-  */
   getMenuItemPath = (item) => {
     const itemPath = this.conversionPath(item.path);
     const icon = getIcon(item.icon);
@@ -108,7 +85,8 @@ export default class SiderMenu extends PureComponent {
     if (/^https?:\/\//.test(itemPath)) {
       return (
         <a href={itemPath} target={target}>
-          {icon}<span>{name}</span>
+          {icon}
+          <span>{name}</span>
         </a>
       );
     }
@@ -117,16 +95,23 @@ export default class SiderMenu extends PureComponent {
         to={itemPath}
         target={target}
         replace={itemPath === this.props.location.pathname}
-        onClick={this.props.isMobile ? () => { this.props.onCollapse(true); } : undefined}
+        onClick={
+          this.props.isMobile
+            ? () => {
+                this.props.onCollapse(true);
+              }
+            : undefined
+        }
       >
-        {icon}<span>{name}</span>
+        {icon}
+        <span>{name}</span>
       </Link>
     );
-  }
+  };
   /**
    * get SubMenu or Item
    */
-  getSubMenuOrItem=(item) => {
+  getSubMenuOrItem = (item) => {
     if (item.children && item.children.some(child => child.name)) {
       return (
         <SubMenu
@@ -136,8 +121,10 @@ export default class SiderMenu extends PureComponent {
                 {getIcon(item.icon)}
                 <span>{item.name}</span>
               </span>
-            ) : item.name
-            }
+            ) : (
+              item.name
+            )
+          }
           key={item.path}
         >
           {this.getNavMenuItems(item.children)}
@@ -145,16 +132,14 @@ export default class SiderMenu extends PureComponent {
       );
     } else {
       return (
-        <Menu.Item key={item.path}>
-          {this.getMenuItemPath(item)}
-        </Menu.Item>
+        <Menu.Item key={item.path}>{this.getMenuItemPath(item)}</Menu.Item>
       );
     }
-  }
+  };
   /**
-  * 获得菜单子节点
-  * @memberof SiderMenu
-  */
+   * 获得菜单子节点
+   * @memberof SiderMenu
+   */
   getNavMenuItems = (menusData) => {
     if (!menusData) {
       return [];
@@ -162,49 +147,60 @@ export default class SiderMenu extends PureComponent {
     return menusData
       .filter(item => item.name && !item.hideInMenu)
       .map((item) => {
+        // make dom
         const ItemDom = this.getSubMenuOrItem(item);
         return this.checkPermissionItem(item.authority, ItemDom);
       })
-      .filter(item => !!item);
-  }
+      .filter(item => item);
+  };
+  // Get the currently selected menu
+  getSelectedMenuKeys = () => {
+    const { location: { pathname } } = this.props;
+    return urlToList(pathname).map(itemPath =>
+      getMeunMatcheys(this.flatMenuKeys, itemPath).pop(),
+    );
+  };
   // conversion Path
   // 转化路径
-  conversionPath=(path) => {
+  conversionPath = (path) => {
     if (path && path.indexOf('http') === 0) {
       return path;
     } else {
       return `/${path || ''}`.replace(/\/+/g, '/');
     }
-  }
+  };
   // permission to check
   checkPermissionItem = (authority, ItemDom) => {
     if (this.props.Authorized && this.props.Authorized.check) {
       const { check } = this.props.Authorized;
-      return check(
-        authority,
-        ItemDom
-      );
+      return check(authority, ItemDom);
     }
     return ItemDom;
+  };
+  isMainMenu = (key) => {
+    return this.menus.some(
+      item =>
+        key && (item.key === key || item.path === key),
+    );
   }
   handleOpenChange = (openKeys) => {
     const lastOpenKey = openKeys[openKeys.length - 1];
-    const isMainMenu = this.menus.some(
-      item => lastOpenKey && (item.key === lastOpenKey || item.path === lastOpenKey)
-    );
+    const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
     this.setState({
-      openKeys: isMainMenu ? [lastOpenKey] : [...openKeys],
+      openKeys: moreThanOne ? [lastOpenKey] : [...openKeys],
     });
-  }
+  };
   render() {
-    const { logo, collapsed, location: { pathname }, onCollapse } = this.props;
+    const { logo, collapsed, onCollapse } = this.props;
     const { openKeys } = this.state;
     // Don't show popup menu when it is been collapsed
-    const menuProps = collapsed ? {} : {
-      openKeys,
-    };
+    const menuProps = collapsed
+      ? {}
+      : {
+        openKeys,
+      };
     // if pathname can't match, use the nearest parent's key
-    let selectedKeys = this.getSelectedMenuKeys(pathname);
+    let selectedKeys = this.getSelectedMenuKeys();
     if (!selectedKeys.length) {
       selectedKeys = [openKeys[openKeys.length - 1]];
     }

+ 36 - 0
src/components/SiderMenu/SilderMenu.test.js

@@ -0,0 +1,36 @@
+import { getMeunMatcheys } from './SiderMenu';
+
+const meun = [
+  '/dashboard',
+  '/userinfo',
+  '/dashboard/name',
+  '/userinfo/:id',
+  '/userinfo/:id/info',
+];
+
+describe('test meun match', () => {
+  it('simple path', () => {
+    expect(getMeunMatcheys(meun, '/dashboard')).toEqual(['/dashboard']);
+  });
+  it('error path', () => {
+    expect(getMeunMatcheys(meun, '/dashboardname')).toEqual([]);
+  });
+
+  it('Secondary path', () => {
+    expect(getMeunMatcheys(meun, '/dashboard/name')).toEqual([
+      '/dashboard/name',
+    ]);
+  });
+
+  it('Parameter path', () => {
+    expect(getMeunMatcheys(meun, '/userinfo/2144')).toEqual([
+      '/userinfo/:id',
+    ]);
+  });
+
+  it('three parameter path', () => {
+    expect(getMeunMatcheys(meun, '/userinfo/2144/info')).toEqual([
+      '/userinfo/:id/info',
+    ]);
+  });
+});

+ 7 - 0
src/components/utils/pathTools.js

@@ -0,0 +1,7 @@
+// /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
+export function urlToList(url) {
+  const urllist = url.split('/').filter(i => i);
+  return urllist.map((urlItem, index) => {
+    return `/${urllist.slice(0, index + 1).join('/')}`;
+  });
+}

+ 20 - 0
src/components/utils/pathTools.test.js

@@ -0,0 +1,20 @@
+import { urlToList } from './pathTools';
+
+describe('test urlToList', () => {
+  it('A path', () => {
+    expect(urlToList('/userinfo')).toEqual(['/userinfo']);
+  });
+  it('Secondary path', () => {
+    expect(urlToList('/userinfo/2144')).toEqual([
+      '/userinfo',
+      '/userinfo/2144',
+    ]);
+  });
+  it('Three paths', () => {
+    expect(urlToList('/userinfo/2144/addr')).toEqual([
+      '/userinfo',
+      '/userinfo/2144',
+      '/userinfo/2144/addr',
+    ]);
+  });
+});

+ 2 - 2
src/layouts/BasicLayout.js

@@ -27,8 +27,8 @@ const getRedirect = (item) => {
   if (item && item.children) {
     if (item.children[0] && item.children[0].path) {
       redirectData.push({
-        from: `/${item.path}`,
-        to: `/${item.children[0].path}`,
+        from: `${item.path}`,
+        to: `${item.children[0].path}`,
       });
       item.children.forEach((children) => {
         getRedirect(children);

+ 3 - 0
src/routes/List/Applications.less

@@ -14,6 +14,9 @@
     .ant-card-actions {
       background: #f7f9fa;
     }
+    .ant-list .ant-list-item-content-single {
+      max-width: 100%;
+    }
   }
   .cardInfo {
     .clearfix();

+ 6 - 0
src/routes/List/CardList.less

@@ -25,6 +25,12 @@
   .item {
     height: 64px;
   }
+
+  :global {
+    .ant-list .ant-list-item-content-single {
+      max-width: 100%;
+    }
+  }
 }
 
 .extraImg {

+ 3 - 2
src/routes/List/Projects.js

@@ -6,6 +6,7 @@ import { Row, Col, Form, Card, Select, List } from 'antd';
 import StandardFormRow from '../../components/StandardFormRow';
 import TagSelect from '../../components/TagSelect';
 import AvatarList from '../../components/AvatarList';
+import Ellipsis from '../../components/Ellipsis';
 
 import styles from './Projects.less';
 
@@ -54,7 +55,7 @@ export default class CoverCardList extends PureComponent {
       <List
         rowKey="id"
         loading={loading}
-        grid={{ gutter: 24, lg: 4, md: 3, sm: 2, xs: 1 }}
+        grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}
         dataSource={list}
         renderItem={item => (
           <List.Item>
@@ -65,7 +66,7 @@ export default class CoverCardList extends PureComponent {
             >
               <Card.Meta
                 title={<a href="#">{item.title}</a>}
-                description={item.subDescription}
+                description={<Ellipsis lines={2}>{item.subDescription}</Ellipsis>}
               />
               <div className={styles.cardItemContent}>
                 <span>{moment(item.updatedAt).fromNow()}</span>

+ 6 - 0
src/routes/List/Projects.less

@@ -46,4 +46,10 @@
   .cardList {
     margin-top: 24px;
   }
+
+  :global {
+    .ant-list .ant-list-item-content-single {
+      max-width: 100%;
+    }
+  }
 }