Explorar el Código

商户、校区管理模块

zhanghe hace 7 años
padre
commit
2cb2a0c336
Se han modificado 52 ficheros con 2786 adiciones y 547 borrados
  1. 1 0
      package.json
  2. 27 11
      src/common/menu.js
  3. 28 2
      src/common/router.js
  4. 0 39
      src/components/CardList/index.js
  5. 1 1
      src/components/EditableItem/EditableInput.js
  6. 7 7
      src/components/RBItem/PictureItem.js
  7. 5 4
      src/components/RBItem/PictureItem.less
  8. 181 182
      src/components/RBList/StandardCardList.js
  9. 11 6
      src/components/RBList/StandardCardList.less
  10. 313 29
      src/components/RBList/StandardTableList.js
  11. 67 0
      src/components/RBList/StandardTableList.less
  12. 0 0
      src/components/RBRemoteSelect/index.js
  13. 20 29
      src/components/RBUpload/index.js
  14. 90 0
      src/components/RBVideoPlayer/index.js
  15. 8 0
      src/components/RBVideoPlayer/index.less
  16. 3 0
      src/components/SiderMenu/index.less
  17. 9 0
      src/index.less
  18. 1 1
      src/layouts/BasicLayout.js
  19. 96 0
      src/models/campus.js
  20. 4 4
      src/models/error.js
  21. 2 2
      src/models/global.js
  22. 107 0
      src/models/merchant.js
  23. 31 8
      src/models/resource.js
  24. 32 0
      src/routes/Campus/Campus.js
  25. 176 0
      src/routes/Campus/CampusList.js
  26. 16 0
      src/routes/Campus/CampusList.less
  27. 32 0
      src/routes/Merchant/Merchant.js
  28. 294 0
      src/routes/Merchant/MerchantCreate.js
  29. 10 0
      src/routes/Merchant/MerchantCreate.less
  30. 110 0
      src/routes/Merchant/MerchantDespoit.js
  31. 6 0
      src/routes/Merchant/MerchantDespoit.less
  32. 211 0
      src/routes/Merchant/MerchantList.js
  33. 23 0
      src/routes/Merchant/MerchantList.less
  34. 55 0
      src/routes/Resource/PictureCardList.js
  35. 1 1
      src/routes/Resource/PictureCreate.js
  36. 9 12
      src/routes/Resource/PictureCreateMultiple.js
  37. 0 1
      src/routes/Resource/PictureCreateMultiple.less
  38. 17 18
      src/routes/Resource/PictureCreateSingle.js
  39. 50 71
      src/routes/Resource/PictureList.js
  40. 114 0
      src/routes/Resource/PictureTableList.js
  41. 0 9
      src/routes/Resource/VideoList.less
  42. 85 101
      src/routes/Resource/VideoList.js
  43. 181 0
      src/routes/Resource/VideoPlayList.js
  44. 91 0
      src/routes/Resource/VideoPlayList.less
  45. 111 0
      src/routes/Resource/VideoTableList.js
  46. 0 1
      src/routes/User/Login.js
  47. 34 0
      src/services/campus.js
  48. 42 0
      src/services/merchant.js
  49. 3 5
      src/services/resource.js
  50. 13 1
      src/utils/config.js
  51. 2 2
      src/utils/request.js
  52. 56 0
      src/utils/utils.js

+ 1 - 0
package.json

@@ -31,6 +31,7 @@
     "dva-loading": "^1.0.4",
     "enquire-js": "^0.1.1",
     "fastclick": "^1.0.6",
+    "hls.js": "^0.9.1",
     "lodash": "^4.17.4",
     "lodash-decorators": "^4.4.1",
     "moment": "^2.19.1",

+ 27 - 11
src/common/menu.js

@@ -4,7 +4,7 @@ import { plantform } from '../utils/config';
 import RBIcon from '../components/RBIcon';
 
 const menuData = () => {
-  if ('LJ' === plantform) {
+  if (plantform === 'LJ') {
     return [{
       name: '统计概览',
       icon: 'dashboard',
@@ -94,25 +94,41 @@ const menuData = () => {
       }, {
         name: '订单列表',
         icon: <RBIcon type="order" />,
-      }]
+      }],
     }, {
       name: '厂商管理',
       icon: 'team',
       path: 'merchant',
+      children: [{
+        name: '商户列表',
+        path: 'list',
+      }],
     }, {
       name: '校区管理',
       icon: <RBIcon type="campus" />,
       path: 'campus',
+      children: [{
+        name: '校区列表',
+        path: 'list',
+      }],
     }, {
       name: '终端用户',
       path: 'terminal',
-      icon: <RBIcon type="terminal" />
+      icon: <RBIcon type="terminal" />,
+      children: [{
+        name: '终端列表',
+        path: 'list',
+      }],
     }, {
-      name: '系统用户',
+      name: '系统管理',
       path: 'system',
-      icon: <RBIcon type="systemuser" />
+      icon: <RBIcon type="systemuser" />,
+      children: [{
+        name: '用户列表',
+        path: 'list',
+      }],
     }];
-  } else if ('PJ' === plantform) {
+  } else if (plantform === 'PJ') {
     return [{
       name: '统计概览',
       icon: 'dashboard',
@@ -155,7 +171,7 @@ const menuData = () => {
       }, {
         name: '订单列表',
         icon: <RBIcon type="order" />,
-      }]
+      }],
     }, {
       name: '校区管理',
       icon: <RBIcon type="campus" />,
@@ -163,13 +179,13 @@ const menuData = () => {
     }, {
       name: '终端用户',
       path: 'terminal',
-      icon: <RBIcon type="terminal" />
+      icon: <RBIcon type="terminal" />,
     }, {
       name: '账户信息',
       icon: 'team',
       path: 'merchant',
     }];
-  } else if ('CP' === plantform) {
+  } else if (plantform === 'CP') {
     return [{
       name: '统计概览',
       icon: 'dashboard',
@@ -178,7 +194,7 @@ const menuData = () => {
         name: '营销统计',
         path: 'monitor',
         icon: <RBIcon type="monitor" />,
-      }]
+      }],
     }, {
       name: '产品库',
       icon: 'shop',
@@ -200,7 +216,7 @@ const menuData = () => {
       children: [{
         name: '订单列表',
         icon: <RBIcon type="order" />,
-      }]
+      }],
     }, {
       name: '账户信息',
       icon: 'team',

+ 28 - 2
src/common/router.js

@@ -81,15 +81,41 @@ export const getRouterData = (app) => {
     },
     '/resource/imageCreate/single': {
       component: dynamicWrapper(app, ['resource'], () => import('../routes/Resource/PictureCreateSingle')),
-      name: '单图上传'
+      name: '单图上传',
     },
     '/resource/imageCreate/multiple': {
       component: dynamicWrapper(app, [], () => import('../routes/Resource/PictureCreateMultiple')),
-      name: '多图上传'
+      name: '多图上传',
     },
     '/resource/video': {
       component: dynamicWrapper(app, ['resource'], () => import('../routes/Resource/VideoList')),
     },
+    '/merchant': {
+      component: dynamicWrapper(app, ['merchant'], () => import('../routes/Merchant/Merchant')),
+    },
+    '/merchant/list': {
+      component: dynamicWrapper(app, ['merchant'], () => import('../routes/Merchant/MerchantList')),
+      name: '商户列表',
+    },
+    '/merchant/create': {
+      component: dynamicWrapper(app, ['merchant'], () => import('../routes/Merchant/MerchantCreate')),
+      name: '添加商户',
+    },
+    '/merchant/edit/:id': {
+      component: dynamicWrapper(app, ['merchant'], () => import('../routes/Merchant/MerchantCreate')),
+      name: '编辑商户',
+    },
+    '/merchant/despoit/:id': {
+      component: dynamicWrapper(app, ['merchant'], () => import('../routes/Merchant/MerchantDespoit')),
+      name: '余额充值',
+    },
+    '/campus': {
+      component: dynamicWrapper(app, ['campus'], () => import('../routes/Campus/Campus')),
+    },
+    '/campus/list': {
+      component: dynamicWrapper(app, ['campus'], () => import('../routes/Campus/CampusList')),
+      name: '校区列表',
+    },
     '/dashboard/analysis': {
       component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')),
     },

+ 0 - 39
src/components/CardList/index.js

@@ -1,39 +0,0 @@
-import React, { PureComponent } from 'react';
-import { List } from 'antd';
-
-export default class CardList extends PureComponent {
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    const { dataset, loading, grid, children, pagination, header, footer, bordered } = this.props;
-    return (
-      <div className={styles.list}>
-        <div className={styles.header}></div>
-        <div className={styles.content}>
-          <List
-            pagination={false}
-            bordered={bordered}
-            header={header}
-            footer={footer}
-            grid={grid}
-            dataSource={dataset}
-            loading={loading}
-            renderItem={item =>
-              <List.Item key={item.id}>
-                <PictureItem
-                  picUrl={item.url}
-                  picCode={item.code}
-                  picName={item.name}
-                  checked={item.checked}
-                />
-              </List.Item>
-            }
-          />
-        </div>
-        <div className={styles.footer}></div>
-      </div>
-    );
-  }
-}

+ 1 - 1
src/components/EditableItem/EditableInput.js

