Browse Source

1. add resource manage module;
2. add merchant manage module;

zhanghe 7 years ago
parent
commit
2a0f82f043
47 changed files with 2436 additions and 289 deletions
  1. 67 25
      .roadhogrc.mock.js
  2. 36 0
      mock/merchant.js
  3. 38 0
      mock/resource.js
  4. 9 0
      mock/signature.js
  5. 1 0
      package.json
  6. 18 50
      src/common/menu.js
  7. 17 0
      src/common/router.js
  8. 0 84
      src/components/DataTable/DataTable.js
  9. 0 78
      src/components/DataTable/TableBodyWrapper.js
  10. 0 40
      src/components/DataTable/TableBodyWrapper.less
  11. 0 1
      src/components/DataTable/index.js
  12. 0 6
      src/components/DataTable/package.json
  13. 29 0
      src/components/FilterItem/FilterItem.js
  14. 17 0
      src/components/FilterItem/FilterItem.less
  15. 6 0
      src/components/FilterItem/package.json
  16. 40 0
      src/components/SelectSearch/index.js
  17. 141 0
      src/components/Uploader/index.js
  18. 26 0
      src/components/Uploader/index.less
  19. 86 0
      src/components/VideoPlayer/index.js
  20. 2 2
      src/layouts/PageHeaderLayout.less
  21. 49 0
      src/models/merchant/detail.js
  22. 113 0
      src/models/merchant/merchant.js
  23. 148 0
      src/models/resource.js
  24. 97 0
      src/routes/Merchant/detail/baseInfo.js
  25. 70 0
      src/routes/Merchant/detail/index.js
  26. 12 0
      src/routes/Merchant/detail/recommend.js
  27. 131 0
      src/routes/Merchant/index.js
  28. 48 0
      src/routes/Merchant/search.js
  29. 112 0
      src/routes/Merchant/table.js
  30. 82 0
      src/routes/Merchant/table.less
  31. 174 0
      src/routes/Resource/gallery/index.js
  32. 89 0
      src/routes/Resource/gallery/modal.js
  33. 48 0
      src/routes/Resource/gallery/search.js
  34. 117 0
      src/routes/Resource/gallery/table.js
  35. 86 0
      src/routes/Resource/gallery/table.less
  36. 141 0
      src/routes/Resource/video/index.js
  37. 25 0
      src/routes/Resource/video/modal.js
  38. 44 0
      src/routes/Resource/video/search.js
  39. 105 0
      src/routes/Resource/video/table.js
  40. 82 0
      src/routes/Resource/video/table.less
  41. 32 0
      src/services/merchant.js
  42. 32 0
      src/services/resource.js
  43. 1 0
      src/theme.js
  44. 5 0
      src/utils/api.js
  45. 28 1
      src/utils/config.js
  46. 21 0
      src/utils/helper.js
  47. 11 2
      src/utils/utils.js

+ 67 - 25
.roadhogrc.mock.js

@@ -2,14 +2,22 @@ import mockjs from 'mockjs';
 import { format, delay } from 'roadhog-api-doc';
 import { campusList } from './mock/campus';
 import { terminalList } from './mock/terminal';
+import { merchantList } from './mock/merchant';
+import { resourceList } from './mock/resource';
+import { signature } from './mock/signature';
 import * as api from './src/utils/api';
 
 // mock数据持久化
 global.campusList = campusList;
 global.terminalList = terminalList;
+global.merchantList = merchantList;
+global.resourceList = resourceList;
+global.signature = signature;
 
 // 操作成功响应内容
-const successMsg = { code: 200, success: true, message: null };
+const SUCCESS = { code: 200, success: true, message: null };
+// 资源未找到响应内容
+const NOTFOUND = { code: 404, message: 'Not Found!' };
 
 // 查询
 const query = (dataset, params) => {
@@ -31,7 +39,7 @@ const query = (dataset, params) => {
   const totalSize = newDataset.length || 0;
   const list = newDataset.slice(pageSize * (pageNo - 1), pageSize * pageNo);
   return {
-    ...successMsg,
+    ...SUCCESS,
     data: { pageSize, pageNo, totalSize: newDataset.length, list }
   };
 }
@@ -42,7 +50,7 @@ const create = (dataset, params) => {
   const data = { ...params, id: last.id + 1 };
   dataset.push(data);
   return {
-    ...successMsg,
+    ...SUCCESS,
     data,
   };
 }
@@ -55,18 +63,10 @@ const update = (dataset, params) => {
     }
   }
   return {
-    ...successMsg,
-  }
-}
-
-// 删除
-const remove = (dataset, params) => {
-  return {
-    ...successMsg,
+    ...SUCCESS,
   }
 }
 