@@ -28,7 +28,7 @@ export default class EditableInput extends PureComponent {
       <Input
         value={value}
         onChange={this.handleChange}
-        disabled={ editable ? false : true }
+        disabled={!editable}
         addonAfter={
           editable ? (
             <Icon

+ 7 - 7
src/components/RBItem/PictureItem.js

@@ -4,26 +4,25 @@ import { ossHost } from '../../utils/config';
 import styles from './PictureItem.less';
 
 class PictureItem extends Component {
-  constructor(props) {
-    super(props);
-  }
   handleItemChecked = (e) => {
     const { id, onSelectChange } = this.props;
     onSelectChange(id, e.target.checked);
   }
   handleItemDelete = () => {
     const { id, onDelete } = this.props;
-    onDelete({id});
+    onDelete({ id });
   }
   render() {
-    const { picPath, picName, picCode, selected } = this.props;
+    const { picPath, picCode, selected } = this.props;
     return (
       <Card
         bordered
         hoverable
         className={styles.card}
       >
-        <div className={styles.picWrapper}><img src={`${ossHost}/${picPath}`}/></div>
+        <div className={styles.picWrapper}>
+          <img src={`${ossHost}/${picPath}`} alt="cover" />
+        </div>
         <Checkbox
           checked={selected}
           className={styles.checkbox}
@@ -50,7 +49,8 @@ class PictureItem extends Component {
             type="primary"
             icon="delete"
             onClick={this.handleItemDelete}
-          >删除</Button>
+          >删除
+          </Button>
         </div>
       </Card>
     );

+ 5 - 4
src/components/RBItem/PictureItem.less

@@ -9,10 +9,11 @@
       display: block;
     }
   }
-}
-
-:global(.ant-card-body) {
-  padding: 0 !important;
+  :global {
+    .ant-card-body {
+      padding: 0 !important;
+    }
+  }
 }
 
 .checkbox {

+ 181 - 182
src/components/RBList/StandardCardList.js

@@ -1,5 +1,5 @@
 import React, { PureComponent, Fragment } from 'react';
-import { Alert, List, Select, Input, Checkbox, Button, Icon, Pagination } from 'antd';
+import { Alert, List, Select, Input, Checkbox, Button, Pagination } from 'antd';
 import PropTypes from 'prop-types';
 import Authorized from '../../utils/Authorized';
 import styles from './StandardCardList.less';
@@ -7,48 +7,27 @@ import styles from './StandardCardList.less';
 function generator(props) {
   return (WrappedComponent) => {
     return (
-      <WrappedComponent {...props}/>
+      <WrappedComponent {...props} />
     );
-  }
+  };
 }
 function getSearchField(options) {
   if (options && options.keys && options.keys.length) {
     return options.keys[0].field;
-  } else {
-    return;
   }
 }
 
 class StandardCardList extends PureComponent {
-  constructor(props) {
-    super(props);
-    const {
-      keepUIState,
-      header: {
-        basicSearch,
-      }
-    } = this.props;
-    this.state = {
-      batchActionKey: keepUIState.batchActionKey,
-      isAdvancedLevel: keepUIState.isAdvancedLevel || false,
-      searchSelectKey: keepUIState.searchSelectKey || getSearchField(basicSearch),
-      searchInputValue: keepUIState.searchInputValue || '',
-      selectedKeys: keepUIState.selectedKeys || [],
-      allChecked: keepUIState.allChecked || false,
-      selectedStatusKey: keepUIState.selectedStatusKey || 'ALL',
-      pageNo: keepUIState.pageNo || 1,
-      pageSize: keepUIState.pageSize || 15,
-      totalSize: keepUIState.totalSize || 0,
-    }
-  }
   static defaultProps = {
     loading: false,
     dataSource: [],
+    header: false,
+    footer: false,
   }
   static propTypes = {
     loading: PropTypes.bool,
     dataSource: PropTypes.array,
-    component: PropTypes.func,
+    component: PropTypes.func.isRequired,
     header: PropTypes.oneOfType([
       PropTypes.object,
       PropTypes.bool,
@@ -58,6 +37,30 @@ class StandardCardList extends PureComponent {
       PropTypes.bool,
     ]),
   }
+  constructor(props) {
+    super(props);
+    const {
+      keepUIState,
+      header: {
+        basicSearch,
+      },
+      footer: {
+        pagination,
+      },
+    } = this.props;
+    this.state = {
+      batchActionKey: keepUIState.batchActionKey,
+      isAdvancedLevel: keepUIState.isAdvancedLevel || false,
+      searchSelectKey: keepUIState.searchSelectKey || getSearchField(basicSearch),
+      searchInputValue: keepUIState.searchInputValue || '',
+      selectedKeys: keepUIState.selectedKeys || [],
+      allChecked: keepUIState.allChecked || false,
+      selectedStatusKey: keepUIState.selectedStatusKey || 'ALL',
+      pageNo: keepUIState.pageNo || pagination.pageNo || 1,
+      pageSize: keepUIState.pageSize || pagination.pageSize || 15,
+      totalSize: keepUIState.totalSize || pagination.totalSize || 0,
+    };
+  }
   componentWillReceiveProps(nextProps) {
     if (nextProps.footer) {
       const { pagination } = nextProps.footer;
@@ -65,11 +68,142 @@ class StandardCardList extends PureComponent {
       this.setState({ pageNo, pageSize, totalSize });
     }
   }
-  //过滤
+  getListHeader = () => {
+    const {
+      header: { basicSearch, advancedSearch, onCreateClick },
+      footer: { pagination },
+    } = this.props;
+    const { keys } = basicSearch;
+    return (
+      <div className={styles.header}>
+        <div className={styles.headerSearch}>
+          <div className={styles.basicSearch}>
+            <div className={styles.left}>
+              <Select
+                onChange={this.handleStatusChange}
+                value={this.state.selectedStatusKey}
+                style={{ width: '20%', marginRight: 10 }}
+              >
+                <Select.Option key="all" value="ALL">全部状态</Select.Option>
+                <Select.Option key="normal" value="NORMAL">正常</Select.Option>
+                <Select.Option key="delete" value="DELETE">已删除</Select.Option>
+              </Select>
+              <Input.Search
+                value={this.state.searchInputValue}
+                style={{ width: '75%' }}
+                addonBefore={
+                  <Select
+                    placeholder="请选择"
+                    value={this.state.searchSelectKey}
+                    onChange={this.handleSearchSelectChange}
+                  >
+                    {keys.map(item => (
+                      <Select.Option
+                        key={item.field}
+                        value={item.field}
+                      >
+                        {item.name}
+                      </Select.Option>))}
+                  </Select>
+                }
+                placeholder="请输入"
+                enterButton
+                onChange={this.handleInputChange}
+                onSearch={this.handleSearchBtnClick}
+              />
+            </div>
+            <div className={styles.right}>
+              {advancedSearch &&
+                (this.state.isAdvancedLevel ? <a>普通搜索</a> : <a>高级搜索</a>)
+              }
+              <Button icon="sync" onClick={this.handleRefreshBtnClick}>刷新</Button>
+              <Authorized authority="root" noMatch={null}>
+                <Button
+                  icon="plus"
+                  type="primary"
+                  style={{ marginLeft: 5 }}
+                  onClick={onCreateClick}
+                >新建
+                </Button>
+              </Authorized>
+            </div>
+          </div>
+          {advancedSearch &&
+            this.state.isAdvancedLevel ?
+              <div className={styles.advancedSearch}>{advancedSearch}</div>
+            : null
+          }
+        </div>
+        <Alert
+          message={(
+            <Fragment>
+              已选择 <a style={{ fontWeight: 600 }}>{this.state.selectedKeys.length}</a> 项&nbsp;&nbsp;
+              总计 <a style={{ fontWeight: 600 }}>{pagination.totalSize}</a> 项&nbsp;&nbsp;
+              <a onClick={this.cleanSelectedKeys} style={{ fontWeight: 600 }}>清空</a>
+            </Fragment>
+          )}
+          type="info"
+          showIcon
+        />
+      </div>
+    );
+  }
+  getListFooter = () => {
+    const { footer: { batchActions, pagination } } = this.props;
+    const paginationProps = {
+      total: this.state.totalSize,
+      current: this.state.pageNo,
+      pageSize: this.state.pageSize,
+      showSizeChanger: true,
+      showQuickJumper: true,
+      onChange: this.handleListPageChange,
+      onShowSizeChange: this.handleListPageSizeChange,
+    };
+    return (
+      <div className={styles.footer}>
+        {batchActions && (
+        <Authorized authority="root" noMatch={null}>
+          <div className={styles.batch}>
+            <Checkbox
+              checked={this.state.allChecked}
+              onChange={this.handleAllSelectChange}
+            >全选
+            </Checkbox>
+            <Select
+              style={{ width: 100 }}
+              placeholder="请选择..."
+              value={this.state.batchActionKey}
+              onChange={this.handleBatchActionSelectChange}
+            >
+              {batchActions.map(item => (
+                <Select.Option key={item.key} value={item.key}>
+                  {item.name}
+                </Select.Option>))}
+            </Select>
+            <Button
+              type="primary"
+              style={{ marginLeft: 15 }}
+              onClick={this.handleBatchBtnClick}
+              disabled={!this.state.selectedKeys.length}
+            >确定
+            </Button>
+          </div>
+        </Authorized>)}
+        {pagination && (
+        <div className={styles.pagination}>
+          <Pagination
+            {...paginationProps}
+            showTotal={total => `共 ${total} 条`}
+          />
+        </div>)}
+      </div>
+    );
+  }
+  // 过滤
   handleFilterOperation = (kv) => {
     const {
       header: {
-        onFilterClick
+        onFilterClick,
       },
     } = this.props;
     const {
@@ -79,7 +213,7 @@ class StandardCardList extends PureComponent {
       pageNo,
       pageSize,
     } = this.state;
-    let queryParams = {
+    const queryParams = {
       pageNo,
       pageSize,
       [searchSelectKey]: searchInputValue,
@@ -91,7 +225,7 @@ class StandardCardList extends PureComponent {
     }
     onFilterClick(queryParams, this.state);
   }
-  //单选/取消单选
+  // 单选/取消单选
   handleItemSelectChange = (itemId, checked) => {
     const { selectedKeys } = this.state;
     const newSelectedKeys = [...selectedKeys];
@@ -101,7 +235,7 @@ class StandardCardList extends PureComponent {
       this.setState({ selectedKeys: newSelectedKeys.filter(a => a !== itemId) });
     }
   }
-  //全选/取消全选
+  // 全选/取消全选
   handleAllSelectChange = (e) => {
     const { dataSource } = this.props;
     let newSelectedKeys = [];
@@ -113,14 +247,14 @@ class StandardCardList extends PureComponent {
       selectedKeys: newSelectedKeys,
     });
   }
-  //取消全选
+  // 取消全选
   cleanSelectedKeys = () => {
     this.setState({
       selectedKeys: [],
       allChecked: false,
     });
   }
-  //刷新页面,重置筛选参数
+  // 刷新页面,重置筛选参数
   cleanFilterParams = () => {
     const { header: { basicSearch } } = this.props;
     this.setState({
@@ -133,36 +267,36 @@ class StandardCardList extends PureComponent {
       selectedStatusKey: 'ALL',
     });
   }
-  //过滤状态
+  // 过滤状态
   handleStatusChange = (value) => {
     this.setState({
-        selectedStatusKey: value,
+      selectedStatusKey: value,
     }, () =>
       this.handleFilterOperation({ status: value })
     );
   }
-  //选择搜索字段
+  // 选择搜索字段
   handleSearchSelectChange = (value) => {
     this.setState({ searchSelectKey: value });
   }
-  //响应input输入
+  // 响应input输入
   handleInputChange = (e) => {
     this.setState({ searchInputValue: e.target.value });
   }
-  //筛选搜索操作
+  // 筛选搜索操作
   handleSearchBtnClick = (value) => {
     const { searchSelectKey } = this.state;
     this.setState({
-        searchInputValue: value,
+      searchInputValue: value,
     }, () =>
       this.handleFilterOperation({ [searchSelectKey]: value })
     );
   }
-  //刷新操作
+  // 刷新操作
   handleRefreshBtnClick = () => {
     this.handleFilterOperation({});
   }
-  //list pageNo变化
+  // list pageNo变化
   handleListPageChange = (page, pageSize) => {
     this.setState({
       pageSize,
@@ -174,7 +308,7 @@ class StandardCardList extends PureComponent {
       });
     });
   }
-  //list pageSize变化
+  // list pageSize变化
   handleListPageSizeChange = (current, size) => {
     this.setState({
       pageSize: size,
@@ -186,152 +320,16 @@ class StandardCardList extends PureComponent {
       });
     });
   }
-  //选择批量处理类型
+  // 选择批量处理类型
   handleBatchActionSelectChange = (value) => {
     this.setState({ batchActionKey: value });
   }
-  //批量处理操作
+  // 批量处理操作
   handleBatchBtnClick = () => {
     const { footer: { onBatchClick } } = this.props;
     const { batchActionKey, selectedKeys } = this.state;
     onBatchClick(batchActionKey, selectedKeys);
   }
-  getListHeader = () => {
-    const {
-      header: { basicSearch, advancedSearch, onCreateClick },
-      footer: { pagination },
-    } = this.props;
-    const { keys, value } = basicSearch;
-    return (
-      <div className={styles.header}>
-        <div className={styles.headerSearch}>
-          <div className={styles.basicSearch}>
-            <div className={styles.left}>
-              <Select
-                onChange={this.handleStatusChange}
-                value={this.state.selectedStatusKey}
-                style={{ width: '20%', marginRight: 10 }}
-              >
-                <Select.Option key="all" value="ALL">全部状态</Select.Option>
-                <Select.Option key="normal" value="NORMAL">正常</Select.Option>
-                <Select.Option key="delete" value="DELETE">已删除</Select.Option>
-              </Select>
-              <Input.Search
-                value={this.state.searchInputValue}
-                style={{ width: '75%' }}
-                addonBefore={
-                  <Select
-                    placeholder="请选择"
-                    value={this.state.searchSelectKey}
-                    onChange={this.handleSearchSelectChange}
-                  >
-                    {keys.map(item =>
-                      <Select.Option
-                        key={item.field}
-                        value={item.field}
-                      >
-                        {item.name}
-                      </Select.Option>
-                    )}
-                  </Select>
-                }
-                placeholder="请输入"
-                enterButton
-                onChange={this.handleInputChange}
-                onSearch={this.handleSearchBtnClick}
-              />
-            </div>
-            <div className={styles.right}>
-              {advancedSearch &&
-                (this.state.isAdvancedLevel ? <a>普通搜索</a> : <a>高级搜索</a>)
-              }
-              <Button icon="sync" onClick={this.handleRefreshBtnClick}>刷新</Button>
-              <Authorized authority="root" noMatch={null}>
-                <Button
-                  icon="plus"
-                  type="primary"
-                  style={{ marginLeft: 5 }}
-                  onClick={onCreateClick}
-                >新建
-                </Button>
-              </Authorized>
-            </div>
-          </div>
-          {advancedSearch &&
-            this.state.isAdvancedLevel ?
-              <div className={styles.advancedSearch}>{advancedSearch}</div>
-            : null
-          }
-        </div>
-        <Alert
-          message={(
-            <Fragment>
-              已选择 <a style={{ fontWeight: 600 }}>{this.state.selectedKeys.length}</a> 项&nbsp;&nbsp;
-              总计 <a style={{ fontWeight: 600 }}>{pagination.totalSize}</a> 项&nbsp;&nbsp;
-              <a onClick={this.cleanSelectedKeys} style={{ fontWeight: 600 }}>清空</a>
-            </Fragment>
-          )}
-          type="info"
-          showIcon
-        />
-      </div>
-    );
-  }
-  getListFooter = () => {
-    const { footer: { batchActions, pagination } } = this.props;
-    const paginationProps = {
-      total: this.state.totalSize,
-      current: this.state.pageNo,
-      pageSize: this.state.pageSize,
-      showSizeChanger: true,
-      showQuickJumper: true,
-      onChange: this.handleListPageChange,
-      onShowSizeChange: this.handleListPageSizeChange,
-      ...pagination,
-    };
-    return (
-      <div className={styles.footer}>
-        {batchActions &&
-          <Authorized authority="root" noMatch={null}>
-            <div className={styles.batch}>
-              <Checkbox
-                checked={this.state.allChecked}
-                onChange={this.handleAllSelectChange}
-              >全选
-              </Checkbox>
-              <Select
-                style={{ width: 100 }}
-                placeholder="请选择..."
-                value={this.state.batchActionKey}
-                onChange={this.handleBatchActionSelectChange}
-              >
-                {batchActions.map((item) =>
-                  <Select.Option key={item.key} value={item.key}>
-                    {item.name}
-                  </Select.Option>
-                )}
-              </Select>
-              <Button
-                type="primary"
-                style={{ marginLeft: 15 }}
-                onClick={this.handleBatchBtnClick}
-                disabled={this.state.selectedKeys.length ? false : true}
-              >确定
-              </Button>
-            </div>
-          </Authorized>
-        }
-        {pagination &&
-          <div className={styles.pagination}>
-            <Pagination
-              {...paginationProps}
-              showTotal={(total) => `共 ${total} 条`}
-            />
-          </div>
-        }
-      </div>
-    );
-  }
   render() {
     const { selectedKeys } = this.state;
     const { component, dataSource, loading, header, footer, grid, onDelete } = this.props;
@@ -339,6 +337,7 @@ class StandardCardList extends PureComponent {
     const listHeader = header ? this.getListHeader() : false;
     return (
       <List
+        className={styles.list}
         grid={grid}
         bordered={false}
         split={false}
@@ -350,7 +349,7 @@ class StandardCardList extends PureComponent {
           <List.Item>
             {generator({
               onDelete,
-              selected: -1 === selectedKeys.indexOf(item.id) ? false : true,
+              selected: selectedKeys.indexOf(item.id) !== -1,
               onSelectChange: this.handleItemSelectChange,
               ...item,
             })(component)}

+ 11 - 6
src/components/RBList/StandardCardList.less

@@ -30,12 +30,17 @@
   }
 }
 
-:global {
-  .ant-list-footer {
-    background-color: #dadada;
-  }
-  .ant-list-something-after-last-item .ant-list-item:last-child {
-    border-bottom: 0;
+.list {
+  :global {
+    .ant-list-header {
+      padding-top: 0;
+    }
+    .ant-list-footer {
+      background-color: #dadada;
+    }
+    .ant-list-something-after-last-item .ant-list-item:last-child {
+      border-bottom: 0;
+    }
   }
 }
 

+ 313 - 29
src/components/RBList/StandardTableList.js

@@ -1,62 +1,346 @@
-import React, { PureComponent } from 'react';
-import { Table } from 'antd';
+import React, { PureComponent, Fragment } from 'react';
+import { Alert, Table, Select, Input, Button, Checkbox, Pagination } from 'antd';
+import PropTypes from 'prop-types';
+import Authorized from '../../utils/Authorized';
+import styles from './StandardTableList.less';
+
+function getSearchField(options) {
+  if (options && options.keys && options.keys.length) {
+    return options.keys[0].field;
+  }
+}
 
 export default class StandardTableList extends PureComponent {
+  static defaultProps = {
+    keepUIState: {},
+    loading: false,
+    dataSource: [],
+    columns: [],
+    header: false,
+    footer: false,
+    showStatusSelect: true,
+  }
+  static propTypes = {
+    loading: PropTypes.bool,
+    dataSource: PropTypes.array,
+    columns: PropTypes.array,
+    header: PropTypes.oneOfType([
+      PropTypes.object,
+      PropTypes.bool,
+    ]),
+    footer: PropTypes.oneOfType([
+      PropTypes.object,
+      PropTypes.bool,
+    ]),
+    showStatusSelect: PropTypes.bool,
+  }
   constructor(props) {
     super(props);
+    const {
+      keepUIState,
+      header: {
+        basicSearch,
+      },
+      footer: {
+        pagination,
+      },
+    } = this.props;
     this.state = {
-      selectedRowKeys: [],
-    }
+      batchActionKey: keepUIState.batchActionKey,
+      isAdvancedLevel: keepUIState.isAdvancedLevel || false,
+      searchSelectKey: keepUIState.searchSelectKey || getSearchField(basicSearch),
+      searchInputValue: keepUIState.searchInputValue || '',
+      selectedKeys: keepUIState.selectedKeys || [],
+      selectedStatusKey: keepUIState.selectedStatusKey || 'ALL',
+      pageNo: keepUIState.pageNo || pagination.pageNo || 1,
+      pageSize: keepUIState.pageSize || pagination.pageSize || 15,
+      totalSize: keepUIState.totalSize || pagination.totalSize || 0,
+    };
   }
   componentWillReceiveProps(nextProps) {
-    if (nextProps.selectedRows.length === 0) {
-      this.setState({
-        selectedRowKeys: [],
-      });
+    if (nextProps.footer) {
+      const { pagination } = nextProps.footer;
+      const { pageNo, pageSize, totalSize } = pagination;
+      this.setState({ pageNo, pageSize, totalSize });
     }
   }
-  handleRowSelectChange = (selectedRowKeys, selectedRows) => {
-    if (this.props.onSelectRow) {
-      this.props.onSelectRow(selectedRows);
+  getListHeader = () => {
+    const {
+      showStatusSelect,
+      header: { basicSearch, advancedSearch, onCreateClick },
+      footer: { pagination },
+    } = this.props;
+    const { keys } = basicSearch;
+    return (
+      <div className={styles.header}>
+        <div className={styles.headerSearch}>
+          <div className={styles.basicSearch}>
+            <div className={styles.left}>
+              {showStatusSelect &&
+                <Select
+                  onChange={this.handleStatusChange}
+                  value={this.state.selectedStatusKey}
+                  style={{ width: '20%', marginRight: 10 }}
+                >
+                  <Select.Option key="all" value="ALL">全部状态</Select.Option>
+                  <Select.Option key="normal" value="NORMAL">正常</Select.Option>
+                  <Select.Option key="delete" value="DEL">已删除</Select.Option>
+                </Select>
+              }
+              <Input.Search
+                value={this.state.searchInputValue}
+                style={{ width: '75%' }}
+                addonBefore={
+                  <Select
+                    placeholder="请选择"
+                    value={this.state.searchSelectKey}
+                    onChange={this.handleSearchSelectChange}
+                  >
+                    {keys.map(item => (
+                      <Select.Option
+                        key={item.field}
+                        value={item.field}
+                      >
+                        {item.name}
+                      </Select.Option>))}
+                  </Select>
+                }
+                placeholder="请输入"
+                enterButton
+                onChange={this.handleInputChange}
+                onSearch={this.handleSearchBtnClick}
+              />
+            </div>
+            <div className={styles.right}>
+              {advancedSearch &&
+                (this.state.isAdvancedLevel ?
+                  <a className={styles.searchLevel}>普通搜索</a> :
+                  <a className={styles.searchLevel}>高级搜索</a>
+                )
+              }
+              <Button icon="sync" onClick={this.handleRefreshBtnClick}>刷新</Button>
+              {/* noCreate 参数控制是否显示新建按钮 */}
+              {onCreateClick !== undefined &&
+                <Authorized authority="root" noMatch={null}>
+                  <Button
+                    icon="plus"
+                    type="primary"
+                    style={{ marginLeft: 5 }}
+                    onClick={onCreateClick}
+                  >新建
+                  </Button>
+                </Authorized>
+              }
+            </div>
+          </div>
+          {advancedSearch &&
+            this.state.isAdvancedLevel ?
+              <div className={styles.advancedSearch}>{advancedSearch}</div>
+            : null
+          }
+        </div>
+        <Alert
+          message={(
+            <Fragment>
+              已选择 <a style={{ fontWeight: 600 }}>{this.state.selectedKeys.length}</a> 项&nbsp;&nbsp;
+              总计 <a style={{ fontWeight: 600 }}>{pagination.totalSize}</a> 项&nbsp;&nbsp;
+            </Fragment>
+          )}
+          type="info"
+          showIcon
+        />
+      </div>
+    );
+  }
+  getListFooter = () => {
+    const { footer: { batchActions, pagination } } = this.props;
+    const paginationProps = {
+      total: this.state.totalSize,
+      current: this.state.pageNo,
+      pageSize: this.state.pageSize,
+      showSizeChanger: true,
+      showQuickJumper: true,
+      onChange: this.handleListPageChange,
+      onShowSizeChange: this.handleListPageSizeChange,
+    };
+    return (
+      <div className={styles.footer}>
+        {batchActions && (
+        <Authorized authority="root" noMatch={null}>
+          <div className={styles.batch}>
+            <Select
+              style={{ width: 100 }}
+              placeholder="请选择..."
+              value={this.state.batchActionKey}
+              onChange={this.handleBatchActionSelectChange}
+            >
+              {batchActions.map(item => (
+                <Select.Option key={item.key} value={item.key}>
+                  {item.name}
+                </Select.Option>))}
+            </Select>
+            <Button
+              type="primary"
+              style={{ marginLeft: 15 }}
+              onClick={this.handleBatchBtnClick}
+              disabled={!this.state.selectedKeys.length}
+            >确定
+            </Button>
+          </div>
+        </Authorized>)}
+        {pagination && (
+        <div className={styles.pagination}>
+          <Pagination
+            {...paginationProps}
+            showTotal={total => `共 ${total} 条`}
+          />
+        </div>)}
+      </div>
+    );
+  }
+  // 过滤
+  handleFilterOperation = (kv) => {
+    const {
+      header: {
+        onFilterClick,
+      },
+    } = this.props;
+    const {
+      searchSelectKey,
+      searchInputValue,
+      selectedStatusKey,
+      pageNo,
+      pageSize,
+    } = this.state;
+    const queryParams = {
+      pageNo,
+      pageSize,
+      [searchSelectKey]: searchInputValue,
+      status: selectedStatusKey,
+      ...kv,
+    };
+    if (queryParams.status === 'ALL') {
+      delete queryParams.status;
     }
-    this.setState({ selectedRowKeys });
+    onFilterClick(queryParams, this.state);
+  }
+  // 单选/取消单选
+  handleItemSelectChange = (itemId, checked) => {
+    const { selectedKeys } = this.state;
+    const newSelectedKeys = [...selectedKeys];
+    if (checked) {
+      this.setState({ selectedKeys: [...newSelectedKeys, itemId] });
+    } else {
+      this.setState({ selectedKeys: newSelectedKeys.filter(a => a !== itemId) });
+    }
+  }
+  // 刷新页面,重置筛选参数
+  cleanFilterParams = () => {
+    const { header: { basicSearch } } = this.props;
+    this.setState({
+      batchActionKey: undefined,
+      isAdvancedLevel: false,
+      searchSelectKey: getSearchField(basicSearch),
+      selectedKeys: [],
+      searchInputValue: '',
+      allChecked: false,
+      selectedStatusKey: 'ALL',
+    });
+  }
+  // 过滤状态
+  handleStatusChange = (value) => {
+    this.setState({
+      selectedStatusKey: value,
+    }, () =>
+      this.handleFilterOperation({ status: value })
+    );
+  }
+  // 选择搜索字段
+  handleSearchSelectChange = (value) => {
+    this.setState({ searchSelectKey: value });
+  }
+  // 响应input输入
+  handleInputChange = (e) => {
+    this.setState({ searchInputValue: e.target.value });
+  }
+  // 筛选搜索操作
+  handleSearchBtnClick = (value) => {
+    const { searchSelectKey } = this.state;
+    this.setState({
+      searchInputValue: value,
+    }, () =>
+      this.handleFilterOperation({ [searchSelectKey]: value })
+    );
+  }
+  // 刷新操作
+  handleRefreshBtnClick = () => {
+    this.handleFilterOperation({});
+  }
+  // list pageNo变化
+  handleListPageChange = (page, pageSize) => {
+    this.setState({
+      pageSize,
+      pageNo: page,
+    }, () => {
+      this.handleFilterOperation({
+        pageSize,
+        pageNo: page,
+      });
+    });
+  }
+  // list pageSize变化
+  handleListPageSizeChange = (current, size) => {
+    this.setState({
+      pageSize: size,
+      pageNo: current,
+    }, () => {
+      this.handleFilterOperation({
+        pageSize: size,
+        pageNo: current,
+      });
+    });
   }
-  handleTableChange = (pagination, filters, sorter) => {
-    this.props.onChange(pagination, filters, sorter);
+  // 选择批量处理类型
+  handleBatchActionSelectChange = (value) => {
+    this.setState({ batchActionKey: value });
   }
-  cleanSelectedKeys = () => {
-    this.handleRowSelectChange([], []);
+  // 批量处理操作
+  handleBatchBtnClick = () => {
+    const { footer: { onBatchClick } } = this.props;
+    const { batchActionKey, selectedKeys } = this.state;
+    onBatchClick(batchActionKey, selectedKeys);
+  }
+  handleRowSelectChange = (selectedKeys) => {
+    this.setState({ selectedKeys });
   }
 
   render() {
     const {
       loading,
       columns,
-      onChange,
       dataSource,
-      pagination,
+      header,
+      footer,
     } = this.props;
-    const { selectedRowKeys } = this.state;
-    const paginationProps = {
-      showSizeChanger: true,
-      showQuickJumper: true,
-      ...pagination,
-    };
+    const listFooter = footer ? this.getListFooter : false;
+    const listHeader = header ? this.getListHeader : false;
+    const { selectedKeys } = this.state;
     const rowSelection = {
-      selectedRowKeys,
+      selectedKeys,
       onChange: this.handleRowSelectChange,
-      getCheckboxProps: record => ({
-        disabled: record.disabled,
-      }),
     };
     return (
       <Table
+        bordered={false}
+        title={listHeader}
+        footer={listFooter}
         loading={loading}
         rowKey={record => record.key}
         rowSelection={rowSelection}
         columns={columns}
         dataSource={dataSource}
-        pagination={paginationProps}
+        pagination={false}
+        className={styles.table}
       />
     );
   }

+ 67 - 0
src/components/RBList/StandardTableList.less

@@ -0,0 +1,67 @@
+@import "~antd/lib/style/themes/default.less";
+
+.table {
+  :global {
+    .ant-table-title {
+      padding: 0 0 16px 0;
+    }
+    .ant-table-footer {
+      padding: 10px;
+    }
+    .ant-table-tbody > tr > td {
+      padding: 5px;
+    }
+    .ant-table-thead > tr > th {
+      padding: 10px 5px;
+    }
+  }
+}
+
+.header {
+  .headerSearch {
+    margin-bottom: 10px;
+    .basicSearch {
+      height: 35px;
+      .left {
+        width: 50%;
+        float: left;
+      }
+      .right {
+        float: right;
+      }
+    }
+  }
+}
+
+.footer {
+  height: 35px;
+  line-height: 35px;
+  vertical-align: middle;
+  .batch {
+    float: left;
+    margin-left: 8px;
+  }
+  .pagination {
+    float: right;
+    margin-right: 15px;
+  }
+}
+
+.searchLevel {
+  margin-right: 10px;
+}
+
+@media (min-width: 1200px) {
+  :global {
+    .ant-col-xl-4 {
+      width: 20%;
+    }
+  }
+}
+@media (min-width: 1600px) {
+  :global {
+    .ant-col-xxl-2 {
+      width: 10%;
+    }
+  }
+}

+ 0 - 0
src/components/RBRemoteSelect/index.js


+ 20 - 29
src/components/RBUpload/index.js

@@ -1,5 +1,4 @@
 import React, { Component } from 'react';
-import PropTypes from 'prop-types';
 import { Upload, Icon, message, Modal } from 'antd';
 import { queryOssSignature } from '../../services/resource';
 import { fileMaxSize } from '../../utils/config';
@@ -8,7 +7,7 @@ import styles from './index.less';
 
 const Message = message;
 
-//允许上传的文件类型
+// 允许上传的文件类型
 const renderAccept = (accept) => {
   if (!accept) {
     return null;
@@ -20,16 +19,16 @@ const renderAccept = (accept) => {
     return 'application/zip,application/x-zip,application/x-zip-compressed';
   }
   return `.${accept}`;
-}
+};
 
-//检查文件大小
+// 检查文件大小
 const checkFileSize = (fileName, fileSize) => {
-  if (fileSize > fileMaxSize*1000*1000) {
+  if (fileSize > fileMaxSize * 1000 * 1000) {
     Message.error(`${fileName}文件大小超过${fileSize}M!`);
     return false;
   }
   return true;
-}
+};
 
 /**
  * 将传入文件对象处理成upload组件格式
@@ -50,28 +49,21 @@ const renderFileList = (fileList) => {
       type: file.type,
       status: 'done',
       uid: index,
-      processed: true, //标记是否图片已处理
+      processed: true, // 标记是否图片已处理
     };
   });
-}
+};
 
 class Uploader extends Component {
-  static propTypes = {
-    fileList: PropTypes.array,
-    accept: PropTypes.string,
-    totalLimit: PropTypes.number,
-    multiple: PropTypes.bool,
-    onUpload: PropTypes.func,
-  };
   constructor(props) {
     super(props);
     this.state = {
-      ossSign: {}, //保存oss签名
-      fileList: renderFileList(this.props.fileList), //文件对象列表
+      ossSign: {}, // 保存oss签名
+      fileList: renderFileList(this.props.fileList), // 文件对象列表
       curFileName: '',
       previewImg: '',
       previewVisiable: false,
-    }
+    };
   }
   handleBeforeUpload = (file) => {
     const { fileCode } = this.props;
@@ -84,10 +76,10 @@ class Uploader extends Component {
           ossSign: res.data || res,
           curFileName: fileCode ?
             fileCode.replace(/-/g, '/') :
-            file.name.replace(/-/g, '/'), //处理图片文件名
+            file.name.replace(/-/g, '/'), // 处理图片文件名
         });
         if (res.data) {
-          setSignature(res.data); //保存签名到本地
+          setSignature(res.data); // 保存签名到本地
         }
       });
   }
@@ -97,7 +89,7 @@ class Uploader extends Component {
     const { dir, host } = ossSign;
     fileList = fileList.filter((file) => {
       if (file.status === 'error') {
-        Message.error(`${file.name}上传失败!`); //上传失败给出提示并去掉
+        Message.error(`${file.name}上传失败!`); // 上传失败给出提示并去掉
         return false;
       }
       if (file.status === 'done' && !file.processed) {
@@ -112,10 +104,9 @@ class Uploader extends Component {
       }
       return true;
     });
-
     this.setState({
       fileList: renderFileList(fileList),
-    })
+    });
     const uploaded = fileList.filter((file) => {
       if (file.status === 'done' && file.processed) {
         return true;
@@ -151,14 +142,14 @@ class Uploader extends Component {
     });
   }
   render() {
-    const { multiple, accept, totalLimit, forbidden, fileCode } = this.props;
+    const { multiple, accept, totalLimit, forbidden } = this.props;
     const { ossSign, fileList, curFileName, previewImg, previewVisiable } = this.state;
     const { host, policy, accessid, signature, dir } = ossSign;
 
     const uploadProps = {
       action: host,
       headers: {
-        'Authorization': `OSS${accessid}:${signature}`,
+        Authorization: `OSS${accessid}:${signature}`,
       },
       data: {
         policy,
@@ -169,7 +160,7 @@ class Uploader extends Component {
       },
       fileList,
       multiple,
-      disabled: forbidden || this.state.fileList.length >= totalLimit ? true : false,
+      disabled: !!(forbidden || this.state.fileList.length >= totalLimit),
       accept: renderAccept(accept),
       listType: 'picture',
       beforeUpload: this.handleBeforeUpload,
@@ -182,10 +173,10 @@ class Uploader extends Component {
       <div>
         <Upload.Dragger {...uploadProps}>
           <p className={styles.dragIcon}>
-            <Icon type="inbox"/>
+            <Icon type="inbox" />
           </p>
           <p className={styles.dragText}>点击或者拖拽图片到此区域进行上传</p>
-          <p className={styles.dragHint}>{`支持jpg/png图片格式,大小需小于2M`}</p>
+          <p className={styles.dragHint}>支持jpg/png图片格式,大小需小于2M</p>
         </Upload.Dragger>
         <Modal
           visible={previewVisiable}
@@ -193,7 +184,7 @@ class Uploader extends Component {
           onCancel={this.handleCancelPreview}
           width={650}
         >
-          <img src={previewImg} alt="" style={{ width: '100%', marginTop: 20 }}/>
+          <img src={previewImg} alt="" style={{ width: '100%', marginTop: 20 }} />
         </Modal>
       </div>
     );

+ 90 - 0
src/components/RBVideoPlayer/index.js

@@ -0,0 +1,90 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import Hls from 'hls.js';
+import styles from './index.less';
+
+export default class RBVideoPlayer extends PureComponent {
+  static propTypes = {
+    url : PropTypes.string.isRequired,
+    autoplay : PropTypes.bool,
+    hlsConfig : PropTypes.object, //https://github.com/dailymotion/hls.js/blob/master/API.md#fine-tuning
+    controls : PropTypes.bool,
+    width : PropTypes.oneOfType([
+      PropTypes.number,
+      PropTypes.string,
+    ]),
+    height : PropTypes.oneOfType([
+      PropTypes.number,
+      PropTypes.string,
+    ]),
+    poster : PropTypes.string,
+    videoProps : PropTypes.object,
+  };
+  static defaultProps = {
+    autoplay : false,
+    hlsConfig : {},
+    controls : true,
+    width : 500,
+    height : 375,
+  };
+  constructor (props) {
+    super(props);
+    this.state = {
+      playerId : Date.now()
+    };
+    this.hls = null;
+  }
+  componentDidUpdate () {
+    if (this.props.isM3U8) {
+      this._initPlayer();
+    }
+  }
+  componentDidMount () {
+    if (this.props.isM3U8) {
+      this._initPlayer();
+    }
+  }
+  componentWillUnmount () {
+    if (this.hls) {
+      this.hls.destroy();
+    }
+  }
+  _initPlayer () {
+    if (this.hls) {
+      this.hls.destroy();
+    }
+    let { url, autoplay, hlsConfig } = this.props;
+    let { video : $video } = this.refs;
+    let hls = new Hls(hlsConfig);
+    hls.loadSource(url);
+    hls.attachMedia($video);
+    hls.on(Hls.Events.MANIFEST_PARSED, () => {
+      if (autoplay) {
+        $video.play();
+      }
+    });
+    this.hls = hls;
+  }
+
+  render () {
+    let { playerId } = this.state;
+    const { url, controls, width, height, poster, videoProps } = this.props;
+
+    return (
+      <div key={playerId} className={styles.playerArea}>
+        <video
+          ref="video"
+          className={styles.hlsVideo}
+          id={`react-hls-${playerId}`}
+          controls={controls}
+          width={width}
+          height={height}
+          src={url}
+          poster={poster}
+          {...videoProps}
+        >
+        </video>
+      </div>
+    );
+  }
+}

+ 8 - 0
src/components/RBVideoPlayer/index.less

@@ -0,0 +1,8 @@
+.playerArea {
+  height: 100%;
+}
+
+.hlsVideo {
+  background: black;
+  border: 2px solid #dadada;
+}

+ 3 - 0
src/components/SiderMenu/index.less

@@ -36,6 +36,9 @@
 :global {
   .ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title {
     padding: 0 16px !important;
+    .realbull + span {
+      opacity: 0;
+    }
   }
   .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected {
     background-color: #00c1de !important;

+ 9 - 0
src/index.less

@@ -28,4 +28,13 @@ body {
   .ant-spin-container {
     overflow: visible !important;
   }
+  .ant-card-body {
+    padding: 16px !important;
+  }
+  .ant-message {
+    width: unset !important;
+    top: 50px !important;
+    right: 10px;
+    left: unset !important;
+  }
 }

+ 1 - 1
src/layouts/BasicLayout.js

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { Layout, message } from 'antd';
 import DocumentTitle from 'react-document-title';
 import { connect } from 'dva';
-import { Route, Redirect, Switch, routerRedux } from 'dva/router';
+import { Route, Redirect, Switch } from 'dva/router';
 import { ContainerQuery } from 'react-container-query';
 import classNames from 'classnames';
 import { enquireScreen } from 'enquire-js';

+ 96 - 0
src/models/campus.js

@@ -0,0 +1,96 @@
+import { message } from 'antd';
+import { routerRedux } from 'dva/router';
+import {
+  queryCampusList,
+  queryCampusItem,
+  createCampusItem,
+  updateCampusItem,
+  deleteCampusItem,
+} from '../services/campus';
+
+export default {
+  namespace: 'campus',
+
+  state: {
+    list: [],
+    pageNo: 1,
+    pageSize: 15,
+    totalSize: 0,
+    currentItem: {},
+  },
+
+  effects: {
+    *fetchCampusList({ payload }, { call, put }) {
+      const response = yield call(queryCampusList, payload);
+      if (response.success) {
+        message.success('加载校区列表成功');
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: response.data.list || [],
+            pageSize: response.data.pageSize,
+            totalSize: response.data.totalSize,
+            pageNo: response.data.pageNo,
+          },
+        });
+      }
+    },
+    *fetchCampusItem({ payload }, { call, put }) {
+      const response = yield call(queryCampusItem, payload);
+      if (response.success) {
+        message.success('加载校区详情成功');
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            currentItem: response.data || {},
+          }
+        });
+      }
+    },
+    *createCampusItem({ payload, state }, { call, put }) {
+      const response = yield call(createCampusItem, payload);
+      if (response.success) {
+        message.success('创建校区成功');
+        yield put(routerRedux.push({
+          state,
+          pathname: '/campus/list',
+        }));
+      }
+    },
+    *deleteCampusItem({ payload, states }, { call, put }) {
+      const response = yield call(deleteCampusItem, payload);
+      if (response.success) {
+        message.success('删除校区成功');
+        yield put({
+          type: 'fetchCampusList',
+          payload: states.Queryers,
+        });
+      }
+    },
+    *updateCampusItem({ payload, states }, { call, put }) {
+      const response = yield call(updateCampusItem, payload);
+      if (response.success) {
+        message.success('修改校区成功');
+        yield put(routerRedux.push({
+          pathname: '/campus/list',
+          state: states,
+        }));
+      }
+    },
+  },
+
+  reducers: {
+    querySuccess(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+    cleanItemState(state) {
+      return {
+        ...state,
+        currentItem: {},
+      };
+    }
+  },
+};

+ 4 - 4
src/models/error.js

@@ -10,28 +10,28 @@ export default {
 
   effects: {
     *query403(_, { call, put }) {
-      yield call(query403);
+      // yield call(query403);
       yield put({
         type: 'trigger',
         payload: '403',
       });
     },
     *query401(_, { call, put }) {
-      yield call(query401);
+      // yield call(query401);
       yield put({
         type: 'trigger',
         payload: '401',
       });
     },
     *query500(_, { call, put }) {
-      yield call(query500);
+      // yield call(query500);
       yield put({
         type: 'trigger',
         payload: '500',
       });
     },
     *query404(_, { call, put }) {
-      yield call(query404);
+      // yield call(query404);
       yield put({
         type: 'trigger',
         payload: '404',

+ 2 - 2
src/models/global.js

@@ -1,4 +1,4 @@
-//import { queryNotices } from '../services/api';
+// import { queryNotices } from '../services/api';
 
 export default {
   namespace: 'global',
@@ -9,7 +9,7 @@ export default {
   },
 
   effects: {
-    *fetchNotices(_, { call, put }) {
+    *fetchNotices(_, { put }) {
       // TODO 此功能占位,暂不提供通知功能
       // const data = yield call(queryNotices);
       const data = [];

+ 107 - 0
src/models/merchant.js

@@ -0,0 +1,107 @@
+import { message } from 'antd';
+import { routerRedux } from 'dva/router';
+import {
+  queryMerchantList,
+  queryMerchantItem,
+  createMerchantItem,
+  updateMerchantItem,
+  deleteMerchantItem,
+  despoitMerchantItem,
+} from '../services/merchant';
+
+export default {
+  namespace: 'merchant',
+
+  state: {
+    list: [],
+    pageNo: 1,
+    pageSize: 15,
+    totalSize: 0,
+    currentItem: {},
+  },
+
+  effects: {
+    *fetchMerchantList({ payload }, { call, put }) {
+      const response = yield call(queryMerchantList, payload);
+      if (response.success) {
+        message.success('加载商户列表成功');
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: response.data.list || [],
+            pageSize: response.data.pageSize,
+            totalSize: response.data.totalSize,
+            pageNo: response.data.pageNo,
+          },
+        });
+      }
+    },
+    *fetchMerchantItem({ payload }, { call, put }) {
+      const response = yield call(queryMerchantItem, payload);
+      if (response.success) {
+        message.success('加载商户详情成功');
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            currentItem: response.data || {},
+          }
+        });
+      }
+    },
+    *createMerchantItem({ payload, state }, { call, put }) {
+      const response = yield call(createMerchantItem, payload);
+      if (response.success) {
+        message.success('创建商户成功');
+        yield put(routerRedux.push({
+          state,
+          pathname: '/merchant/list',
+        }));
+      }
+    },
+    *deleteMerchantItem({ payload, states }, { call, put }) {
+      const response = yield call(deleteMerchantItem, payload);
+      if (response.success) {
+        message.success('删除商户成功');
+        yield put({
+          type: 'fetchMerchantList',
+          payload: states.Queryers,
+        });
+      }
+    },
+    *updateMerchantItem({ payload, states }, { call, put }) {
+      const response = yield call(updateMerchantItem, payload);
+      if (response.success) {
+        message.success('修改商户成功');
+        yield put(routerRedux.push({
+          pathname: '/merchant/list',
+          state: states,
+        }));
+      }
+    },
+    *despoitMerchantItem({ payload, states }, { call, put }) {
+      const response = yield call(despoitMerchantItem, payload);
+      if (response.success) {
+        message.success('账户充值成功');
+        yield put(routerRedux.push({
+          pathname: '/merchant/list',
+          state: states,
+        }));
+      }
+    }
+  },
+
+  reducers: {
+    querySuccess(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+    cleanItemState(state) {
+      return {
+        ...state,
+        currentItem: {},
+      };
+    }
+  },
+};

+ 31 - 8
src/models/resource.js

@@ -2,6 +2,7 @@ import { routerRedux } from 'dva/router';
 import { message } from 'antd';
 import {
   createImage,
+  deleteImage,
   queryImageResource,
   queryVideoResource,
 } from '../services/resource';
@@ -11,15 +12,16 @@ export default {
 
   state: {
     list: [],
-    pageNo: null,
-    pageSize: null,
-    totalSize: null,
+    pageNo: 1,
+    pageSize: 15,
+    totalSize: 0,
   },
 
   effects: {
     *fetchImageList({ payload }, { call, put }) {
       const response = yield call(queryImageResource, payload);
       if (response.success) {
+        message.success('加载图片列表成功');
         yield put({
           type: 'querySuccess',
           payload: {
@@ -27,16 +29,37 @@ export default {
             pageSize: response.data.pageSize,
             totalSize: response.data.totalSize,
             pageNo: response.data.pageNo,
-          }
+          },
+        });
+      }
+    },
+    *fetchVideoList({ payload }, { call, put }) {
+      const response = yield call(queryVideoResource, payload);
+      if (response.success) {
+        message.success('加载视频列表成功');
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: response.data.list || [],
+            pageSize: response.data.pageSize,
+            totalSize: response.data.totalSize,
+            pageNo: response.data.pageNo,
+          },
         });
       }
     },
     *createImage({ payload }, { call, put }) {
       const response = yield call(createImage, payload);
       if (response.success) {
-        message.success('图片创建成功');
+        message.success('创建成功');
         yield put(routerRedux.goBack());
       }
+    },
+    *deleteImage({ payload }, { call, put }) {
+      const response = yield call(deleteImage, payload);
+      if (response && response.success) {
+        message.success('删除成功');
+      }
     }
   },
 
@@ -46,6 +69,6 @@ export default {
         ...state,
         ...action.payload,
       };
-    }
-  }
-}
+    },
+  },
+};

+ 32 - 0
src/routes/Campus/Campus.js

@@ -0,0 +1,32 @@
+import React, { Component } from 'react';
+import { Route, Switch } from 'dva/router';
+import { connect } from 'dva';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import { getRoutes } from '../../utils/utils';
+
+@connect()
+export default class Campus extends Component {
+  render() {
+    const { match, routerData, location } = this.props;
+    const routes = getRoutes(match.path, routerData);
+
+    return (
+      <PageHeaderLayout>
+        <Switch>
+          {
+            routes.map(item =>
+              (
+                <Route
+                  key={item.key}
+                  path={item.path}
+                  component={item.component}
+                  exact={item.exact}
+                />
+              )
+            )
+          }
+        </Switch>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 176 - 0
src/routes/Campus/CampusList.js

@@ -0,0 +1,176 @@
+import React, { Component, Fragment } from 'react';
+import moment from 'moment';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Modal, Button, message } from 'antd';
+import { StandardTableList } from '../../components/RBList';
+import { renderCategory, addRowKey } from '../../utils/utils';
+import styles from './CampusList.less';
+
+const Message = message;
+
+@connect(({loading, campus}) => ({
+  campus,
+  loading: loading.models.campus,
+}))
+export default class CampusListPage extends Component {
+  constructor(props) {
+    super(props);
+    const { state } = props.location;
+    this.state = {
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+    };
+  }
+  componentDidMount() {
+    this.props.dispatch({
+      type: 'campus/fetchCampusList',
+      payload: {...this.state.Queryers},
+    });
+  }
+  handleCreateOperation = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/campus/create',
+      state: this.state,
+    }));
+  }
+  handleEditOperation = (item) => {
+    this.props.dispatch(routerRedux.push({
+      pathname: `/campus/edit/${item.id}`,
+      state: this.state,
+    }));
+  }
+  handleFilterOperation = (params, states) => {
+    this.props.dispatch({
+      type: 'campus/fetchCampusList',
+      payload: params,
+    });
+    this.setState({
+      UIParams: states,
+      Queryers: params,
+    });
+  }
+  handleBatchOperation = () => {
+    Message.info('暂不支持批量操作!');
+  }
+
+  render() {
+    const { loading, campus } = this.props;
+    const { list, totalSize, pageSize, pageNo } = campus;
+
+    const renderOperation = (item) => {
+      return (
+        <div>
+          <Button
+            size="small"
+            className={styles.editBtn}
+            onClick={() => this.handleEditOperation(item)}
+          >编辑
+          </Button>
+        </div>
+      );
+    }
+
+    const batchActions = [{
+      key: 'delete',
+      name: '批量删除',
+    }, {
+      key: 'recovery',
+      name: '批量恢复',
+    }];
+    const basicSearch = {
+      keys: [{
+        name: '校区编号',
+        field: 'code',
+      }, {
+        name: '校区名称',
+        field: 'name',
+      }],
+    };
+    const pagination = {
+      pageNo,
+      pageSize,
+      totalSize,
+    };
+    const columns = [{
+      title: '校区编号',
+      key: 1,
+      dataIndex: 'code',
+      render: (text, record) => (
+        <a className={styles.link}
+          onClick={() => this.handleEditOperation(record)}
+        >
+          {text}
+        </a>
+      ),
+      width: '12%',
+    }, {
+      title: '校区名称',
+      key: 2,
+      dataIndex: 'name',
+      render: (text, record) => (
+        <a className={styles.link}
+          onClick={() => this.handleEditOperation(record)}
+        >
+          {text}
+        </a>
+      ),
+      width: '34%',
+    }, {
+      title: '所属渠道',
+      key: 3,
+      dataIndex: 'merchantName',
+      width: '10%',
+    }, {
+      title: '校区联系人',
+      key: 4,
+      dataIndex: 'contactName',
+      width: '10%',
+    }, {
+      title: '联系电话',
+      key: 5,
+      dataIndex: 'mobile',
+      width: '12%',
+    }, {
+      title: '更新时间',
+      key: 6,
+      dataIndex: 'gmtModified',
+      render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+      width: '16%',
+    }, {
+      title: '操作',
+      key: 7,
+      dataIndex: 'operation',
+      render: (_, record) => renderOperation(record),
+      width: '6%',
+    }];
+    const renderAdvancedSearch = () => {
+      return (
+        <div>测试
+        </div>
+      );
+    }
+    return (
+      <Fragment>
+        <StandardTableList
+          columns={columns}
+          loading={loading}
+          dataSource={addRowKey(list)}
+          header={{
+            basicSearch,
+            advancedSearch: renderAdvancedSearch(),
+            onFilterClick: this.handleFilterOperation,
+            onCreateClick: this.handleCreateOperation,
+          }}
+          footer={{
+            pagination,
+            batchActions,
+            onBatchClick: this.handleBatchOperation,
+          }}
+          keepUIState={{...this.state.UIParams}}
+          showStatusSelect={false}
+        />
+      </Fragment>
+    );
+  }
+}

+ 16 - 0
src/routes/Campus/CampusList.less

@@ -0,0 +1,16 @@
+@import "~antd/lib/style/themes/default.less";
+
+.link {
+  font-weight: 500;
+}
+.editBtn {
+  margin-right: 10px;
+  background: @primary-5;
+  color: #fff;
+  font-weight: 500;
+}
+.deleteBtn {
+  background: #f5222d;
+  color: #fff;
+  font-weight: 500;
+}

+ 32 - 0
src/routes/Merchant/Merchant.js

@@ -0,0 +1,32 @@
+import React, { Component } from 'react';
+import { Route, Switch } from 'dva/router';
+import { connect } from 'dva';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import { getRoutes } from '../../utils/utils';
+
+@connect()
+export default class Merchant extends Component {
+  render() {
+    const { match, routerData, location } = this.props;
+    const routes = getRoutes(match.path, routerData);
+
+    return (
+      <PageHeaderLayout>
+        <Switch>
+          {
+            routes.map(item =>
+              (
+                <Route
+                  key={item.key}
+                  path={item.path}
+                  component={item.component}
+                  exact={item.exact}
+                />
+              )
+            )
+          }
+        </Switch>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 294 - 0
src/routes/Merchant/MerchantCreate.js

@@ -0,0 +1,294 @@
+import React, { PureComponent } from 'react';
+import pathToRegexp from 'path-to-regexp';
+import { Form, Input, Button, Radio, Switch } from 'antd';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import {
+  DOMAIN_CP,
+  DOMAIN_LJ,
+  DOMAIN_PJ,
+  STATUS_NORMAL,
+  STATUS_DELETE,
+} from '../../utils/config';
+import styles from './MerchantCreate.less';
+
+const fieldLabels = {
+  type: '商户类型',
+  name: '商户名称',
+  code: '商户编号',
+  contactName: '商户联系人',
+  mobile: '联系电话',
+  depositBank: '开户银行',
+  bankAccount: '银行账户',
+  licenseId: '营业执照',
+  taxNumber: '纳税人识别号',
+  receiptType: '发票类型',
+};
+const domains = [{
+  title: '平台方',
+  value: DOMAIN_LJ,
+}, {
+  title: '渠道商',
+  value: DOMAIN_PJ,
+}, {
+  title: '供应商',
+  value: DOMAIN_CP,
+}];
+const receipts = [{
+  title: '普通发票',
+  value: 'COMMON',
+}, {
+  title: '增值税发票',
+  value: 'SPECIAL',
+}];
+const formItemLayout = {
+  labelCol: {
+    xs: { span: 24 },
+    sm: { span: 6 },
+  },
+  wrapperCol: {
+    xs: { span: 24 },
+    sm: { span: 14 },
+    md: { span: 12 },
+  },
+};
+const submitFormLayout = {
+  wrapperCol: {
+    xs: { span: 24, offset: 0 },
+    sm: { span: 12, offset: 6 },
+  },
+};
+
+@Form.create()
+@connect(({loading, merchant}) => ({
+  merchant,
+  submitting: loading.models.merchant,
+}))
+export default class MerchantCreatePage extends PureComponent {
+  componentWillMount() {
+    //如果是添加商户操作,进入此页面时把state清一下
+    const { location, dispatch } = this.props;
+    const match = pathToRegexp('/merchant/create').exec(location.pathname);
+    if (match) {
+      this.cleanPageState();
+    }
+  }
+  componentDidMount() {
+    const matchId = this.isEdit();
+    if (matchId) {
+      this.props.dispatch({
+        type: 'merchant/fetchMerchantItem',
+        payload: {id: matchId},
+      });
+    }
+  }
+  isEdit = () => {
+    //根据路径判断操作类型并决定是否需加载该条目信息<create/edit>
+    const { location } = this.props;
+    const match = pathToRegexp('/merchant/edit/:id').exec(location.pathname);
+    if (match) {
+      return match[1];
+    }
+    return false;
+  }
+  cleanPageState = () => {
+    this.props.dispatch({
+      type: 'merchant/cleanItemState',
+      payload: {},
+    });
+  }
+  handlePageBack = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/merchant/list',
+      state: this.props.location.state,
+    }));
+  }
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+    this.props.form.validateFieldsAndScroll((err, values) => {
+      if (!err) {
+        const { status, ...rest } = values;
+        const matchId = this.isEdit();
+        if (status) {
+          rest.status = STATUS_NORMAL;
+        } else {
+          rest.status = STATUS_DELETE;
+        }
+        if (matchId) {
+          rest.id = matchId;
+          this.props.dispatch({
+            type: 'merchant/updateMerchantItem',
+            payload: rest,
+            states: this.props.location.state,
+          });
+        } else {
+          this.props.dispatch({
+            type: 'merchant/createMerchantItem',
+            payload: rest,
+            states: this.props.location.state,
+          });
+        }
+      }
+    });
+  }
+
+  render() {
+    const { submitting, merchant, form } = this.props;
+    const { getFieldDecorator } = form;
+    const { currentItem } = merchant;
+
+    const isChecked = (status) => {
+      if (status === STATUS_NORMAL) {
+        return true;
+      } else if (status === STATUS_DELETE) {
+        return false;
+      } else {
+        return true;
+      }
+    }
+
+    return (
+      <Form onSubmit={this.handlePageSubmit}>
+        <Form.Item label={fieldLabels.type} {...formItemLayout}>
+          {getFieldDecorator('domain', {
+            rules: [{ required: true, message: '请选择商户类型!' }],
+            initialValue: currentItem.domain || DOMAIN_PJ,
+          })(
+            <Radio.Group className={styles.radio}>
+              {
+                domains.map(item =>
+                  (
+                    <Radio.Button
+                      key={item.value}
+                      value={item.value}
+                    >{item.title}
+                    </Radio.Button>
+                  )
+                )
+              }
+            </Radio.Group>
+          )}
+        </Form.Item>
+        <Form.Item hasFeedback label={fieldLabels.code} {...formItemLayout}>
+          {getFieldDecorator('code', {
+            rules: [
+              {
+                required: true, message: '请给商户编号!',
+              }, {
+                pattern: /^[0-9]{4,6}$/g, message: '请输入4-6位数字编号!',
+              },
+            ],
+            initialValue: currentItem.code,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item hasFeedback label={fieldLabels.name} {...formItemLayout}>
+          {getFieldDecorator('name', {
+            rules: [{ required: true, message: '请给商户命名!' }],
+            initialValue: currentItem.name,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item hasFeedback label={fieldLabels.contactName} {...formItemLayout}>
+          {getFieldDecorator('contactName', {
+            rules: [{ required: true, message: '请填写商户联系人!' }],
+            initialValue: currentItem.contactName,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item hasFeedback label={fieldLabels.mobile} {...formItemLayout}>
+          {getFieldDecorator('mobile', {
+            rules: [
+              {
+                required: true, message: '请填写联系电话!',
+              }, {
+                pattern: /^[1][3,4,5,7,8][0-9]{9}$/g, message: '请输入11位有效手机号!',
+              }],
+            initialValue: currentItem.mobile,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item hasFeedback label={fieldLabels.depositBank} {...formItemLayout}>
+          {getFieldDecorator('depositBank', {
+            rules: [{
+              required: true, message: '开户银行不能为空!',
+            }],
+            initialValue: currentItem.depositBank,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item hasFeedback label={fieldLabels.bankAccount} {...formItemLayout}>
+          {getFieldDecorator('bankAccount', {
+            rules: [{
+              required: true, message: '银行账号不能为空!',
+            }, {
+              pattern: /^[0-9]{1,22}$/g, message: '请输入有效的银行账户!',
+            }],
+            initialValue: currentItem.bankAccount,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item label={fieldLabels.licenseId} {...formItemLayout}>
+          {getFieldDecorator('licenseId', {
+            initialValue: currentItem.licenseId,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item label={fieldLabels.taxNumber} {...formItemLayout}>
+          {getFieldDecorator('taxNumber', {
+            initialValue: currentItem.taxNumber,
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item label={fieldLabels.receiptType} {...formItemLayout}>
+          {getFieldDecorator('receiptType', {
+            initialValue: currentItem.receiptType || 'COMMON',
+          })(
+            <Radio.Group className={styles.radio}>
+              {
+                receipts.map(item =>
+                  (
+                    <Radio.Button
+                      key={item.value}
+                      value={item.value}
+                    >{item.title}
+                    </Radio.Button>
+                  )
+                )
+              }
+            </Radio.Group>
+          )}
+        </Form.Item>
+        <Form.Item label="状态" {...formItemLayout}>
+          {getFieldDecorator('status', {
+            valuePropName: 'checked',
+            initialValue: isChecked(currentItem.status),
+          })(
+            <Switch
+              checkedChildren="启用"
+              unCheckedChildren="不启用"
+            />
+          )}
+        </Form.Item>
+        <Form.Item {...submitFormLayout} style={{ marginTop: 32 }}>
+          <Button onClick={this.handlePageBack}>取消</Button>
+          <Button
+            type="primary"
+            htmlType="submit"
+            loading={submitting}
+            style={{ marginLeft: 8 }}
+          >提交
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  }
+}

+ 10 - 0
src/routes/Merchant/MerchantCreate.less

@@ -0,0 +1,10 @@
+@import "~antd/lib/style/themes/default.less";
+
+.radio {
+  :global {
+    .ant-radio-button-wrapper-checked {
+      background-color: @primary-5;
+      color: #fff;
+    }
+  }
+}

+ 110 - 0
src/routes/Merchant/MerchantDespoit.js

@@ -0,0 +1,110 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Form, Input, Button } from 'antd';
+import { toDecimal2 } from '../../utils/utils';
+import styles from './MerchantDespoit.less';
+
+const fieldLabels = {
+  code: '商户编号',
+  name: '商户名称',
+  balance: '账户余额',
+  quantity: '充值金额',
+  note: '备注',
+};
+const formItemLayout = {
+  labelCol: {
+    xs: { span: 24 },
+    sm: { span: 6 },
+  },
+  wrapperCol: {
+    xs: { span: 24 },
+    sm: { span: 14 },
+    md: { span: 12 },
+  },
+};
+const submitFormLayout = {
+  wrapperCol: {
+    xs: { span: 24, offset: 0 },
+    sm: { span: 12, offset: 6 },
+  },
+};
+
+@Form.create()
+@connect(({loading, merchant}) => ({
+  merchant,
+  submitting: loading.models.merchant,
+}))
+export default class MerchantDespoitPage extends PureComponent {
+  handlePageBack = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/merchant/list',
+      state: this.props.location.state,
+    }));
+  }
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+    this.props.form.validateFieldsAndScroll((err, values) => {
+      if (!err) {
+        const { note, quantity } = values;
+        const { id } = this.props.location.state.currentItem;
+        this.props.dispatch({
+          type: 'merchant/despoitMerchantItem',
+          payload: {
+            note,
+            quantity,
+            merchantId: id,
+          },
+          states: this.props.location.state,
+        });
+      }
+    });
+  }
+  render() {
+    const { location, form, submitting } = this.props;
+    const { currentItem } = location.state;
+    const { getFieldDecorator } = form;
+    return (
+      <Form onSubmit={this.handlePageSubmit}>
+        <Form.Item label={fieldLabels.name} {...formItemLayout}>
+          <span>{`${currentItem.name}【${currentItem.code}】`}</span>
+        </Form.Item>
+        <Form.Item label={fieldLabels.balance} {...formItemLayout}>
+          <span className={styles.balance}>{toDecimal2(currentItem.balance)}元</span>
+        </Form.Item>
+        <Form.Item label={fieldLabels.quantity} {...formItemLayout}>
+          {getFieldDecorator('quantity', {
+            rules: [
+              {
+                required: true, type: 'string', message: '充值金额不能为空!',
+              }, {
+                pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '充值金额格式错误!',
+              }
+            ],
+          })(
+            <Input placeholder="请输入充值金额" addonAfter="元" />
+          )}
+        </Form.Item>
+        <Form.Item label={fieldLabels.note} {...formItemLayout}>
+          {getFieldDecorator('note', {
+          })(
+            <Input placeholder="请输入" />
+          )}
+        </Form.Item>
+        <Form.Item {...submitFormLayout} style={{ marginTop: 32 }}>
+          <Button
+            onClick={this.handlePageBack}
+          >取消
+          </Button>
+          <Button
+            type="primary"
+            htmlType="submit"
+            loading={submitting}
+            style={{ marginLeft: 8 }}
+          >提交
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  }
+}

+ 6 - 0
src/routes/Merchant/MerchantDespoit.less

@@ -0,0 +1,6 @@
+@import "~antd/lib/style/themes/default.less";
+
+.balance {
+  color: red;
+  font-weight: 500;
+}

+ 211 - 0
src/routes/Merchant/MerchantList.js

@@ -0,0 +1,211 @@
+import React, { Component, Fragment } from 'react';
+import moment from 'moment';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Modal, Button, message } from 'antd';
+import { StandardTableList } from '../../components/RBList';
+import Authorized from '../../utils/Authorized';
+import { renderStatus, renderCategory, addRowKey } from '../../utils/utils';
+import styles from './MerchantList.less';
+
+const Message = message;
+
+@connect(({loading, merchant}) => ({
+  merchant,
+  loading: loading.models.merchant,
+}))
+export default class MerchantListPage extends Component {
+  constructor(props) {
+    super(props);
+    const { state } = props.location;
+    this.state = {
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+    };
+  }
+  componentDidMount() {
+    this.props.dispatch({
+      type: 'merchant/fetchMerchantList',
+      payload: {...this.state.Queryers},
+    });
+  }
+  handleCreateOperation = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/merchant/create',
+      state: this.state,
+    }));
+  }
+  handleDeleteOperation = (item) => {
+    Modal.confirm({
+      okText: '确定',
+      cancelText: '取消',
+      title: '你确定要删除这条商户记录吗?',
+      onOk: () => {
+        this.props.dispatch({
+          type: 'merchant/deleteMerchantItem',
+          payload: { id: item.id },
+          states: this.state,
+        });
+      },
+    });
+  }
+  handleEditOperation = (item) => {
+    this.props.dispatch(routerRedux.push({
+      pathname: `/merchant/edit/${item.id}`,
+      state: this.state,
+    }));
+  }
+  handleFilterOperation = (params, states) => {
+    this.props.dispatch({
+      type: 'merchant/fetchMerchantList',
+      payload: params,
+    });
+    this.setState({
+      UIParams: states,
+      Queryers: params,
+    });
+  }
+  handleDespoitOperation = (item) => {
+    this.props.dispatch(routerRedux.push({
+      pathname: `/merchant/despoit/${item.id}`,
+      state: {...this.state, currentItem: item},
+    }));
+  }
+  handleBatchOperation = () => {
+    Message.info('暂不支持批量操作!');
+  }
+
+  render() {
+    const { loading, merchant } = this.props;
+    const { list, totalSize, pageSize, pageNo } = merchant;
+
+    const renderOperation = (item) => {
+      return (
+        <div>
+          <Authorized authority="root" noMatch={null}>
+            <Button
+              size="small"
+              className={styles.despoitBtn}
+              onClick={() => this.handleDespoitOperation(item)}
+            >充值
+            </Button>
+          </Authorized>
+          <Button
+            size="small"
+            className={styles.editBtn}
+            onClick={() => this.handleEditOperation(item)}
+          >编辑
+          </Button>
+          <Button
+            size="small"
+            className={styles.deleteBtn}
+            onClick={() => this.handleDeleteOperation(item)}
+          >删除
+          </Button>
+        </div>
+      );
+    }
+
+    const batchActions = [{
+      key: 'delete',
+      name: '批量删除',
+    }, {
+      key: 'recovery',
+      name: '批量恢复',
+    }];
+    const basicSearch = {
+      keys: [{
+        name: '商户编号',
+        field: 'code',
+      }, {
+        name: '商户名称',
+        field: 'name',
+      }],
+    };
+    const pagination = {
+      pageNo,
+      pageSize,
+      totalSize,
+    };
+    const columns = [{
+      title: '商户编号',
+      key: 1,
+      dataIndex: 'code',
+      render: (text, record) => (
+        <a className={styles.link}
+          onClick={() => this.handleEditOperation(record)}
+        >
+          {text}
+        </a>
+      ),
+      width: '12%',
+    }, {
+      title: '商户名称',
+      key: 2,
+      dataIndex: 'name',
+      render: (text, record) => (
+        <a className={styles.link}
+          onClick={() => this.handleEditOperation(record)}
+        >
+          {text}
+        </a>
+      ),
+      width: '16%',
+    }, {
+      title: '商户类型',
+      key: 3,
+      dataIndex: 'domain',
+      render: (text) => renderCategory(text),
+      width: '11%',
+    }, {
+      title: '账户余额(¥)',
+      key: 4,
+      dataIndex: 'balance',
+      width: '12%',
+    }, {
+      title: '状态',
+      key: 5,
+      dataIndex: 'status',
+      render: (text) => renderStatus(text),
+      width: '11%',
+    }, {
+      title: '更新时间',
+      key: 6,
+      dataIndex: 'gmtModified',
+      render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+      width: '20%',
+    }, {
+      title: '操作',
+      key: 7,
+      dataIndex: 'operation',
+      render: (_, record) => renderOperation(record),
+      width: '18%',
+    }];
+    return (
+      <Fragment>
+        <StandardTableList
+          columns={columns}
+          loading={loading}
+          dataSource={addRowKey(list)}
+          header={{
+            basicSearch,
+            onFilterClick: this.handleFilterOperation,
+            onCreateClick: this.handleCreateOperation,
+          }}
+          footer={{
+            pagination,
+            batchActions,
+            onBatchClick: this.handleBatchOperation,
+          }}
+          keepUIState={{...this.state.UIParams}}
+        />
+        <Modal
+          title="账户充值"
+          cancelText="取消"
+          okText="充值"
+        >
+        </Modal>
+      </Fragment>
+    );
+  }
+}

+ 23 - 0
src/routes/Merchant/MerchantList.less

@@ -0,0 +1,23 @@
+@import "~antd/lib/style/themes/default.less";
+
+.link {
+  // text-decoration: underline;
+  font-weight: 500;
+}
+.editBtn {
+  margin-right: 10px;
+  background: @primary-5;
+  color: #fff;
+  font-weight: 500;
+}
+.despoitBtn {
+  margin-right: 10px;
+  background: #a0d911;
+  color: #fff;
+  font-weight: 500;
+}
+.deleteBtn {
+  background: #f5222d;
+  color: #fff;
+  font-weight: 500;
+}

+ 55 - 0
src/routes/Resource/PictureCardList.js

@@ -0,0 +1,55 @@
+import React from 'react';
+import { PictureItem } from '../../components/RBItem';
+import { StandardCardList } from '../../components/RBList';
+
+function pictureItemFormatter(originData) {
+  return originData.map((item) => {
+    return {
+      id: item.id,
+      picPath: item.path,
+      picCode: item.code,
+      picStatus: item.state,
+    };
+  });
+}
+
+function PictureCardList({
+  UIParams, dataSource, loading, totalSize, pageSize, pageNo,
+  onFilterClick, onCreateClick, onBatchClick, onDeleteClick,
+}) {
+  const batchActions = [{
+    key: 'delete',
+    name: '批量删除',
+  }, {
+    key: 'edit',
+    name: '批量编辑',
+  }];
+  const basicSearch = {
+    keys: [{
+      name: '图片编号',
+      field: 'code',
+    }, {
+      name: '图片名称',
+      field: 'name',
+    }],
+  };
+  const pagination = {
+    pageNo,
+    pageSize,
+    totalSize,
+  };
+  return (
+    <StandardCardList
+      loading={loading}
+      component={PictureItem}
+      keepUIState={{ ...UIParams }}
+      dataSource={pictureItemFormatter(dataSource)}
+      header={{basicSearch, onFilterClick, onCreateClick}}
+      footer={{pagination, batchActions, onBatchClick}}
+      grid={{ gutter: 16, xxl: 12, xl: 6, lg: 4, md: 3, sm: 2, xs: 1 }}
+      onDelete={onDeleteClick}
+    />
+  );
+}
+
+export default PictureCardList;

+ 1 - 1
src/routes/Resource/PictureCreate.js

@@ -21,7 +21,7 @@ export default class PictureCreate extends Component {
   }
 
   render() {
-    //tab面板列表
+    // tab面板列表
     const tabList = [{
       key: 'single',
       tab: '单图上传',

+ 9 - 12
src/routes/Resource/PictureCreateMultiple.js

@@ -1,19 +1,18 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'dva';
 import { routerRedux } from 'dva/router';
-import { Card, List, Input, Button, message } from 'antd';
+import { Input, Button, message } from 'antd';
 import Uploader from '../../components/RBUpload';
 import styles from './PictureCreateMultiple.less';
 
 const Message = message;
 
 @connect(({ loading }) => ({
-  submitting: loading.models.resource
+  submitting: loading.models.resource,
 }))
 export default class CreateMultiplePicture extends PureComponent {
   state = {
     fileList: [],
-    pictures: [],
   }
 
   handleUploadSuccess = (fileList) => {
@@ -50,7 +49,7 @@ export default class CreateMultiplePicture extends PureComponent {
       }
     });
     if (correct) {
-      const payload = fileList.map((file) => ({
+      const payload = fileList.map(file => ({
         name: file.name,
         code: file.code,
         format: file.type,
@@ -59,14 +58,12 @@ export default class CreateMultiplePicture extends PureComponent {
         url: file.url,
         status: 'NORMAL',
       }));
-      console.log('result:', payload);
       // this.props.dispatch({
       //   payload,
       //   type: 'resource/createImage',
       // });
     } else {
       Message.error('还有图片编号或名称未指定!');
-      return;
     }
   }
 
@@ -78,7 +75,7 @@ export default class CreateMultiplePicture extends PureComponent {
         <div key={`item-${index}`} className={styles.itemWrapper}>
           <div className={styles.item}>
             <div className={styles.imgWrapper}>
-              <img src={data.url}/>
+              <img src={data.url} alt="uploaded" />
             </div>
           </div>
           <div className={styles.inputWrapper}>
@@ -86,26 +83,26 @@ export default class CreateMultiplePicture extends PureComponent {
               value={data.code}
               style={{ width: '100%', marginTop: 2 }}
               placeholder="请输入图片编号"
-              onChange={(e) => this.handleCodeInputChange(index, e)}
+              onChange={e => this.handleCodeInputChange(index, e)}
             />
             <Input
               value={data.name && data.name.split('.')[0]}
               style={{ width: '100%', marginTop: 2 }}
               placeholder="请输入图片名称"
-              onChange={(e) => this.handleNameInputChange(index, e)}
+              onChange={e => this.handleNameInputChange(index, e)}
             />
           </div>
         </div>
       );
-    }
+    };
     return (
       <div className={styles.content}>
         <div className={styles.left}>
           <Uploader
             forbidden={false}
             fileList={fileList}
-            accept='image'
-            multiple={true}
+            accept="image"
+            multiple
             totalLimit={20}
             onUpload={this.handleUploadSuccess}
             onRemove={this.handleOnRemove}

+ 0 - 1
src/routes/Resource/PictureCreateMultiple.less

@@ -24,7 +24,6 @@
     line-height: 50px;
     vertical-align: middle;
     text-align: center;
-    border-top: 1px solid #e1e5e9;
   }
 }
 

+ 17 - 18
src/routes/Resource/PictureCreateSingle.js

@@ -1,14 +1,13 @@
 import React, { PureComponent, Fragment } from 'react';
-import { Card, Form, Input, Button, Switch, Alert } from 'antd';
+import { Form, Input, Button, Switch, Alert } from 'antd';
 import { connect } from 'dva';
 import { routerRedux } from 'dva/router';
 import Uploader from '../../components/RBUpload';
-import PageHeaderLayout from '../../layouts/PageHeaderLayout';
 import { STATUS_DELETE, STATUS_NORMAL } from '../../utils/config';
 
 @Form.create()
 @connect(({ loading }) => ({
-  submitting: loading.models.resource
+  submitting: loading.models.resource,
 }))
 export default class CreateOnePicture extends PureComponent {
   state = {
@@ -27,11 +26,11 @@ export default class CreateOnePicture extends PureComponent {
     return fileList;
   }
   handlePageBack = () => {
-    const UIParams = {};
-    const Queryers = {};
+    let UIParams = {};
+    let Queryers = {};
     if (this.props.location.state) {
-      UIParams = this.props.location.state.UIParams;
-      Queryers = this.props.location.state.Queryers;
+      UIParams = this.props.location.state.UIParams; //no-eslint-disable
+      Queryers = this.props.location.state.Queryers; //no-eslint-disable
     }
     this.props.dispatch(
       routerRedux.push({
@@ -39,7 +38,7 @@ export default class CreateOnePicture extends PureComponent {
         state: {
           UIParams,
           Queryers,
-        }
+        },
       })
     );
   }
@@ -91,9 +90,9 @@ export default class CreateOnePicture extends PureComponent {
             type="warning"
             message={
               <Fragment>
-                <p>{"1.上传图片前应先填写符合规范的图片编号和名称,否则不能上传图片;"}</p>
-                <p>{"2.上传成功后会自动生成图片大小、格式、路径等信息,无需手动填写;"}</p>
-                <p>{"3.图片一旦创建完成,再次编辑时,图片编号不可修改。"}</p>
+                <p>1.上传图片前应先填写符合规范的图片编号和名称,否则不能上传图片;</p>
+                <p>2.上传成功后会自动生成图片大小、格式、路径等信息,无需手动填写;</p>
+                <p>3.图片一旦创建完成,再次编辑时,图片编号不可修改。</p>
               </Fragment>
             }
           />
@@ -106,14 +105,14 @@ export default class CreateOnePicture extends PureComponent {
               pattern: /^[a-zA-Z0-9|-]+$/ig, message: '编号格式错误!',
             }],
           })(
-            <Input onChange={this.handleCodeInputChange}/>
+            <Input onChange={this.handleCodeInputChange} />
           )}
         </Form.Item>
         <Form.Item label="图片名称" hasFeedback {...formItemLayout}>
           {getFieldDecorator('name', {
             rules: [{ required: true, message: '名称不能为空!' }],
           })(
-            <Input onChange={this.handleNameInputChange}/>
+            <Input onChange={this.handleNameInputChange} />
           )}
         </Form.Item>
         <Form.Item label="图片上传" {...formItemLayout}>
@@ -122,11 +121,11 @@ export default class CreateOnePicture extends PureComponent {
           })(
             <Uploader
               forbidden={
-                (code && /^[a-zA-Z0-9|-]+$/ig.test(code) && name) ? false : true
+                !((code && /^[a-zA-Z0-9|-]+$/ig.test(code) && name))
               }
               fileCode={code}
               fileList={fileList}
-              accept='image'
+              accept="image"
               multiple={false}
               totalLimit={1}
             />
@@ -136,21 +135,21 @@ export default class CreateOnePicture extends PureComponent {
           {getFieldDecorator('path', {
             initialValue: path,
           })(
-            <Input disabled={true}/>
+            <Input disabled />
           )}
         </Form.Item>
         <Form.Item label="图片格式" {...formItemLayout}>
           {getFieldDecorator('format', {
             initialValue: type ? type.split('/')[1] : '',
           })(
-            <Input disabled={true}/>
+            <Input disabled />
           )}
         </Form.Item>
         <Form.Item label="图片大小" {...formItemLayout}>
           {getFieldDecorator('size', {
             initialValue: size,
           })(
-            <Input disabled={true}/>
+            <Input disabled />
           )}
         </Form.Item>
         <Form.Item label="图片状态" {...formItemLayout}>

+ 50 - 71
src/routes/Resource/PictureList.js

@@ -1,50 +1,38 @@
-import React, { PureComponent } from 'react';
+import React, { Component } from 'react';
 import { connect } from 'dva';
 import { routerRedux } from 'dva/router';
-import { Card, message, Button, Icon, Modal } from 'antd';
-import { PictureItem } from '../../components/RBItem';
-import { StandardCardList } from '../../components/RBList';
+import { Card, Modal, Button, message } from 'antd';
+import PictureCardList from './PictureCardList';
+import PictureTableList from './PictureTableList';
 
 const Message = message;
 
-function pictureItemFormatter(originData) {
-  return originData.map(item => {
-    return {
-      id: item.id,
-      picPath: item.path,
-      picName: item.name,
-      picCode: item.code,
-      picStatus: item.state,
-    };
-  });
-}
-
-@connect(({ resource, loading })=> ({
+@connect(({ resource, loading }) => ({
   resource,
   loading: loading.models.resource,
 }))
-export default class PictureList extends PureComponent {
+export default class PictureListPage extends Component {
   constructor(props) {
     super(props);
     const { state } = props.location;
     this.state = {
-      UIParams: (state || {}).UIParams, //组件的状态参数
-      Queryers: (state || {}).Queryers, //查询的条件参数
-    }
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+      isCard: true,
+    };
   }
-  state = {
-    UIParams: {},
-    Queryers: {},
-  };
   componentDidMount() {
     this.props.dispatch({
       type: 'resource/fetchImageList',
-      payload: {...this.state.Queryers},
+      payload: { ...this.state.Queryers },
     });
   }
-  //增加
+  handleShowTypeChange = () => {
+    this.setState({ isCard: !!!this.state.isCard });
+  }
+  // 增加
   handleCreateOperation = () => {
-    //页面跳转把这些参数传递过去,返回的时候再传回来,来保证组件等状态不变
+    // 页面跳转把这些参数传递过去,返回的时候再传回来,来保证组件等状态不变
     const { Queryers, UIParams } = this.state;
     this.props.dispatch(
       routerRedux.push({
@@ -56,7 +44,7 @@ export default class PictureList extends PureComponent {
       })
     );
   }
-  //删除
+  // 删除
   handleDeleteOperation = (item) => {
     Modal.confirm({
       okText: '确定',
@@ -66,16 +54,16 @@ export default class PictureList extends PureComponent {
       onOk: () => {
         this.props.dispatch({
           type: 'resource/deleteImage',
-          payload: {id:item.id},
+          payload: { id: item.id },
         });
       },
     });
   }
-  //修改
+  // 修改
   handleEditOperation = () => {
 
   }
-  //查询
+  // 查询
   handleFilterOperation = (params, states) => {
     this.props.dispatch({
       type: 'resource/fetchImageList',
@@ -86,55 +74,46 @@ export default class PictureList extends PureComponent {
       Queryers: params,
     });
   }
-  //批量
-  handleBatchOperation = (actionName, keys) => {
+  // 批量
+  handleBatchOperation = () => {
     Message.info('暂不支持批量操作!');
   }
+
   render() {
-    const { UIParams } = this.state;
     const { resource, loading } = this.props;
     const { list, totalSize, pageSize, pageNo } = resource;
-    const batchActions = [{
-      key: 'delete',
-      name: '批量删除',
-    }, {
-      key: 'edit',
-      name: '批量编辑',
-    }];
-    const basicSearch = {
-      keys: [{
-        name: '图片编号',
-        field: 'code',
-      }, {
-        name: '图片名称',
-        field: 'name',
-      }],
-    };
-    const pagination = {
+    const publicProps = {
+      loading,
       pageNo,
       pageSize,
       totalSize,
+      dataSource: list,
+      onCreateClick: this.handleCreateOperation,
+      onDeleteClick: this.handleDeleteOperation,
+      onEditClick: this.handleEditOperation,
+      onFilterClick: this.handleFilterOperation,
+      onBatchClick: this.handleBatchOperation,
     };
     return (
-      <Card bordered={false} style={{ padding: 16 }}>
-        <StandardCardList
-          loading={loading}
-          component={PictureItem}
-          keepUIState={{...UIParams}}
-          dataSource={pictureItemFormatter(list)}
-          header={{
-            basicSearch,
-            onFilterClick: this.handleFilterOperation,
-            onCreateClick: this.handleCreateOperation,
-          }}
-          footer={{
-            pagination,
-            batchActions,
-            onBatchClick: this.handleBatchOperation,
-          }}
-          grid={{ gutter: 16, xxl: 12, xl:6, lg: 4, md: 3, sm: 2, xs: 1 }}
-          onDelete={this.handleDeleteOperation}
-        />
+      <Card bordered={false}>
+        <Button.Group style={{marginBottom: 16}}>
+          <Button
+            style={{width: 50}}
+            onClick={this.handleShowTypeChange}
+            icon="appstore-o"
+            type={this.state.isCard ? 'primary' : null}
+          />
+          <Button
+            style={{width: 50}}
+            onClick={this.handleShowTypeChange}
+            icon="table"
+            type={this.state.isCard ? null : 'primary'}
+          />
+        </Button.Group>
+        {this.state.isCard ?
+          <PictureCardList {...publicProps} /> :
+          <PictureTableList {...publicProps} />
+        }
       </Card>
     );
   }

+ 114 - 0
src/routes/Resource/PictureTableList.js

@@ -0,0 +1,114 @@
+import React from 'react';
+import moment from 'moment';
+import { Divider, message } from 'antd';
+import { ossHost } from '../../utils/config';
+import { renderStatus, addRowKey } from '../../utils/utils';
+import { StandardTableList } from '../../components/RBList';
+import styles from './PictureTableList.less';
+
+function PictureTableList({
+  UIParams, dataSource, loading, totalSize, pageSize, pageNo,
+  onFilterClick, onCreateClick, onBatchClick, onDeleteClick,
+}) {
+  const batchActions = [{
+    key: 'delete',
+    name: '批量删除',
+  }, {
+    key: 'edit',
+    name: '批量编辑',
+  }];
+  const basicSearch = {
+    keys: [{
+      name: '图片编号',
+      field: 'code',
+    }, {
+      name: '图片名称',
+      field: 'name',
+    }],
+  };
+  const pagination = {
+    pageNo,
+    pageSize,
+    totalSize,
+  };
+  const renderThumbPic = (url) => {
+    return (
+      <div className={styles.thumbPic}>
+        <img src={url} alt="thumb" />
+      </div>
+    );
+  };
+  const renderMetaData = ({ code, name }) => {
+    return (
+      <div className={styles.meta}>
+        <p>{name}</p>
+        <p>{code}</p>
+      </div>
+    );
+  };
+  const renderActions = () => {
+    return (
+      <div>
+        <a>查看</a>
+        <Divider type="vertical" />
+        <a>编辑</a>
+        <Divider type="vertical" />
+        <a onClick={onDeleteClick}>删除</a>
+      </div>
+    );
+  };
+  const columns = [{
+    title: '缩略图',
+    key: 1,
+    dataIndex: 'path',
+    render: text => renderThumbPic(`${ossHost}/${text}`),
+    width: '15%',
+  }, {
+    title: '名称/编号',
+    key: 2,
+    dataIndex: 'meta',
+    render: (_, record) => renderMetaData(record),
+    width: '25%',
+  }, {
+    title: '格式',
+    key: 3,
+    dataIndex: 'format',
+    width: '10%',
+  }, {
+    title: '大小(Byte)',
+    key: 4,
+    dataIndex: 'size',
+    width: '10%',
+  }, {
+    title: '状态',
+    key: 5,
+    dataIndex: 'status',
+    render: text => renderStatus(text),
+    width: '8%',
+  }, {
+    title: '更新日期',
+    key: 6,
+    dataIndex: 'gmtModified',
+    render: text => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+    width: '17%',
+  }, {
+    title: '操作',
+    key: 7,
+    dataIndex: 'action',
+    render: (_, record) => renderActions(record),
+    width: '15%',
+  }];
+
+  return (
+    <StandardTableList
+      columns={columns}
+      loading={loading}
+      dataSource={addRowKey(dataSource)}
+      keepUIState={{...UIParams}}
+      header={{basicSearch, onFilterClick, onCreateClick}}
+      footer={{pagination, batchActions, onBatchClick}}
+    />
+  );
+}
+
+export default PictureTableList;

+ 0 - 9
src/routes/Resource/VideoList.less

@@ -20,12 +20,3 @@
     font-weight: 500;
   }
 }
-
-:global {
-  .ant-table-tbody > tr > td {
-    padding: 0;
-  }
-  .ant-table-thead > tr > th {
-    padding: 16px 0;
-  }
-}

+ 85 - 101
src/routes/Resource/VideoList.js

@@ -1,119 +1,103 @@
 import React, { Component } from 'react';
-import { Card, Divider, Badge, Input, Select, Icon } from 'antd';
 import { connect } from 'dva';
-import { ossHost } from '../../utils/config';
-import moment from 'moment';
-import { StandardTableList } from '../../components/RBList';
-import styles from './VideoList.less';
+import { Card, Button, message } from 'antd';
+import RBVideoPlayer from '../../components/RBVideoPlayer';
+import VideoPlayList from './VideoPlayList';
+import VideoTableList from './VideoTableList';
+import { addRowKey } from '../../utils/utils';
 
-@connect(({resource, loading}) => ({
+const Message = message;
+
+function deleteBlankKey(obj) {
+  if (!(typeof obj === 'object')) {
+    return;
+  }
+  let newObj = {...obj};
+  for (let key in newObj) {
+    if (newObj.hasOwnProperty(key) && !newObj[key]) {
+      delete newObj[key];
+    }
+  }
+  return newObj;
+}
+
+@connect(({ loading, resource }) => ({
   resource,
   loading: loading.models.resource,
 }))
-export default class PictureListPage extends Component {
+export default class VideoListPage extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isCard: true,
+      destory: true,
+      current: {},
+    }
+  }
   componentDidMount() {
     this.props.dispatch({
-      type: 'resource/fetchImageList',
-      payload: {}
+      type: 'resource/fetchVideoList',
+      payload: {},
+    });
+  }
+  handleShowTypeChange = () => {
+    this.setState({ isCard: !!!this.state.isCard });
+  }
+  handleFilterOperation = (params) => {
+    const newParams = deleteBlankKey(params);
+    this.props.dispatch({
+      type: 'resource/fetchVideoList',
+      payload: newParams,
     });
   }
+  handleBatchOperation = () => {
+    Message.info('暂不支持批量操作!');
+  }
+  handleModalDestory = () => {
+    this.setState({ destory: true });
+  }
+  handleModalCreate = (item) => {
+    this.setState({ destory: false, current: item });
+  }
+
   render() {
     const { loading, resource } = this.props;
-    const { list, total, current, pageSize } = resource;
-
-    const pagination = {
-      total,
-      current,
+    const { list, totalSize, pageSize, pageNo } = resource;
+    const publicProps = {
+      loading,
+      pageNo,
       pageSize,
+      totalSize,
+      dataSource: addRowKey(list),
+      onFilterClick: this.handleFilterOperation,
     };
-    const renderThumbPic = (url) => {
-      return (
-        <div className={styles.thumbPic}>
-          <img src={url} alt="thumb"/>
-        </div>
-      );
-    }
-    const renderMetaData = ({code,name}) => {
-      return (
-        <div className={styles.meta}>
-          <p>{name}</p>
-          <p>{code}</p>
-        </div>
-      );
-    }
-    const renderStatus = (status) => {
-      if (status === 'NORMAL') {
-        return (
-          <Badge status="success" text="正常"/>
-        );
-      } else {
-        return (
-          <Badge status="error" text="删除"/>
-        );
-      }
-    }
-    const renderActions = () => {
-      return (
-        <div>
-          <a>查看</a>
-          <Divider type="vertical"/>
-          <a>编辑</a>
-          <Divider type="vertical"/>
-          <a>删除</a>
-        </div>
-      );
-    }
-
-    const columns = [{
-      title: '缩略图',
-      key: 1,
-      dataIndex: 'path',
-      render: (text) => renderThumbPic(`${ossHost}/${text}`),
-      width: '15%',
-    }, {
-      title: '名称/编号',
-      key: 2,
-      dataIndex: 'meta',
-      render: (_, record) => renderMetaData(record),
-      width: '25%',
-    }, {
-      title: '格式',
-      key: 3,
-      dataIndex: 'format',
-      width: '10%',
-    }, {
-      title: '大小',
-      key: 4,
-      dataIndex: 'size',
-      width: '10%',
-    }, {
-      title: '状态',
-      key: 5,
-      dataIndex: 'status',
-      render: (text) => renderStatus(text),
-      width: '8%',
-    }, {
-      title: '更新日期',
-      key: 6,
-      dataIndex: 'gmtModified',
-      render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
-      width: '17%',
-    }, {
-      title: '操作',
-      key: 7,
-      dataIndex: 'action',
-      render: () => renderActions(),
-      width: '15%',
-    }];
     return (
-      <Card bordered={false} style={{ padding: 16 }}>
-        <StandardTableList
-          columns={columns}
-          selectedRows={[]}
-          loading={loading}
-          dataSource={list}
-          pagination={pagination}
-        />
+      <Card bordered={false}>
+        <Button.Group style={{marginBottom: 16}}>
+          <Button
+            style={{width: 50}}
+            onClick={this.handleShowTypeChange}
+            icon="appstore-o"
+            type={this.state.isCard ? 'primary' : null}
+          />
+          <Button
+            style={{width: 50}}
+            onClick={this.handleShowTypeChange}
+            icon="table"
+            type={this.state.isCard ? null : 'primary'}
+          />
+        </Button.Group>
+        {this.state.isCard ?
+          <VideoPlayList {...publicProps} /> :
+          <VideoTableList
+            {...publicProps}
+            currentItem={this.state.current}
+            modalDestory={this.state.destory}
+            onModalDestory={this.handleModalDestory}
+            onModalCreate={this.handleModalCreate}
+            onBatchClick={this.handleBatchOperation}
+          />
+        }
       </Card>
     );
   }

+ 181 - 0
src/routes/Resource/VideoPlayList.js

@@ -0,0 +1,181 @@
+import React, { PureComponent } from 'react';
+import moment from 'moment';
+import { Table, Pagination, Select, Input } from 'antd';
+import RBVideoPlayer from '../../components/RBVideoPlayer';
+import styles from './VideoPlayList.less';
+
+export default class VideoPlayList extends PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      currentPlayingIndex: 0,
+      searchSelectKey: 'code',
+      searchInputValue: '',
+    };
+  }
+  componentWillReceiveProps(nextProps) {
+    const { dataSource } = nextProps;
+    if (dataSource && dataSource.length) {
+      this.setState({ currentPlayingIndex: 0 });
+    }
+  }
+  handleOnRowClick = (index) => {
+    this.setState({ currentPlayingIndex: index });
+  }
+  handleSearchSelectChange = (value) => {
+    this.setState({ searchSelectKey: value });
+  }
+  handleInputChange = (e) => {
+    this.setState({ searchInputValue: e.target.value });
+  }
+  handleFilterOperation = (params) => {
+    const { pageSize, pageNo, onFilterClick } = this.props;
+    const { searchSelectKey, searchInputValue } = this.state;
+    onFilterClick({
+      pageNo,
+      pageSize,
+      [searchSelectKey]: searchInputValue,
+      ...params,
+    });
+  }
+  handleSearchBtnClick = () => {
+    this.handleFilterOperation();
+  }
+  handleTablePageChange = (page, pageSize) => {
+    this.handleFilterOperation({
+      pageSize,
+      pageNo: page,
+    });
+  }
+  handleTablePageSizeChange = (current, size) => {
+    this.handleFilterOperation({
+      pageSize: size,
+      pageNo: current,
+    });
+  }
+
+  render() {
+    const { dataSource, loading, totalSize, pageSize, pageNo } = this.props;
+    const renderQuality = (quality) => {
+      if (quality === 'high') {
+        return '高清';
+      } else {
+        return '标清';
+      }
+    };
+    const renderItem = (data) => {
+      return (
+        <div className={styles.meta}>
+          <p>
+            <span>{'编号:  '}</span>
+            <a>{`${data.code}`}</a>
+          </p>
+          <p>
+            <span>{'名称:  '}</span>
+            {`${data.name}`}
+          </p>
+          <p>
+            <span>{'格式:  '}</span>
+            {`${renderQuality(data.quality)}[${data.format}]`}
+          </p>
+          <p>
+            <span>{'时间:  '}</span>
+            {`${moment(data.gmtModified).format('YYYY-MM-DD HH:mm:ss')}`}
+          </p>
+        </div>
+      );
+    };
+    const renderVideoPlayer = () => {
+      if (dataSource.length) {
+        const videoItem = dataSource[this.state.currentPlayingIndex];
+        return (
+          <RBVideoPlayer
+            width={'100%'}
+            height={'100%'}
+            url={videoItem.url}
+            isM3U8={videoItem.format === 'm3u8' ? true : false}
+          />
+        );
+      }
+      return null;
+    }
+    const columns = [{
+      title: '名称/编号/状态/格式/质量/日期',
+      key: 1,
+      dataIndex: 'cn',
+      render: (_, record) => renderItem(record),
+    }];
+    const paginationProps = {
+      pageSize,
+      total: totalSize,
+      current: pageNo,
+      simple: true,
+      showTotal: (total) => `共 ${total} 条`,
+      onChange: this.handleTablePageChange,
+      onShowSizeChange: this.handleTablePageSizeChange,
+    };
+    const selectOptions = [{
+      field: 'code',
+      name: '编号',
+    }, {
+      field: 'name',
+      name: '名称',
+    }];
+    const onRowClick = (_, index) => {
+      return {
+        onClick: () => this.handleOnRowClick(index),
+      };
+    }
+    const renderHeader = () => {
+      return (
+        <Input.Search
+          value={this.state.searchInputValue}
+          style={{ width: '100%' }}
+          addonBefore={
+            <Select
+              placeholder="请选择"
+              value={this.state.searchSelectKey}
+              onChange={this.handleSearchSelectChange}
+            >
+              {selectOptions.map(item => (
+                <Select.Option
+                  key={item.field}
+                  value={item.field}
+                >
+                  {item.name}
+                </Select.Option>))}
+            </Select>
+          }
+          placeholder="请输入"
+          enterButton
+          onChange={this.handleInputChange}
+          onSearch={this.handleSearchBtnClick}
+        />
+      );
+    }
+    return (
+      <div className={styles.content}>
+        <div className={styles.left}>
+          <Table
+            bordered
+            title={() => renderHeader()}
+            footer={() => <Pagination {...paginationProps} />}
+            columns={columns}
+            pagination={false}
+            onRow={onRowClick}
+            rowKey={(record) => record.key}
+            className={styles.table}
+            rowClassName={(_,index) =>
+              index === this.state.currentPlayingIndex ? styles.rowChecked: null
+            }
+            loading={loading}
+            dataSource={dataSource}
+          />
+        </div>
+        <div className={styles.right}>
+          {renderVideoPlayer()}
+        </div>
+      </div>
+    );
+  }
+}

+ 91 - 0
src/routes/Resource/VideoPlayList.less

@@ -0,0 +1,91 @@
+@import "~antd/lib/style/themes/default.less";
+
+.table {
+  font-size: 16px;
+  :global {
+    .ant-table table {
+      border-collapse: collapse;
+    }
+    .ant-table-wrapper {
+      height: 100%;
+    }
+    .ant-table-tbody > tr > td {
+      padding: 5px 5px;
+    }
+    .ant-table-thead > tr > th {
+      padding: 10px 5px;
+    }
+    .ant-table-row {
+      &:hover {
+        cursor: pointer;
+      }
+    }
+    .ant-table-title {
+      padding: 0;
+    }
+    .ant-table-footer {
+      text-align: center;
+    }
+    .ant-table.ant-table-bordered .ant-table-title {
+      border: unset;
+      padding-left: 0;
+      padding-right: 0;
+    }
+  }
+}
+
+.rowChecked {
+  border: 5px solid @primary-5;
+}
+
+.meta {
+  p {
+    margin: 0;
+    font-weight: 400;
+    span {
+      font-weight: 500;
+    }
+  }
+}
+
+@media (min-width: 1200px) {
+  .table {
+    :global {
+      .ant-table-body {
+        height: 480px;
+        overflow-y: scroll;
+      }
+    }
+  }
+  .left {
+    width: 32%;
+    float: left;
+    height: 570px;
+  }
+  .right {
+    width: 68%;
+    float: left;
+    height: 570px;
+  }
+}
+
+@media (min-width: 1600px) {
+  .table {
+    :global {
+      .ant-table-body {
+        height: 760px;
+        overflow-y: scroll;
+      }
+    }
+  }
+  .left {
+    width: 32%;
+    float: left;
+    height: 850px;
+  }
+  .right {
+    width: 68%;
+    float: left;
+    height: 850px;
+  }
+}

+ 111 - 0
src/routes/Resource/VideoTableList.js

@@ -0,0 +1,111 @@
+import React from 'react';
+import moment from 'moment';
+import { Card, Badge, Modal, message } from 'antd';
+import { ossHost } from '../../utils/config';
+import { StandardTableList } from '../../components/RBList';
+import RBVideoPlayer from '../../components/RBVideoPlayer';
+import { renderStatus } from '../../utils/utils';
+
+function VideoTableList({
+  dataSource, loading, totalSize, pageSize, pageNo, modalDestory, currentItem,
+  onFilterClick, onBatchClick, onModalCreate, onModalDestory,
+}) {
+  const renderQuality = (quality) => {
+    if (quality === 'high') {
+      return '高清';
+    } else {
+      return '标清';
+    }
+  };
+  const pagination = {
+    pageNo,
+    pageSize,
+    totalSize,
+  };
+  const batchActions = [{
+    key: 'delete',
+    name: '批量删除',
+  }, {
+    key: 'edit',
+    name: '批量编辑',
+  }];
+  const basicSearch = {
+    keys: [{
+      name: '视频编号',
+      field: 'code',
+    }, {
+      name: '视频名称',
+      field: 'name',
+    }],
+  };
+  const columns = [{
+    title: '视频编号',
+    key: 1,
+    dataIndex: 'code',
+    width: '15%',
+  }, {
+    title: '视频名称',
+    key: 2,
+    dataIndex: 'name',
+    width: '32%',
+  }, {
+    title: '视频格式',
+    key: 3,
+    dataIndex: 'format',
+    width: '10%',
+  }, {
+    title: '视频质量',
+    key: 4,
+    dataIndex: 'quality',
+    render: (text) => renderQuality(text),
+    width: '10%',
+  }, {
+    title: '视频状态',
+    key: 5,
+    dataIndex: 'status',
+    render: (text) => renderStatus(text),
+    width: '10%',
+  }, {
+    title: '创建时间',
+    key: 6,
+    dataIndex: 'gmtModified',
+    render: (text) => moment(text).format('YYYY-MM-DD'),
+    width: '15%',
+  }, {
+    title: '操作',
+    key: 8,
+    dataIndex: 'operation',
+    render: (_, record) => <a onClick={() => onModalCreate(record)}>播放</a>,
+    width: '8%',
+  }];
+  return (
+    <div>
+      <StandardTableList
+        columns={columns}
+        loading={loading}
+        dataSource={dataSource}
+        header={{basicSearch, onFilterClick }}
+        footer={{pagination, batchActions, onBatchClick}}
+      />
+      {!modalDestory &&
+        <Modal
+          title={currentItem.name}
+          visible={true}
+          footer={null}
+          onCancel={onModalDestory}
+          maskClosable={false}
+          width={800}
+        >
+          <RBVideoPlayer
+            width={'100%'}
+            height={'100%'}
+            url={currentItem.url}
+            isM3U8={currentItem.format === 'm3u8' ? true : false}
+          />
+        </Modal>
+      }
+    </div>
+  );
+}
+
+export default VideoTableList;

+ 0 - 1
src/routes/User/Login.js

@@ -21,7 +21,6 @@ export default class LoginPage extends Component {
   }
 
   handleSubmit = (err, values) => {
-    const { type } = this.state;
     if (!err) {
       this.props.dispatch({
         type: 'login/login',

+ 34 - 0
src/services/campus.js

@@ -0,0 +1,34 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { api } from '../utils/config';
+
+export async function queryCampusList(params) {
+  return request(`${api.campus}?${stringify(params)}`);
+}
+
+export async function queryCampusItem({ id }) {
+  return request(`${api.campusItem}/${id}`);
+}
+
+export async function createCampusItem(params) {
+  const options = {
+    method: 'POST',
+    body: params,
+  };
+  return request(`${api.campusItem}`, options);
+}
+
+export async function updateCampusItem(params) {
+  const options = {
+    method: 'PUT',
+    body: params,
+  };
+  return request(`${api.campusItem}`, options);
+}
+
+export async function deleteCampusItem({ id }) {
+  const options = {
+    method: 'DELETE',
+  };
+  return request(`${api.campusItem}/${id}`, options);
+}

+ 42 - 0
src/services/merchant.js

@@ -0,0 +1,42 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { api } from '../utils/config';
+
+export async function queryMerchantList(params) {
+  return request(`${api.merchant}?${stringify(params)}`);
+}
+
+export async function queryMerchantItem({ id }) {
+  return request(`${api.merchantItem}/${id}`);
+}
+
+export async function createMerchantItem(params) {
+  const options = {
+    method: 'POST',
+    body: params,
+  };
+  return request(`${api.merchantItem}`, options);
+}
+
+export async function updateMerchantItem(params) {
+  const options = {
+    method: 'PUT',
+    body: params,
+  };
+  return request(`${api.merchantItem}`, options);
+}
+
+export async function deleteMerchantItem({ id }) {
+  const options = {
+    method: 'DELETE',
+  };
+  return request(`${api.merchantItem}/${id}`, options);
+}
+
+export async function despoitMerchantItem(params) {
+  const options = {
+    method: 'POST',
+    body: params,
+  };
+  return request(`${api.despoit}`, options);
+}

+ 3 - 5
src/services/resource.js

@@ -21,15 +21,13 @@ export async function queryVideoResource(params) {
 
 export async function queryOssSignature(params) {
   const signature = getSignature();
-  const expireTime = Math.floor(
-    (new Date()).getTime() / 1000 + 5
-  ).toString(); // 5s缓冲时间
+  const expireTime = Math.floor(((new Date()).getTime() / 1000) + 5).toString();
 
-  //检查本地签名是否过期
+  // 检查本地签名是否过期
   if (signature.expire >= expireTime) {
     return signature;
   }
-  //过期后请求新的签名
+  // 过期后请求新的签名
   return request(`${api.signature}?${stringify(params)}`);
 }
 

+ 13 - 1
src/utils/config.js

@@ -4,7 +4,11 @@ const RESOURCE_IMAGE = 3;
 const STATUS_NORMAL = 'NORMAL';
 const STATUS_DELETE = 'DEL';
 
-const fileMaxSize = 15; //file max size should below 2M
+const DOMAIN_CP = 1010;
+const DOMAIN_LJ = 2010;
+const DOMAIN_PJ = 3010;
+
+const fileMaxSize = 15; // file max size should below 2M
 const pageSize = 20;
 const plantform = 'LJ';
 const projectName = 'RealBull';
@@ -17,6 +21,11 @@ const api = {
   resource: `${apiHost}/resource/list`,
   signature: `${apiHost}/oss/signature`,
   picture: `${apiHost}/resource`,
+  merchant: `${apiHost}/merchant/list`,
+  merchantItem: `${apiHost}/merchant`,
+  despoit: `${apiHost}/money/charge`,
+  campus: `${apiHost}/campus/list`,
+  campusItem: `${apiHost}/campus`,
 };
 
 export {
@@ -24,6 +33,9 @@ export {
   RESOURCE_IMAGE,
   STATUS_NORMAL,
   STATUS_DELETE,
+  DOMAIN_CP,
+  DOMAIN_LJ,
+  DOMAIN_PJ,
   plantform,
   projectName,
   copyright,

+ 2 - 2
src/utils/request.js

@@ -25,7 +25,7 @@ function checkHttpStatus(response) {
     return response;
   }
   const errortext = codeMessage[response.status] || response.statusText;
-  message.error(`请求错误 ${response.url}: ${errotext}`);
+  message.error(`请求错误 ${response.url}: ${errortext}`);
   const error = new Error(errortext);
   error.name = response.status;
   error.response = response;
@@ -81,7 +81,7 @@ export default function request(url, options) {
   return fetch(url, newOptions)
     .then(checkHttpStatus)
     .then((response) => {
-      if (newOptions.method === 'DELETE' || response.status === 204) {
+      if (response.status === 204) {
         return response.text();
       }
       return response.json();

+ 56 - 0
src/utils/utils.js

@@ -1,4 +1,12 @@
+import React from 'react';
 import moment from 'moment';
+import { Badge } from 'antd';
+import {
+  STATUS_NORMAL,
+  DOMAIN_CP,
+  DOMAIN_LJ,
+  DOMAIN_PJ,
+} from './config';
 
 export function fixedZero(val) {
   return val * 1 < 10 ? `0${val}` : val;
@@ -157,3 +165,51 @@ const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-
 export function isUrl(path) {
   return reg.test(path);
 }
+
+export function addRowKey(data) {
+  return data.map(item => {
+    item.key = item.id;
+    return item;
+  });
+}
+
+export function renderCategory(domain) {
+  switch (domain) {
+    case DOMAIN_CP:
+      return '供应商';
+    case DOMAIN_LJ:
+      return '平台方';
+    case DOMAIN_PJ:
+      return '渠道商';
+    default:
+      return '未知';
+  }
+}
+export function renderStatus(status) {
+  if (status === STATUS_NORMAL) {
+    return (
+      <Badge status="success" text="正常" />
+    );
+  } else {
+    return (
+      <Badge status="error" text="删除" />
+    );
+  }
+};
+export function toDecimal2(x) {
+  let f = parseFloat(x);
+  if (isNaN(f)) {
+    return false;
+  }
+  f = Math.round(x*100)/100;
+  let s = f.toString();
+  let rs = s.indexOf('.');
+  if (rs < 0) {
+    rs = s.length;
+    s += '.';
+  }
+  while (s.length <= rs + 2) {
+    s += '0';
+  }
+  return s;
+}