-const NOTFOUND = { code: 404, message: 'Not Found!' };
 const queryArray = (array, key, keyAlias = 'key') => {
   if (!(array instanceof Array)) {
     return null
@@ -86,41 +86,83 @@ const queryArray = (array, key, keyAlias = 'key') => {
   return null
 }
 
+// 查询一条
+const queryOne = (dataset, id, res) => {
+  const target = queryArray(dataset, id, 'id');
+  if (target) {
+    res.send({ ...SUCCESS, data: { ...target } });
+  } else {
+    res.send({ ...NOTFOUND, data: {} });
+  }
+}
+
+// 删除
+const remove = (dataset, id, res) => {
+  const data = queryArray(dataset, id, 'id');
+  if (data) {
+    dataset.map(item => { item.id === data.id ? item.status = 'DEL' : null });
+    res.send(SUCCESS);
+  } else {
+    res.send(NOTFOUND);
+  }
+}
+
 // mock数据
 const proxy = {
+  [`POST ${api.resource.replace('/:id', '')}`]: (req, res) => {
+    res.send(create(global.resource, req.body));
+  },
+  [`DELETE ${api.resource}`]: (req, res) => {
+    const { id } = req.params;
+    remove(global.resourceList, id, res);
+  },
+  [`PUT ${api.resource.replace('/:id', '')}`]: (req, res) => {
+    res.send(update(global.resourceList, req.body));
+  },
+  [`GET ${api.resources}`]: (req, res) => {
+    res.send(query(global.resourceList, req.query));
+  },
+  [`GET ${api.signature}`]: (req, res) => {
+    res.send({ ...SUCCESS, data: global.signature });
+  },
   [`GET ${api.campuses}`]: (req, res) => {
-    console.log('[CampusList]', req.query);
     res.send(query(global.campusList, req.query));
   },
   [`POST ${api.campus.replace('/:id', '')}`]: (req, res) => {
-    console.log('[CampusCreate]', req.body);
     res.send(create(global.campusList, req.body));
   },
   [`PUT ${api.campus.replace('/:id', '')}`]: (req, res) => {
-    console.log('[CampusUpdate]', req.body);
     res.send(update(global.campusList, req.body));
   },
   [`GET ${api.terminals}`]: (req, res) => {
-    console.log('[TerminalList]', req.query);
     res.send(query(global.terminalList, req.query));
   },
   [`POST ${api.terminal.replace('/:id', '')}`]: (req, res) => {
-    console.log('[TerminalCreate]', req.body);
     res.send(create(global.terminalList, req.body));
   },
   [`PUT ${api.terminal.replace('/:id', '')}`]: (req, res) => {
-    console.log('[TerminalUpdate]', req.body);
     res.send(update(global.terminalList, req.body));
   },
   [`DELETE ${api.terminal}`]: (req, res) => {
     const { id } = req.params;
-    const data = queryArray(global.terminalList, id, 'id');
-    if (data) {
-      global.terminalList = global.terminalList.filter(item => item.id !== id);
-      res.status(204).end();
-    } else {
-      res.status(404).json(NOTFOUND);
-    }
+    remove(global.terminalList, id, res);
+  },
+  [`GET ${api.merchant}`]: (req, res) => {
+    const { id } = req.params;
+    queryOne(global.merchantList, id, res);
+  },
+  [`GET ${api.merchants}`]: (req, res) => {
+    res.send(query(global.merchantList, req.query));
+  },
+  [`POST ${api.merchant.replace('/:id', '')}`]: (req, res) => {
+    res.send(create(global.merchantList, req.body));
+  },
+  [`PUT ${api.merchant.replace('/:id', '')}`]: (req, res) => {
+    res.send(update(global.merchantList, req.body));
+  },
+  [`DELETE ${api.merchant}`]: (req, res) => {
+    const { id } = req.params;
+    remove(global.merchantList, id, res);
   },
 };
 

+ 36 - 0
mock/merchant.js

@@ -0,0 +1,36 @@
+const merchantList = [];
+for (let i = 1; i < 48; i++) {
+  merchantList.push({
+    id: String(i),
+    name: `厂商-${i}`,
+    domain: 2010,
+    code: '0000' + i,
+    licenseId: '79123765223900316',
+    taxNumber: '00130',
+    bankAccount: '2220 7983 5876 7895 021',
+    depositBank: '中国农业银行 朝阳支行',
+    receiptType: 'SPECIAL',
+    status: 'NORMAL',
+    adminList: [{
+      id: '520958243598293485295482',
+      name: '匿名者-1',
+      account: 'angelbell-adminer-1',
+      password: '123456',
+      contactNumber: '18323327652',
+      email: '190324568@qq.com',
+      wechat: 'jxa38012',
+      qq: '7721345689',
+    },{
+      id: '398939481934891581938493',
+      name: '匿名者-2',
+      account: 'angelbell-adminer-2',
+      password: '123456',
+      contactNumber: '13630147258',
+      email: 'tixqojpqr@163.com',
+      wechat: 'axj82301',
+      qq: '8011123689',
+    }],
+  });
+}
+
+module.exports = { merchantList };

+ 38 - 0
mock/resource.js

@@ -0,0 +1,38 @@
+let resourceList = [];
+for (let i = 1; i < 100; i++) {
+  resourceList = resourceList.concat([
+    {
+      id: 'abckiejij23423jkdajkdk0' + i,
+      code: 'J-02-01-612101' + i,
+      name: '破解表情密码01',
+      size: 90,
+      status: 'NORMAL',
+      type: '3',
+      gmtCreated: (new Date()).getTime(),
+      gmtModified: (new Date()).getTime(),
+      url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/612102.jpg',
+    },{
+      id: 'abckiejijnafkdjkdafsk0' + i,
+      code: 'J-01-03' + i,
+      name: '测试图片',
+      size: 100,
+      status: 'DEL',
+      type: '3',
+      gmtCreated: (new Date()).getTime(),
+      gmtModified: (new Date()).getTime(),
+      url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/611901.jpg',
+    },{
+      id: 'dsakfjweioqdasfjkadioe' + i,
+      code: 'V-01-02' + i,
+      name: '测试视频',
+      size: 1024,
+      status: 'NORMAL',
+      type: '0',
+      gmtCreated: (new Date()).getTime(),
+      gmtModified: (new Date()).getTime(),
+      url: 'http://efunvideo.ai160.com/vs2m/015/01503009/01503009031/01503009031.m3u8',
+    }
+  ]);
+}
+
+module.exports = { resourceList };

+ 9 - 0
mock/signature.js

@@ -0,0 +1,9 @@
+const signature = {
+  expire: 1515240873,
+  accessid: "LTAIjmNjhgzDGQ5U",
+  signature: "xVAY7d5lnCvz8M79hdthV4YIt8I=",
+  policy: "eyJjb25kaXRpb25zIjogW1sic3RhcnRzLXdpdGgiLCAiJGtleSIsICJ1cGxvYWRfdGVzdC8iXV0sICJleHBpcmF0aW9uIjogIjIwMTgtMDEtMDZUMjA6MTQ6MzNaIn0=",
+  host: "http://oss-learn.oss-ap-southeast-1.aliyuncs.com",
+  dir: "upload_test/",
+};
+module.exports = { signature };

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "g-cloud": "^1.0.2-beta",
     "g2": "^2.3.13",
     "g2-plugin-slider": "^1.2.1",
+    "hls.js": "^0.8.9",
     "lodash": "^4.17.4",
     "lodash-decorators": "^4.4.1",
     "lodash.clonedeep": "^4.5.0",

+ 18 - 50
src/common/menu.js

@@ -1,30 +1,11 @@
 const menuData = [
   {
-    name: '销售统计',
-    icon: 'area-chart',
-    path: 'sales',
-    children: [{
-      name: '销售概览',
-      path: 'view',
-    },{
-      name: '销售详情',
-      path: 'detail',
-    }]
-  },{
-    name: '交易管理',
-    icon: 'trademark',
-    path: 'trade',
-    children: [{
-      name: '订单管理',
-      path: 'order',
-    }],
-  },{
     name: '资源管理',
     icon: 'folder',
     path: 'resource',
     children: [{
       name: '图库管理',
-      path: 'image',
+      path: 'gallery',
     },{
       name: '视频管理',
       path: 'video',
@@ -69,17 +50,6 @@ const menuData = [
       path: 'combo',
     }]
   },{
-    name: '厂商管理',
-    icon: 'team',
-    path: 'merchant',
-    children: [{
-      name: '供应商管理',
-      path: 'cp',
-    },{
-      name: '渠道方管理',
-      path: 'project',
-    }]
-  },{
     name: '终端管理',
     icon: 'desktop',
     path: 'terminal',
@@ -91,28 +61,26 @@ const menuData = [
       path: 'campus',
     }],
   },{
-    name: '行为统计',
-    icon: 'scan',
-    path: 'action',
-    children: [{
-      name: '使用记录',
-      path: 'usage',
-    }],
+    name: '订单管理',
+    icon: 'trademark',
+    path: 'trade',
+  },{
+    name: '销售统计',
+    icon: 'area-chart',
+    path: 'sales',
+  },{
+    name: '厂商管理',
+    icon: 'team',
+    path: 'merchant',
   },{
     name: '账户管理',
     icon: 'user-add',
-    path: 'user',
-    children: [{
-      name: '领教方账户管理',
-      path: 'lj',
-    },{
-      name: '渠道方账户管理',
-      path: 'project',
-    },{
-      name: '供应商账户管理',
-      path: 'cp',
-    }]
-  }
+    path: 'cms-user',
+  },{
+    name: '行为统计',
+    icon: 'scan',
+    path: 'action',
+  },
 ];
 
 function formatter(data, parentPath = '') {

+ 17 - 0
src/common/router.js

@@ -36,12 +36,29 @@ export const getRouterData = (app) => {
     '/': {
       component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')),
     },
+    '/resource/gallery': {
+      component: dynamicWrapper(app, ['resource'], () => import('../routes/Resource/gallery')),
+    },
+    '/resource/video': {
+      component: dynamicWrapper(app, ['resource'], () => import('../routes/Resource/video')),
+    },
     '/terminal/campus': {
       component: dynamicWrapper(app, ['campus'], () => import('../routes/Campus')),
     },
     '/terminal/user': {
       component: dynamicWrapper(app, ['terminal'], () => import('../routes/Terminal')),
     },
+    '/merchant': {
+      component: dynamicWrapper(app, ['merchant/merchant'], () => import('../routes/Merchant')),
+    },
+    '/merchant/add': {
+      component: dynamicWrapper(app, ['merchant/detail'], () => import('../routes/Merchant/detail')),
+      name: '添加厂商',
+    },
+    '/merchant/edit/:id': {
+      component: dynamicWrapper(app, ['merchant/detail'], () => import('../routes/Merchant/detail')),
+      name: '修改厂商',
+    },
     '/user': {
       component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')),
     },

+ 0 - 84
src/components/DataTable/DataTable.js

@@ -1,84 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'dva';
-import { Table } from 'antd';
-import classnames from 'classnames';
-import TableBodyWrapper from './TableBodyWrapper'
-
-class DataTable extends PureComponent {
-  static propTypes = {
-    location: PropTypes.object,
-    animate: PropTypes.bool,
-    rowKey: PropTypes.oneOfType([
-      PropTypes.string,
-      PropTypes.func,
-    ]).isRequired,
-    pagination: PropTypes.oneOfType([
-      PropTypes.bool,
-      PropTypes.object,
-    ]).isRequired,
-    columns: PropTypes.array.isRequired,
-    dataSource: PropTypes.array.isRequired,
-    className: PropTypes.string,
-    onPageChange: PropTypes.func,
-  };
-
-  static defaultProps = {
-    animate: true,
-  };
-
-  constructor(props) {
-    super(props);
-    this.state = {
-      prevPage: 1,
-      currentPage: 1,
-    };
-  }
-
-  handlePageChange = (page) => {
-    const { location, pagination, onPageChange } = this.props;
-    const { query } = location;
-    this.setState({
-      prevPage: page.current,
-      pageSize: page.pageSize,
-    });
-    onPageChange({
-      ...query,
-      pageNo: page.current,
-      pageSize: page.pageSize,
-    });
-  }
-
-  handleChangePrevPage = () => {
-    this.setState({
-      prevPage: this.state.currentPage,
-    });
-  }
-
-  render() {
-    const { location, className, pagination, onPageChange, animate, ...props } = this.props;
-    const { prevPage, currentPage } = this.state;
-    const getBodyWrapperProps = {
-      prevPage,
-      currentPage,
-      onChangePrevPage: this.handleChangePrevPage,
-    };
-    const getBodyWrapper = body => (<TableBodyWrapper {...getBodyWrapperProps} body={body} />);
-    let tableProps = {
-      simple: true,
-      bordered: true,
-      onChange: this.handlePageChange,
-      pagination: !!pagination && { ...pagination, showSizeChanger: true, showQuickJumper: true, showTotal: total => `共 ${total} 条`},
-      ...props,
-    };
-    if (animate) {
-      tableProps.getBodyWrapper = getBodyWrapper;
-      tableProps.className = classnames(className, 'table-motion');
-    }
-    return (
-      <Table { ...tableProps } />
-    );
-  }
-}
-
-export default DataTable;

+ 0 - 78
src/components/DataTable/TableBodyWrapper.js

@@ -1,78 +0,0 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-import { TweenOneGroup } from 'rc-tween-one';
-import './TableBodyWrapper.less';
-
-const enterAnim = [
-  {
-    opacity: 0,
-    x: 30,
-    backgroundColor: '#fffeee',
-    duration: 0,
-  },{
-    height: 0,
-    duration: 200,
-    type: 'from',
-    delay: 250,
-    ease: 'easeOutQuad',
-    onComplete:(e) => {
-      e.target.style.height = 'auto';
-    }
-  },{
-    opacity: 1,
-    x: 0,
-    duration: 250,
-    ease: 'easeOutQuad',
-  },{
-    delay: 1000,
-    backgroundColor: '#fff',
-  }
-];
-const leaveAnim = [
-  {
-    duration: 250,
-    x: -30,
-    opacity: 0,
-  },{
-    height: 0,
-    duration: 200,
-    ease: 'easeOutQuad',
-  }
-];
-
-class TableBodyWrapper extends PureComponent {
-  static propTypes = {
-    body: PropTypes.element,
-    prevPage: PropTypes.number.isRequired,
-    currentPage: PropTypes.number.isRequired,
-    onChangePrevPage: PropTypes.func.isRequired,
-  };
-
-  componentWillReceiveProps (nextProps) {
-    if (this.props.currentPage !== nextProps.currentPage) {
-      // 等待动画执行完成后修正prevPage
-      setTimeout(() => { this.props.onChangePrevPage() }, 300)
-    }
-  }
-
-  render () {
-    const { body, prevPage, currentPage } = this.props
-    // 切换分页去除动画;
-    if (prevPage !== currentPage) {
-      return body
-    }
-    return (
-      <TweenOneGroup
-        component="tbody"
-        className={body.props.className}
-        enter={enterAnim}
-        leave={leaveAnim}
-        appear={false}
-      >
-        {body.props.children}
-      </TweenOneGroup>
-    )
-  }
-}
-
-export default TableBodyWrapper;

+ 0 - 40
src/components/DataTable/TableBodyWrapper.less

@@ -1,40 +0,0 @@
-:global {
-  .table-motion {
-    .ant-table-tbody > tr > td,
-    .ant-table-thead > tr > th {
-      height: 60px;
-    }
-
-    .ant-table-thead {
-      & > tr {
-        transition: none;
-        display: block;
-
-        & > th {
-          display: inline-flex;
-          align-items: center;
-          justify-content: center;
-        }
-      }
-    }
-
-    .ant-table-tbody {
-      & > tr {
-        transition: none;
-        display: block;
-        border-bottom: 1px solid #f5f5f5;
-
-        & > td {
-          border-bottom: none;
-          display: inline-flex;
-          align-items: center;
-          justify-content: center;
-        }
-
-        &.ant-table-expanded-row-level-1 > td {
-          height: auto;
-        }
-      }
-    }
-  }
-}

+ 0 - 1
src/components/DataTable/index.js

@@ -1 +0,0 @@
-export DataTable './DataTable';

+ 0 - 6
src/components/DataTable/package.json

@@ -1,6 +0,0 @@
-{
-  "name": "DataTable",
-  "version": "0.0.1",
-  "private": true,
-  "main": "./DataTable.js"
-}

+ 29 - 0
src/components/FilterItem/FilterItem.js

@@ -0,0 +1,29 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import styles from './FilterItem.less'
+
+const FilterItem = ({
+  label = '',
+  children,
+}) => {
+  const labelArray = label.split('')
+  return (
+    <div className={styles.filterItem}>
+      {labelArray.length > 0
+        ? <div className={styles.labelWrap}>
+          {labelArray.map((item, index) => <span className="labelText" key={index}>{item}</span>)}
+        </div>
+        : ''}
+      <div className={styles.item}>
+        {children}
+      </div>
+    </div>
+  )
+}
+
+FilterItem.propTypes = {
+  label: PropTypes.string,
+  children: PropTypes.element.isRequired,
+}
+
+export default FilterItem

+ 17 - 0
src/components/FilterItem/FilterItem.less

@@ -0,0 +1,17 @@
+.filterItem {
+  display: flex;
+  justify-content: space-between;
+
+  .labelWrap {
+    width: 30px;
+    line-height: 28px;
+    margin-right: 10px;
+    justify-content: flex-start;
+    display: flex;
+    overflow: hidden;
+  }
+
+  .item {
+    flex: 1;
+  }
+}

+ 6 - 0
src/components/FilterItem/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "FilterItem",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./FilterItem.js"
+}

+ 40 - 0
src/components/SelectSearch/index.js

@@ -0,0 +1,40 @@
+import react, { PureComponent } from 'react';
+import { Select, Input, Icon } from 'antd';
+
+const Option = Select.Option;
+
+export default class SelectSearch extends PureComponent {
+  handleChange = () => {
+    console.log('changing...');
+  }
+
+  render() {
+    return (
+      <Select
+        showSearch
+        allowClear
+        style={{ width: 200 }}
+        placeholder="请选择人员"
+        optionFilterProp="children"
+        notFoundContent="无法找到"
+        onChange={() => this.handleChange()}
+      >
+          <Option value="jack">张贺</Option>
+          <Option value="lucy">露西</Option>
+          <Option value="tom">汤姆</Option>
+          <Option value="jack1">张贺</Option>
+          <Option value="lucy1">露西</Option>
+          <Option value="tom1">汤姆</Option>
+          <Option value="jack2">张贺</Option>
+          <Option value="jack3">张贺</Option>
+          <Option value="jack4">张贺</Option>
+          <Option value="lucy13">露西</Option>
+          <Option value="tom2">汤姆</Option>
+          <Option value="lucy2">露西</Option>
+          <Option value="tom3">汤姆</Option>
+          <Option value="lucy3">露西</Option>
+          <Option value="tom4">汤姆</Option>
+      </Select>
+    );
+  }
+}

+ 141 - 0
src/components/Uploader/index.js

@@ -0,0 +1,141 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { message, Upload, Icon, Modal } from 'antd';
+import styles from './index.less';
+
+const getFileList = (files) => {
+  if (Array.isArray(files)) {
+    return files.map((item, key) => {
+      const urlArr = item.full_url.split('/');
+      return { url: item.full_url, id: item.id, uid: key, name: urlArr[urlArr.length - 1], status: 'done' };
+    });
+  }
+  if (files && !!files.length) {
+    const filesArr = files.split('/');
+    return [{ uid: -1, url: files, name: filesArr[filesArr.length - 1], status: 'done' }];
+  }
+  return '';
+}
+
+const renderAccept = (accept) => {
+  if (!accept) {
+    return null;
+  }
+  if (['image', 'video', 'audio'].find(ext => ext === accept)) {
+    return `${accept}/*`;
+  }
+  if (accept === 'zip') {
+    return 'application/zip,application/x-zip,application/x-zip-compressed';
+  }
+  return `.${accept}`;
+}
+
+export default class Uploader extends PureComponent {
+  static propTypes = {
+    files: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
+    onUpload: PropTypes.func.isRequired,
+    multiple: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
+    disabled: PropTypes.bool,
+    path: PropTypes.string,
+    accept: PropTypes.string,
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      previewVisible: false,
+      previewImage: '',
+      files: getFileList(props.files),
+    };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (Array.isArray(this.props.files) && !this.props.files.length && !!nextProps.files.length) {
+      this.setState({ files: getFileList(nextProps.files) });
+    }
+  }
+
+  render() {
+    const { previewVisible, previewImage, files } = this.state;
+    const { multiple = 1, onUpload, disabled, path, accept, signature } = this.props;
+
+    const renderFiles = (fileList) => {
+      const newFiles = fileList.map(file => {
+        return file.response ? file.response.data.file : file;
+      });
+      if (multiple === 1) {
+        return newFiles[0];
+      }
+      return newFiles;
+    }
+
+    const uploadProps = {
+      accept: renderAccept(accept),
+      action: signature.host,
+      headers: {
+        'Authorization': `OSS${signature.accessid}:${signature.signature}`,
+      },
+      data: {
+        policy: signature.policy,
+        signature: signature.signature,
+        OSSAccessKeyId: signature.accessid,
+        key: `${signature.dir}${signature.path}` + '${filename}',
+        success_action_status: '200',
+      },
+      disabled,
+      listType: 'picture-card',
+      fileList: files,
+      multiple: multiple === true,
+      onPreview: (file) => {
+        this.setState({ previewImage: file.url || file.thumbUrl, previewVisible: true });
+      },
+      beforeUpload: (file, fileList) => {
+        return true;
+      },
+      onChange: ({ file, fileList }) => {
+        if (file && file.size > 5 * 1024 * 1024) {
+          message.error('图片大小不能超过5M!');
+          return;
+        }
+        this.setState({ files: fileList });
+        if (file.percent === 100 && file.status === 'done') {
+          onUpload(renderFiles(fileList, 1));
+        } else if (file.status === 'error') {
+          message.error('图片上传失败!');
+        }
+      },
+      onRemove: (file) => {
+        if (disabled) {
+          return false;
+        }
+        const fileList = this.state.files.filter(item => item.uid !== file.uid);
+        onUpload(renderFiles(fileList, 0));
+        return true;
+      },
+    };
+
+    const modalProps = {
+      visible: previewVisible,
+      footer: null,
+      onCancel: () => this.setState({ previewVisible: false }),
+    };
+
+    const uploadButton = (
+      <div>
+        <Icon type="plus" />
+        <div className="ant-upload-text">点击上传</div>
+      </div>
+    );
+
+    return (
+      <div className="clearfix">
+        <Upload {...uploadProps}>
+          {multiple === true ? uploadButton : (files.length < multiple && uploadButton)}
+        </Upload>
+        <Modal {...modalProps}>
+          <img className={styles.previewImage} alt="" src={previewImage} />
+        </Modal>
+      </div>
+    );
+  }
+}

+ 26 - 0
src/components/Uploader/index.less

@@ -0,0 +1,26 @@
+:global {
+  .ant-upload-select-picture-card i {
+    font-size: 40px;
+    color: #999;
+  }
+
+  .ant-upload-select-picture-card .ant-upload-text {
+    margin-top: -10px;
+    font-size: 12px;
+    color: #666;
+  }
+
+  .ant-upload-list-picture-card .ant-upload-list-item {
+    width: 100px;
+    height: auto;
+  }
+
+  .ant-upload-list-picture-card .ant-upload-list-item-thumbnail img {
+    object-fit: contain; //cover
+  }
+}
+
+.previewImage{
+  width: 100%;
+  margin-top: 20px;
+}

+ 86 - 0
src/components/VideoPlayer/index.js

@@ -0,0 +1,86 @@
+import React, { PureComponent } from 'react';
+import Hls from 'hls.js';
+
+export default class VideoPlayer extends PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isMuted: false,       //无声/有声
+      isPlaying: false,
+      playerId: Date.now(),
+    };
+    this.hls = null;
+    this.playVideo = this.playVideo.bind(this);
+  }
+
+  componentDidMount() {
+    this._initPlayer();
+  }
+
+  componentWillUnmount() {
+    if(this.hls) {
+      this.hls.destroy();
+    }
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.isPaused) {
+      this.refs.video.pause();
+    }
+  }
+
+  playVideo() {
+    let { video : $video } = this.refs;
+    $video.play();
+
+    this.setState({isPlaying: true});
+  }
+
+  _initPlayer () {
+    if(this.hls) {
+      this.hls.destroy();
+    }
+
+    let { url, autoplay, hlsConfig } = this.props;
+    let { video : $video } = this.refs;
+    let hls = new Hls(hlsConfig);
+
+    hls.attachMedia($video);
+
+    hls.on(Hls.Events.MEDIA_ATTACHED, () => {
+      hls.loadSource(url);
+
+      hls.on(Hls.Events.MANIFEST_PARSED, () => {
+        if(autoplay) {
+          $video.play();
+        }
+        else {
+          $video.pause();
+        }
+      });
+    });
+
+    this.hls = hls;
+  }
+
+  render() {
+    let { isMuted, isPlaying, playerId } = this.state;
+    let { controls, width, height } = this.props;
+
+    return (
+      <div key={playerId}>
+        {!isPlaying &&
+          <span onClick={this.playVideo}></span>
+        }
+        <video ref="video"
+          id={`react-hls-${playerId}`}
+          controls={controls}
+          width={width}
+          height={height}
+          muted={isMuted}
+          playsInline>
+        </video>
+      </div>
+    );
+  }
+}

+ 2 - 2
src/layouts/PageHeaderLayout.less

@@ -1,11 +1,11 @@
 @import "~antd/lib/style/themes/default.less";
 
 .content {
-  margin: 24px 24px 0;
+  margin: 15px 15px 0;
 }
 
 @media screen and (max-width: @screen-sm) {
   .content {
-    margin: 24px 0 0;
+    margin: 15px 0 0;
   }
 }

+ 49 - 0
src/models/merchant/detail.js

@@ -0,0 +1,49 @@
+import { queryOne } from '../../services/merchant';
+import pathToRegexp from 'path-to-regexp';
+
+export default {
+  namespace: 'merchantDetail',
+
+  state: {
+    filters: {},
+    currentItem: {},
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state }) => {
+        const match = pathToRegexp('/merchant/edit/:id').exec(pathname);
+        if (match) {
+          dispatch({ type: 'query', payload: { id: match[1] } });
+          dispatch({ type: 'saveFilters', payload: state });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { itemLoading: true } });
+      const { data, success } = yield call(queryOne, payload);
+      if (success) {
+        yield put({ type: 'querySuccess', payload: { ...data } });
+      }
+      yield put({ type: 'changeLoading', payload: { itemLoading: false } });
+    }
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    querySuccess(state, { payload }) {
+      return { ...state, currentItem: payload };
+    },
+
+    saveFilters(state, { payload: filters }) {
+      return { ...state, filters };
+    },
+  }
+}

+ 113 - 0
src/models/merchant/merchant.js

@@ -0,0 +1,113 @@
+import { query, create, update, remove } from '../../services/merchant';
+import modelExtend from 'dva-model-extend';
+import queryString from 'query-string';
+import { message } from 'antd';
+import { pageModel } from '../common';
+import config from '../../utils/config';
+import { checkSearchParams } from '../../utils/utils';
+
+export default modelExtend(pageModel, {
+  namespace: 'merchant',
+
+  state: {
+    currentItem: {},
+    itemLoading: false,
+    listLoading: false,
+    modalVisible: false,
+    modalType: 'create',
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen((location) => {
+        if (location.pathname === '/merchant') {
+          const payload = checkSearchParams(queryString.parse(location.search));
+          dispatch({
+            type: 'query',
+            payload,
+          });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload = {} }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { listLoading: true }});
+      const { data, success } = yield call(query, payload);
+      if (success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: data.list,
+            pagination: {
+              current: Number(payload.pageNo) || 1,
+              pageSize: Number(payload.pageSize) || config.pageSize,
+              total: data.totalSize,
+            }
+          }
+        });
+      }
+      yield put({ type: 'changeLoading', payload: { listLoading: false }});
+    },
+    * create ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(create, payload);
+      if (success) {
+        message.success('创建成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        yield put({ type: 'hideModal' });
+        message.success('更新成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    },
+    * delete ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(remove, payload);
+      if (success) {
+        message.success('删除成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('删除失败!');
+      }
+    },
+    * recover ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('恢复成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('恢复失败!');
+      }
+    },
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    showModal(state, { payload }) {
+      return { ...state, ...payload, modalVisible: true };
+    },
+
+    hideModal(state) {
+      return { ...state, modalVisible: false };
+    },
+  }
+})

+ 148 - 0
src/models/resource.js

@@ -0,0 +1,148 @@
+import { query, remove, update, create, getSignature } from '../services/resource';
+import { message } from 'antd';
+import modelExtend from 'dva-model-extend';
+import queryString from 'query-string';
+import { pageModel } from './common';
+import config from '../utils/config';
+import { checkSearchParams } from '../utils/utils';
+import { saveLocalSignature, getLocalSignature } from '../utils/helper';
+
+export default modelExtend(pageModel, {
+  namespace: 'resource',
+
+  state: {
+    signature: {},
+    isPaused: false,
+    currentItem: {},
+    itemLoading: false,
+    listLoading: false,
+    modalVisible: false,
+    modalType: 'create',
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen((location) => {
+        if (location.pathname === '/resource/gallery') {
+          const payload = checkSearchParams(queryString.parse(location.search));
+          payload.type = config.Codes.CODE_IMAGE, // 只查询图片类型
+          dispatch({
+            type: 'query',
+            payload,
+          });
+        }
+        if (location.pathname === '/resource/video') {
+          const payload = checkSearchParams(queryString.parse(location.search));
+          payload.type = config.Codes.CODE_VIDEO, // 只查询视频类型
+          dispatch({
+            type: 'query',
+            payload,
+          });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload = {} }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { listLoading: true }});
+      const { data, success } = yield call(query, payload);
+      if (success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: data.list,
+            pagination: {
+              current: Number(payload.pageNo) || 1,
+              pageSize: Number(payload.pageSize) || config.pageSize,
+              total: data.totalSize,
+            }
+          }
+        });
+      }
+      yield put({ type: 'changeLoading', payload: { listLoading: false }});
+    },
+    * create ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(create, payload);
+      if (success) {
+        yield put({ type: 'hideModal' });
+        message.success('创建成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        yield put({ type: 'hideModal' });
+        message.success('更新成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    },
+    * delete ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(remove, payload);
+      if (success) {
+        message.success('删除成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('删除失败!');
+      }
+    },
+    * recover ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('恢复成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('恢复失败!');
+      }
+    },
+    * getSignAndShowModal ({ payload }, { call, put }) {
+      // 校验签名
+      let ossSignature;
+      const localSignature = getLocalSignature();
+      const expireTime = Math.floor((new Date()).getTime() / 1000 + 5).toString(); // 5s缓冲时间
+      // oss签名不存在或者已经过了有效期则请求新的签名
+      if (!localSignature || localSignature.expire <= expireTime) {
+        const { data, success } = yield call(getSignature, {});
+        if (success) {
+          saveLocalSignature(data);
+          ossSignature = data;
+        }
+      } else {
+        ossSignature = localSignature;
+      }
+      // 保存签名数据展示modal
+      yield put({ type: 'showModal', payload: { ...payload, signature: ossSignature } });
+    }
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    showModal(state, { payload }) {
+      return { ...state, ...payload, modalVisible: true };
+    },
+
+    hideModal(state) {
+      return { ...state, modalVisible: false };
+    },
+
+    closeVideo(state) {
+      return { ...state, isPaused: true, modalVisible: false };
+    },
+  }
+})

+ 97 - 0
src/routes/Merchant/detail/baseInfo.js

@@ -0,0 +1,97 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Card, Form, Switch, Radio, Button, Input } from 'antd';
+import { domains } from '../../../utils/config';
+
+const FormItem = Form.Item;
+const RadioGroup = Radio.Group;
+
+@Form.create()
+export default class BaseInfoCard extends PureComponent {
+  static propTypes = {
+    form: PropTypes.object.isRequired,
+    item: PropTypes.object.isRequired,
+  };
+
+  render() {
+    const { form: { getFieldDecorator }, item, onCancel } = this.props;
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 7 },
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 12 },
+        md: { span: 10 },
+      },
+    };
+
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+
+    return (
+      <Card title="基础信息">
+        <Form layout="horizontal">
+          <FormItem label="厂商编号" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('code',{
+              rules: [{ required: true, type: 'string', message: '编号为必填写!' }],
+              initialValue: item.code,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="厂商名称" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('name',{
+              rules: [{ required: true, type: 'string', message: '名称为必填项!' }],
+              initialValue: item.name,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="厂商类型" {...formItemLayout}>
+            {getFieldDecorator('domain',{
+              initialValue: item.domain || 2010,
+            })(<RadioGroup>{Object.keys(domains).map(key => <Radio value={Number(key)} key={`domain-${key}`}>{domains[key]}</Radio>)}</RadioGroup>)}
+          </FormItem>
+          <FormItem label="开户银行" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('depositBank',{
+              initialValue: item.depositBank,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="银行账户" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('bankAccount',{
+              initialValue: item.bankAccount,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="营业执照编号" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('licenseId',{
+              initialValue: item.licenseId,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="纳税人识别号" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('taxNumber',{
+              initialValue: item.taxNumber,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="发票类型" {...formItemLayout}>
+            {getFieldDecorator('receiptType', {
+              initialValue: item.receiptType || 'SPECIAL',
+            })(
+              <RadioGroup>
+                <Radio value="COMMON" key="receipt-com">普通发票</Radio>
+                <Radio value="SPECIAL" key="receipt-spl">增值税发票</Radio>
+              </RadioGroup>
+            )}
+          </FormItem>
+          <FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
+            <Button onClick={onCancel}>取消</Button>
+            <Button type="primary" style={{ marginLeft: 35 }} htmlType="submit">提交</Button>
+          </FormItem>
+        </Form>
+      </Card>
+    );
+  }
+}

+ 70 - 0
src/routes/Merchant/detail/index.js

@@ -0,0 +1,70 @@
+import React, { PureComponent } from 'react';
+import { routerRedux } from 'dva/router';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { Spin, Card, Form } from 'antd';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import BaseInfoCard from './baseInfo';
+import RecommendList from './recommend';
+
+@connect(state => ({ merchantDetail: state.merchantDetail }))
+export default class MerchantDetail extends PureComponent {
+  static propTypes = {
+    merchantDetail: PropTypes.object,
+  };
+
+  state = { curTab: 'baseInfo' };
+
+  handleTabChange = (key) => {
+    this.setState({ curTab: key });
+  }
+
+  render() {
+    const { dispatch, merchantDetail } = this.props;
+    const { itemLoading, currentItem, filters } = merchantDetail;
+
+    const tabList = [{
+      key: 'baseInfo',
+      tab: '基础信息',
+    },{
+      key: 'recommend',
+      tab: '推荐位设置',
+    }];
+
+    const baseInfoCardProps = {
+      item: currentItem,
+      onCancel: () => {
+        dispatch(
+          routerRedux.push({
+            pathname: '/merchant',
+            search: queryString.stringify(filters)
+          })
+        );
+      },
+      onSubmit: (data) => {
+        console.log(data);
+      }
+    };
+    const recommendCardProps = {
+
+    };
+
+    const contentMap = {
+      baseInfo: <BaseInfoCard { ...baseInfoCardProps }/>,
+      recommend: <RecommendList />
+    };
+
+    return (
+      <PageHeaderLayout
+        title="相关配置"
+        tabList={tabList}
+        onTabChange={this.handleTabChange}
+      >
+        <Spin spinning={itemLoading}>
+          {contentMap[this.state.curTab]}
+        </Spin>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 12 - 0
src/routes/Merchant/detail/recommend.js

@@ -0,0 +1,12 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Card } from 'antd';
+
+export default class RecommendList extends PureComponent {
+  render() {
+    return (
+      <Card title="推荐位配置">
+      </Card>
+    );
+  }
+}

+ 131 - 0
src/routes/Merchant/index.js

@@ -0,0 +1,131 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card } from 'antd';
+import TableList from './table';
+import Search from './search';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+
+@connect(state => ({ merchant: state.merchant }))
+export default class Merchant extends PureComponent {
+  static propTypes = {
+    merchant: PropTypes.object,
+    location: PropTypes.object,
+    dispatch: PropTypes.func,
+  };
+
+  render() {
+    const { location, dispatch, merchant } = this.props;
+
+    location.query = queryString.parse(location.search);
+    const { query, pathname } = location;
+    const { field, keyword, ...filters } = query;
+    const { list, listLoading, pagination, currentItem, itemLoading, modalVisible, modalType } = merchant;
+
+    // 把携带的参数中空值项删除
+    Object.keys(filters).map(key => { filters[key] ? null : delete filters[key] });
+    // 如果搜索内容不为空则添加进filters中
+    if (field && keyword) {
+      filters[field] = keyword;
+    }
+
+    const searchProps = {
+      field,
+      keyword,
+      onSearch: (payload) => {
+        if (!payload.keyword.length) {
+          delete payload.field;
+          delete payload.keyword;
+        }
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...payload
+          })
+        }));
+      },
+      onAdd: () => {
+        dispatch(
+          routerRedux.push({
+            pathname: '/merchant/add',
+            state: filters,
+          })
+        );
+      }
+    };
+
+    const listProps = {
+      pagination,
+      location,
+      dataSource: list,
+      loading: listLoading,
+      curDomain: filters.domain,
+      curStatus: filters.status,
+      onChange: (pagination, filterArgs) => {
+        const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+        const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+          const newObj = { ...obj };
+          newObj[key] = getValue(filterArgs[key]);
+          return newObj;
+        }, {});
+        const data = { ...filters, ...tableFilters };
+        Object.keys(data).map(key => data[key] ? null : delete data[key]);
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...data,
+            pageNo: pagination.current,
+            pageSize: pagination.pageSize,
+          }),
+        }));
+      },
+      onEditItem: (item) => {
+        dispatch(
+          routerRedux.push({
+            pathname: `/merchant/edit/${item.id}`,
+            state: filters,
+          })
+        );
+      },
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'merchant/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+      onRecoverItem: (payload) => {
+        dispatch({
+          type: 'merchant/recover',
+          payload,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      }
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 48 - 0
src/routes/Merchant/search.js

@@ -0,0 +1,48 @@
+import react, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form, Row, Col, Icon } from 'antd';
+import DataSearch from '../../components/DataSearch';
+
+@Form.create()
+export default class Search extends PureComponent {
+  static propTypes = {
+    form: PropTypes.object.isRequired,
+    onSearch: PropTypes.func,
+    onAdd: PropTypes.func,
+    field: PropTypes.string,
+    keyword: PropTypes.string,
+  };
+
+  render() {
+    const { field, keyword, onSearch, onAdd } = this.props;
+
+    const searchGroupProps = {
+      field,
+      keyword,
+      size: 'default',
+      select: true,
+      selectOptions: [{
+        value: 'name', name: '厂商名称'
+      },{
+        value: 'code', name: '厂商编号'
+      }],
+      selectProps: {
+        defaultValue: field || 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    return (
+      <Row gutter={24}>
+        <Col lg={10} md={12} sm={16} xs={24} style={{ marginBottom: 16 }}>
+          <DataSearch { ...searchGroupProps } />
+        </Col>
+        <Col lg={{ offset: 7, span: 7 }} md={12} sm={8} xs={24} style={{ marginBottom: 16, textAlign: 'right' }}>
+          <Button type="ghost" onClick={onAdd}><Icon type="plus-circle" />添加</Button>
+        </Col>
+      </Row>
+    );
+  }
+}

+ 112 - 0
src/routes/Merchant/table.js

@@ -0,0 +1,112 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import classnames from 'classnames';
+import queryString from 'query-string';
+import { Modal, Table, Menu, Icon, Badge } from 'antd';
+import AnimTableBody from '../../components/Animation/AnimTableBody';
+import styles from './table.less';
+import { domains, statuses, Codes } from '../../utils/config';
+
+const confirm = Modal.confirm;
+
+export default class TableList extends PureComponent {
+  static propTypes = {
+    location: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    onDeleteItem: PropTypes.func.isRequired,
+    onEditItem: PropTypes.func.isRequired,
+  };
+
+  handleOperateItem = (record) => {
+    const { onDeleteItem, onRecoverItem } = this.props;
+    confirm({
+      title: `您确定要${record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}该条记录?`,
+      onOk () {
+        if (record.status === Codes.CODE_NORMAL) {
+          onDeleteItem({id: record.id});
+        } else if (record.status === Codes.CODE_DELETE) {
+          onRecoverItem({ id: record.id, status: Codes.CODE_NORMAL });
+        }
+      },
+    })
+  }
+
+  render() {
+    const { curDomain, curStatus, onDeleteItem, onRecoverItem, onEditItem, location, pagination, ...tableProps } = this.props;
+
+    const columns = [{
+      title: '厂商编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '厂商名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '厂商类型',
+      dataIndex: 'domain',
+      key: 'domain',
+      render: (text, record) => (
+        <div>{domains[record.domain] || '未定义'}</div>
+      ),
+      filters: Object.keys(domains).map(key => ({ text: domains[key], value: key })),
+      filterMultiple: false,
+      filteredValue: [curDomain],
+    },{
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {'NORMAL': 'success', 'DEL': 'error'};
+        return (<Badge status={statusMap[record.status]} text={statuses[record.status]} />);
+      },
+      filters: Object.keys(statuses).map(key => ({ text: statuses[key], value: key })),
+      filterMultiple: false,
+      filteredValue: [curStatus],
+    },{
+      title: '添加时间',
+      dataIndex: 'gmtCreated',
+      key: 'gmtCreated',
+      render: (text, record) => (
+        <div>{moment(text).format('YYYY-MM-DD')}</div>
+      )
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <div>
+          <a onClick={() => onEditItem(record)}>编辑</a>
+          <span className={styles.splitLine} />
+            <a onClick={() => this.handleOperateItem(record)}>{record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}</a>
+        </div>
+      )
+    }];
+
+    // 数据table列表表头的筛选按钮点击重置后status与domain值为空,此时删除该参数
+    columns.map(item => {
+      item.dataIndex === 'status' && !curStatus ? delete item.filteredValue : null;
+      item.dataIndex === 'domain' && !curDomain ? delete item.filteredValue : null;
+    });
+
+    tableProps.pagination = !!pagination && { ...pagination, showSizeChanger: true, showQuickJumper: true, showTotal: total => `共 ${total} 条`};
+    const getBodyWrapperProps = {
+      page: location.query.page,
+      current: tableProps.pagination.current,
+    };
+    const getBodyWrapper = (body) => (<AnimTableBody {...getBodyWrapperProps} body={body} />);
+
+    return (
+      <Table
+        simple
+        bordered
+        { ...tableProps }
+        columns={columns}
+        className={classnames({ [styles.table]: true, [styles.motion]: true })}
+        rowKey={record => record.id}
+        getBodyWrapper={getBodyWrapper}
+      />
+    );
+  }
+}

+ 82 - 0
src/routes/Merchant/table.less

@@ -0,0 +1,82 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.table {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 62px;
+    }
+  }
+
+  &.motion {
+    :global {
+      .ant-table-tbody > tr > td,
+      .ant-table-thead > tr > th {
+        &:nth-child(1) {
+          width: 17%;
+        }
+
+        &:nth-child(2) {
+          width: 17%;
+        }
+
+        &:nth-child(3) {
+          width: 17%;
+        }
+
+        &:nth-child(4) {
+          width: 17%;
+        }
+
+        &:nth-child(5) {
+          width: 17%;
+        }
+
+        &:nth-child(6) {
+          width: 15%;
+        }
+      }
+
+      .ant-table-thead {
+        & > tr {
+          transition: none;
+          display: block;
+
+          & > th {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+
+      .ant-table-tbody {
+        & > tr {
+          transition: none;
+          display: block;
+          border-bottom: 1px solid #f5f5f5;
+
+          & > td {
+            border-bottom: none;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+
+          &.ant-table-expanded-row-level-1 > td {
+            height: auto;
+          }
+        }
+      }
+    }
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 174 - 0
src/routes/Resource/gallery/index.js

@@ -0,0 +1,174 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card } from 'antd';
+import TableList from './table';
+import Search from './search';
+import ModalForm from './modal';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+
+@connect(state => ({ resource: state.resource }))
+export default class Gallery extends PureComponent {
+  static propTypes = {
+    resource: PropTypes.object,
+    location: PropTypes.object,
+    dispatch: PropTypes.func,
+  };
+
+  render() {
+    const { location, dispatch, resource } = this.props;
+
+    location.query = queryString.parse(location.search);
+    const { query, pathname } = location;
+    const { field, keyword, ...filters } = query;
+    const { list, listLoading, pagination, currentItem, itemLoading, modalVisible, modalType, signature } = resource;
+
+    // 把携带的参数中空值项删除
+    Object.keys(filters).map(key => { filters[key] ? null : delete filters[key] });
+    // 如果搜索内容不为空则添加进filters中
+    if (field && keyword) {
+      filters[field] = keyword;
+    }
+
+    // 表头的搜索添加组件属性
+    const searchProps = {
+      field,
+      keyword,
+      onSearch: (payload) => {
+        if (!payload.keyword.length) {
+          delete payload.field;
+          delete payload.keyword;
+        }
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...payload
+          })
+        }));
+      },
+      onAdd: () => {
+        dispatch({
+          type: 'resource/getSignAndShowModal',
+          payload: {
+            modalType: 'create',
+          },
+        });
+      }
+    };
+
+    // 列表组件的属性
+    const listProps = {
+      pagination,
+      location,
+      curStatus: filters.status,
+      dataSource: list,
+      loading: listLoading,
+      onChange: (pagination, filterArgs) => {
+        const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+        const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+          const newObj = { ...obj };
+          newObj[key] = getValue(filterArgs[key]);
+          return newObj;
+        }, {});
+        const data = { ...filters, ...tableFilters };
+        Object.keys(data).map(key => data[key] ? null : delete data[key]);
+
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...data,
+            pageNo: pagination.current,
+            pageSize: pagination.pageSize
+          }),
+        }));
+      },
+      onEditItem: (item) => {
+        dispatch({
+          type: 'resource/showModal',
+          payload: {
+            modalType: 'update',
+            currentItem: item,
+          },
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        })
+      },
+      // 删除一条数据后,刷新页面保持筛选数据不变
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'resource/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+      // 恢复一条数据后,刷新页面并保持筛选数据不变
+      onRecoverItem: (payload) => {
+        dispatch({
+          type: 'resource/recover',
+          payload,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      }
+    };
+
+    const modalProps = {
+      item: modalType === 'create' ? {} : currentItem,
+      visible: modalVisible,
+      maskClosable: false,
+      signature,
+      title: `${modalType === 'create' ? '添加图片' : '编辑图片'}`,
+      wrapClassName: 'vertical-center-modal',
+      onOk (data) {
+        dispatch({
+          type: `resource/${modalType}`,
+          payload: data,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          },
+        });
+      },
+      onCancel () {
+        dispatch({
+          type: 'resource/hideModal',
+        });
+      },
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+          <ModalForm { ...modalProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 89 - 0
src/routes/Resource/gallery/modal.js

@@ -0,0 +1,89 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { message, Form, Switch, Input, Modal } from 'antd';
+import Uploader from '../../../components/Uploader';
+import { Codes, statuses } from '../../../utils/config';
+
+const FormItem = Form.Item
+
+@Form.create()
+export default class ModalForm extends PureComponent {
+  static propTypes = {
+    item: PropTypes.object,
+    form: PropTypes.object,
+    onOk: PropTypes.func,
+  };
+  
+  state = { file: '' };
+
+  componentWillReceiveProps(nextProps) {
+    this.setState({ file: nextProps.item.url });
+  }
+
+  handleSingleUpload = (file) => {
+    if (file) {
+      message.success('图片上传成功!');
+    }
+    this.setState({ file });
+  }
+
+  handleOk = () => {
+    const { form: { validateFields, resetFields }, onOk } = this.props;
+    validateFields((errors) => {
+      if (errors) { return; }
+      const data = {
+        ...getFieldsValue(),
+      };
+      item.id ? data.id = item.id : null;
+      onOk(data);
+      resetFields();
+    });
+  }
+
+  render() {
+    const { item = {}, form: { getFieldDecorator }, signature, modalType, ...modalProps } = this.props;
+    const { file } = this.state;
+    const modalOpts = { ...modalProps, onOk: this.handleOk };
+
+    const formItemLayout = {
+      labelCol: {
+        span: 7,
+      },
+      wrapperCol: {
+        span: 14,
+      },
+    };
+
+    return (
+      <Modal key={item.id} {...modalOpts}>
+        <Form layout="horizontal">
+          <FormItem label="图片编号:" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('code', {
+              rules: [{ required: true, type: 'string', message: "此项为必填项!" }],
+              initialValue: item.code,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="图片名称:" hasFeedback {...formItemLayout}>
+            {getFieldDecorator('name', {
+              rules: [{ required: true, type: 'string', message: "此项为必填项!" }],
+              initialValue: item.name,
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="单图上传" {...formItemLayout}>
+            <Uploader
+              accept="image"
+              files={file}
+              signature={signature}
+              onUpload={::this.handleSingleUpload}
+            />
+          </FormItem>
+          <FormItem label="使用状态:" {...formItemLayout}>
+            {getFieldDecorator('status', {
+              valuePropsName: 'checked',
+          })(<Switch defaultChecked={item.status || Codes.CODE_NORMAL === Codes.CODE_NORMAL ? true : false} checkedChildren="使用中" unCheckedChildren="已删除" />)}
+          </FormItem>
+        </Form>
+      </Modal>
+    );
+  }
+}

+ 48 - 0
src/routes/Resource/gallery/search.js

@@ -0,0 +1,48 @@
+import react, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form, Row, Col, Icon } from 'antd';
+import DataSearch from '../../../components/DataSearch';
+
+@Form.create()
+export default class Search extends PureComponent {
+  static propTypes = {
+    form: PropTypes.object.isRequired,
+    onSearch: PropTypes.func,
+    onAdd: PropTypes.func,
+    field: PropTypes.string,
+    keyword: PropTypes.string,
+  };
+
+  render() {
+    const { field, keyword, onSearch, onAdd } = this.props;
+
+    const searchGroupProps = {
+      field,
+      keyword,
+      size: 'default',
+      select: true,
+      selectOptions: [{
+        value: 'name', name: '图片名称'
+      },{
+        value: 'code', name: '图片编号'
+      }],
+      selectProps: {
+        defaultValue: field || 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    return (
+      <Row gutter={24}>
+        <Col lg={10} md={12} sm={16} xs={24} style={{ marginBottom: 16 }}>
+          <DataSearch { ...searchGroupProps } />
+        </Col>
+        <Col lg={{ offset: 7, span: 7 }} md={12} sm={8} xs={24} style={{ marginBottom: 16, textAlign: 'right' }}>
+          <Button type="ghost" onClick={onAdd}><Icon type="plus-circle" />添加</Button>
+        </Col>
+      </Row>
+    );
+  }
+}

+ 117 - 0
src/routes/Resource/gallery/table.js

@@ -0,0 +1,117 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import classnames from 'classnames';
+import queryString from 'query-string';
+import { Popover, Modal, Table, Menu, Icon, Badge } from 'antd';
+import AnimTableBody from '../../../components/Animation/AnimTableBody';
+import { statuses, Codes } from '../../../utils/config'
+import styles from './table.less';
+
+const confirm = Modal.confirm;
+
+export default class TableList extends PureComponent {
+  static propTypes = {
+    location: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    onDeleteItem: PropTypes.func.isRequired,
+    onEditItem: PropTypes.func.isRequired,
+  };
+
+  handleOperateItem = (record) => {
+    const { onDeleteItem, onRecoverItem } = this.props;
+    confirm({
+      title: `您确定要${record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}该条记录?`,
+      onOk () {
+        if (record.status === Codes.CODE_NORMAL) {
+          onDeleteItem({ id: record.id });
+        } else if (record.status === Codes.CODE_DELETE) {
+          onRecoverItem({ id: record.id, status: Codes.CODE_NORMAL });
+        }
+      },
+    })
+  }
+
+  render() {
+    const { curStatus, onDeleteItem, onEditItem, location, pagination, ...tableProps } = this.props;
+
+    const columns = [{
+      title: '缩略图',
+      dataIndex: 'url',
+      key: 'url',
+      render: (text, record) => (
+        <Popover
+          content={<img alt="" src={record.url} width={350} />}
+          title={record.name}
+        >
+          <img alt="" src={record.url} width={70} />
+        </Popover>
+      )
+    },{
+      title: '图片编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '图片名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '图片大小',
+      dataIndex: 'size',
+      key: 'size',
+    },{
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {'NORMAL': 'success', 'DEL': 'error'};
+        return (<Badge status={statusMap[record.status]} text={statuses[record.status]} />);
+      },
+      filters: Object.keys(statuses).map(key => ({ text: statuses[key], value: key })),
+      filterMultiple: false,
+      filteredValue: [curStatus],
+    },{
+      title: '添加时间',
+      dataIndex: 'gmtCreated',
+      key: 'gmtCreated',
+      render: (text, record) => (
+        <div>{moment(text).format('YYYY-MM-DD')}</div>
+      )
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <div>
+          <a onClick={() => onEditItem(record)}>编辑</a>
+          <span className={styles.splitLine} />
+            <a onClick={() => this.handleOperateItem(record)}>{record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}</a>
+        </div>
+      )
+    }];
+
+    // 数据table列表表头的筛选按钮点击重置后status值为空,此时删除该参数
+    columns.map(item => {
+      item.dataIndex === 'status' && !curStatus ? delete item.filteredValue : null;
+    });
+
+    tableProps.pagination = !!pagination && { ...pagination, showSizeChanger: true, showQuickJumper: true, showTotal: total => `共 ${total} 条`};
+    const getBodyWrapperProps = {
+      page: location.query.page,
+      current: tableProps.pagination.current,
+    };
+    const getBodyWrapper = (body) => (<AnimTableBody {...getBodyWrapperProps} body={body} />);
+
+    return (
+      <Table
+        simple
+        bordered
+        { ...tableProps }
+        columns={columns}
+        className={classnames({ [styles.table]: true, [styles.motion]: true })}
+        rowKey={record => record.id}
+        getBodyWrapper={getBodyWrapper}
+      />
+    );
+  }
+}

+ 86 - 0
src/routes/Resource/gallery/table.less

@@ -0,0 +1,86 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../../utils/utils.less";
+
+.table {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 62px;
+    }
+  }
+
+  &.motion {
+    :global {
+      .ant-table-tbody > tr > td,
+      .ant-table-thead > tr > th {
+        &:nth-child(1) {
+          width: 16%;
+        }
+
+        &:nth-child(2) {
+          width: 16%;
+        }
+
+        &:nth-child(3) {
+          width: 16%;
+        }
+
+        &:nth-child(4) {
+          width: 12%;
+        }
+
+        &:nth-child(5) {
+          width: 12%;
+        }
+
+        &:nth-child(6) {
+          width: 14%;
+        }
+
+        &:nth-child(7) {
+          width: 14%;
+        }
+      }
+
+      .ant-table-thead {
+        & > tr {
+          transition: none;
+          display: block;
+
+          & > th {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+
+      .ant-table-tbody {
+        & > tr {
+          transition: none;
+          display: block;
+          border-bottom: 1px solid #f5f5f5;
+
+          & > td {
+            border-bottom: none;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+
+          &.ant-table-expanded-row-level-1 > td {
+            height: auto;
+          }
+        }
+      }
+    }
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 141 - 0
src/routes/Resource/video/index.js

@@ -0,0 +1,141 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card } from 'antd';
+import TableList from './table';
+import Search from './search';
+import ModalForm from './modal';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+
+@connect(state => ({ resource: state.resource }))
+export default class Video extends PureComponent {
+  static propTypes = {
+    resource: PropTypes.object,
+    location: PropTypes.object,
+    dispatch: PropTypes.func,
+  };
+
+  render() {
+    const { location, dispatch, resource } = this.props;
+
+    location.query = queryString.parse(location.search);
+    const { query, pathname } = location;
+    const { field, keyword, ...filters } = query;
+    const { list, listLoading, pagination, currentItem, itemLoading, modalVisible, isPaused, modalType } = resource    // 把携带的参数中空值项删除
+
+    Object.keys(filters).map(key => { filters[key] ? null : delete filters[key] });
+    // 如果搜索内容不为空则添加进filters中
+    if (field && keyword) {
+      filters[field] = keyword;
+    }
+
+    const searchProps = {
+      field,
+      keyword,
+      onSearch: (payload) => {
+        if (!payload.keyword.length) {
+          delete payload.field;
+          delete payload.keyword;
+        }
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...payload
+          })
+        }));
+      }
+    };
+
+    const listProps = {
+      pagination,
+      location,
+      curStatus: filters.status,
+      dataSource: list,
+      loading: listLoading,
+      onChange: (pagination, filterArgs) => {
+        const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+        const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+          const newObj = { ...obj };
+          newObj[key] = getValue(filterArgs[key]);
+          return newObj;
+        }, {});
+        const data = { ...filters, ...tableFilters };
+        Object.keys(data).map(key => data[key] ? null : delete data[key]);
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...data,
+            pageNo: pagination.current,
+            pageSize: pagination.pageSize,
+          }),
+        }));
+      },
+      onPlayVideo: (item) => {
+        dispatch({
+          type: 'resource/showModal',
+          payload: {
+            modalType: 'update',
+            currentItem: item,
+          },
+        })
+      },
+      // 删除一条数据后,刷新页面保持筛选数据不变
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'resource/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+      // 恢复一条数据后,刷新页面并保持筛选数据不变
+      onRecoverItem: (payload) => {
+        dispatch({
+          type: 'resource/recover',
+          payload,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      }
+    };
+
+    const modalProps = {
+      item: currentItem,
+      visible: modalVisible,
+      footer: null,
+      maskClosable: false,
+      isPaused,
+      title: currentItem.name || '观看视频',
+      wrapClassName: 'vertical-center-modal',
+      onCancel () {
+        dispatch({
+          type: 'resource/closeVideo',
+        })
+      },
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+          <ModalForm { ...modalProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 25 - 0
src/routes/Resource/video/modal.js

@@ -0,0 +1,25 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Modal } from 'antd';
+import VideoPlayer from '../../../components/VideoPlayer';
+
+export default class ModalForm extends PureComponent {
+  static propTypes = {
+    item: PropTypes.object,
+  };
+
+  render() {
+    const { item = {}, isPaused, ...modalProps } = this.props;
+    const newModalProps = {
+      ...modalProps,
+      key: item.id,
+    }
+    const playerProps = { url: item.url, isPaused, hlsConfig: {}, width:'100%', height: '100%', controls: true };
+
+    return (
+      <Modal {...newModalProps}>
+        <VideoPlayer { ...playerProps } />
+      </Modal>
+    );
+  }
+}

+ 44 - 0
src/routes/Resource/video/search.js

@@ -0,0 +1,44 @@
+import react, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form, Row, Col, Icon } from 'antd';
+import DataSearch from '../../../components/DataSearch';
+
+@Form.create()
+export default class Search extends PureComponent {
+  static propTypes = {
+    form: PropTypes.object.isRequired,
+    onSearch: PropTypes.func,
+    field: PropTypes.string,
+    keyword: PropTypes.string,
+  };
+
+  render() {
+    const { field, keyword, onSearch } = this.props;
+
+    const searchGroupProps = {
+      field,
+      keyword,
+      size: 'default',
+      select: true,
+      selectOptions: [{
+        value: 'name', name: '视频名称'
+      },{
+        value: 'code', name: '视频编号'
+      }],
+      selectProps: {
+        defaultValue: field || 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    return (
+      <Row gutter={24}>
+        <Col lg={10} md={12} sm={16} xs={24} style={{ marginBottom: 16 }}>
+          <DataSearch { ...searchGroupProps } />
+        </Col>
+      </Row>
+    );
+  }
+}

+ 105 - 0
src/routes/Resource/video/table.js

@@ -0,0 +1,105 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import classnames from 'classnames';
+import queryString from 'query-string';
+import { Popover, Modal, Table, Menu, Icon, Badge } from 'antd';
+import AnimTableBody from '../../../components/Animation/AnimTableBody';
+import { statuses, Codes } from '../../../utils/config'
+import styles from './table.less';
+
+const confirm = Modal.confirm;
+
+export default class TableList extends PureComponent {
+  static propTypes = {
+    location: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    onDeleteItem: PropTypes.func.isRequired,
+    onPlayVideo: PropTypes.func.isRequired,
+  };
+
+  handleOperateItem = (record) => {
+    const { onDeleteItem, onRecoverItem } = this.props;
+    confirm({
+      title: `您确定要${record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}该条记录?`,
+      onOk () {
+        if (record.status === Codes.CODE_NORMAL) {
+          onDeleteItem({id: record.id});
+        } else if (record.status === Codes.CODE_DELETE) {
+          onRecoverItem({ id: record.id, status: Codes.CODE_NORMAL });
+        }
+      },
+    })
+  }
+
+  render() {
+    const { curStatus, onPlayVideo, location, pagination, ...tableProps } = this.props;
+
+    const columns = [{
+      title: '视频编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '视频名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '视频大小(M)',
+      dataIndex: 'size',
+      key: 'size',
+    },{
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {[Codes.CODE_NORMAL]: 'success', [Codes.CODE_DELETE]: 'error'};
+        return (<Badge status={statusMap[record.status]} text={statuses[record.status]} />);
+      },
+      filters: Object.keys(statuses).map(key => ({ text: statuses[key], value: key })),
+      filterMultiple: false,
+      filteredValue: [curStatus],
+    },{
+      title: '添加时间',
+      dataIndex: 'gmtCreated',
+      key: 'gmtCreated',
+      render: (text, record) => (
+        <div>{moment(text).format('YYYY-MM-DD')}</div>
+      )
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <div>
+          <a onClick={() => onPlayVideo(record)}>播放</a>
+          <span className={styles.splitLine} />
+          <a onClick={() => this.handleOperateItem(record)}>{record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}</a>
+        </div>
+      )
+    }];
+
+    // 数据table列表表头的筛选按钮点击重置后status值为空,此时删除该参数
+    columns.map(item => {
+      item.dataIndex === 'status' && !curStatus ? delete item.filteredValue : null;
+    });
+
+    tableProps.pagination = !!pagination && { ...pagination, showSizeChanger: true, showQuickJumper: true, showTotal: total => `共 ${total} 条`};
+    const getBodyWrapperProps = {
+      page: location.query.page,
+      current: tableProps.pagination.current,
+    };
+    const getBodyWrapper = (body) => (<AnimTableBody {...getBodyWrapperProps} body={body} />);
+
+    return (
+      <Table
+        simple
+        bordered
+        { ...tableProps }
+        columns={columns}
+        className={classnames({ [styles.table]: true, [styles.motion]: true })}
+        rowKey={record => record.id}
+        getBodyWrapper={getBodyWrapper}
+      />
+    );
+  }
+}

+ 82 - 0
src/routes/Resource/video/table.less

@@ -0,0 +1,82 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../../utils/utils.less";
+
+.table {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 62px;
+    }
+  }
+
+  &.motion {
+    :global {
+      .ant-table-tbody > tr > td,
+      .ant-table-thead > tr > th {
+        &:nth-child(1) {
+          width: 17%;
+        }
+
+        &:nth-child(2) {
+          width: 17%;
+        }
+
+        &:nth-child(3) {
+          width: 16%;
+        }
+
+        &:nth-child(4) {
+          width: 16%;
+        }
+
+        &:nth-child(5) {
+          width: 16%;
+        }
+
+        &:nth-child(6) {
+          width: 18%;
+        }
+      }
+
+      .ant-table-thead {
+        & > tr {
+          transition: none;
+          display: block;
+
+          & > th {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+
+      .ant-table-tbody {
+        & > tr {
+          transition: none;
+          display: block;
+          border-bottom: 1px solid #f5f5f5;
+
+          & > td {
+            border-bottom: none;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+
+          &.ant-table-expanded-row-level-1 > td {
+            height: auto;
+          }
+        }
+      }
+    }
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 32 - 0
src/services/merchant.js

@@ -0,0 +1,32 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { merchants, merchant } from '../utils/api';
+
+export async function query(params) {
+  return request(`${merchants}?${stringify(params)}`);
+}
+
+export async function queryOne({ id }) {
+  return request(`${merchant.replace('/:id', `/${id}`)}`);
+}
+
+export async function create(params) {
+  const options = {
+    method: 'POST',
+    body: JSON.stringify(params),
+  };
+  return request(`${merchant.replace('/:id', '')}`, options);
+}
+
+export async function update(params) {
+  const options = {
+    method: 'PUT',
+    body: JSON.stringify(params),
+  };
+  return request(`${merchant.replace('/:id', '')}`, options);
+}
+
+export async function remove({ id }) {
+  const options = { method: 'DELETE' }
+  return request(`${merchant.replace('/:id', `/${id}`)}`, options);
+}

+ 32 - 0
src/services/resource.js

@@ -0,0 +1,32 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { resources, resource, signature } from '../utils/api';
+
+export async function getSignature (params) {
+  return request(`${signature}?${stringify(params)}`);
+}
+
+export async function query (params) {
+  return request(`${resources}?${stringify(params)}`);
+}
+
+export async function create(params) {
+  const options = {
+    method: 'POST',
+    body: JSON.stringify(params),
+  };
+  return request(`${resource.replace('/:id', '')}`, options);
+}
+
+export async function remove ({ id }) {
+  const options = { method: 'DELETE' };
+  return request(`${resource.replace('/:id', `/${id}`)}`, options);
+}
+
+export async function update(params) {
+  const options = {
+    method: 'PUT',
+    body: JSON.stringify(params),
+  };
+  return request(`${resource.replace('/:id', '')}`, options);
+}

+ 1 - 0
src/theme.js

@@ -2,4 +2,5 @@
 module.exports = {
   // 'primary-color': '#10e99b',
   'card-actions-background': '#f5f8fa',
+  'border-radius-base': '1px',
 };

+ 5 - 0
src/utils/api.js

@@ -1,6 +1,9 @@
 import config from './config';
 
 module.exports = {
+  resources: `${config.apiHost}/resource/list`,
+  resource: `${config.apiHost}/resource/:id`,
+  signature: `${config.apiHost}/signature`,
   cmsUsers: `${config.apiHost}/cms/user/list`,
   currentCmsUser: `${config.apiHost}/cms/user`,
   userLogin: `${config.apiHost}/login`,
@@ -9,4 +12,6 @@ module.exports = {
   campus: `${config.apiHost}/campus/:id`,
   terminals: `${config.apiHost}/user/list`,
   terminal: `${config.apiHost}/user/:id`,
+  merchants: `${config.apiHost}/merchants/list`,
+  merchant: `${config.apiHost}/merchant/:id`,
 };

+ 28 - 1
src/utils/config.js

@@ -1,7 +1,34 @@
 /* 项目配置文件 */
 
+const Codes = {};
+Codes.CODE_VIDEO = 0;
+Codes.CODE_AUDIO = 1;
+Codes.CODE_LIVE  = 2;
+Codes.CODE_IMAGE = 3;
+Codes.CODE_NORMAL = 'NORMAL';
+Codes.CODE_DELETE = 'DEL';
+
 module.exports = {
   // apiHost: 'http://lj.dev.cms.api.com:9100',
-  apiHost: '',
+  apiHost: '/api',
+  // 每页返回数据量
   pageSize: 10,
+  // 标识码
+  Codes,
+  // 状态类型
+  statuses: {
+    [Codes.CODE_NORMAL]: '使用中',
+    [Codes.CODE_DELETE]: '已删除',
+  },
+  domains: {
+    2010: '供应商',
+    3010: '渠道商',
+  },
+  // 资源类型
+  resourceType: {
+    [Codes.CODE_VIDEO]: '视频',
+    [Codes.CODE_AUDIO]: '音频',
+    [Codes.CODE_LIVE] : '直播',
+    [Codes.CODE_IMAGE]: '图片',
+  },
 };

+ 21 - 0
src/utils/helper.js

@@ -30,3 +30,24 @@ export function getLocalToken() {
     return JSON.parse(localUser).token;
   }
 }
+
+/**
+ * 保存oss签名信息到本地
+ *
+ * @param {object} value [登录接口返回内容]
+ */
+export function saveLocalSignature(value) {
+  window.localStorage.setItem('LJ@2B#CMS!OSS', JSON.stringify(value));
+}
+
+/**
+ * 从本地存储中获取oss签名信息
+ *
+ * @return {JSON} [签名信息]
+ */
+export function getLocalSignature() {
+  const signature = window.localStorage.getItem('LJ@2B#CMS!OSS');
+  if (signature && signature !== 'undefined') {
+    return JSON.parse(signature);
+  }
+}

+ 11 - 2
src/utils/utils.js

@@ -122,8 +122,17 @@ export function getRoutes(path, routerData) {
       renderArr.push(routes[i]);
     }
   }
-  const renderRoutes = renderArr.map((item) => {
-    const exact = !routes.some(route => route !== item && getRelation(route, item) === 1);
+  // const renderRoutes = renderArr.map((item) => {
+  //   const exact = !routes.some(route => route !== item && getRelation(route, item) === 1);
+  //   return {
+  //     key: `${path}${item}`,
+  //     path: `${path}${item}`,
+  //     component: routerData[`${path}${item}`].component,
+  //     exact,
+  //   };
+  // });
+  const renderRoutes = routes.map((item) => {
+    const exact = true;
     return {
       key: `${path}${item}`,
       path: `${path}${item}`,