فهرست منبع

1. add course manage module;
2. add support manage module;

zhanghe 7 سال پیش
والد
کامیت
e658d03774
40فایلهای تغییر یافته به همراه3476 افزوده شده و 31 حذف شده
  1. 50 0
      .roadhogrc.mock.js
  2. 28 0
      mock/course.js
  3. 30 0
      mock/soldProduct.js
  4. 21 0
      mock/support.js
  5. 2 2
      mock/ware.js
  6. 4 1
      src/common/menu.js
  7. 22 0
      src/common/router.js
  8. 246 0
      src/components/CardValuation/index.js
  9. 8 0
      src/components/CardValuation/index.less
  10. 21 2
      src/components/SelectModal/ForSelTable.js
  11. 38 5
      src/components/SelectModal/index.js
  12. 113 0
      src/models/course/course.js
  13. 138 0
      src/models/course/detail.js
  14. 119 0
      src/models/support/detail.js
  15. 104 0
      src/models/support/support.js
  16. 393 0
      src/routes/Course/detail/index.js
  17. 87 0
      src/routes/Course/detail/lessonModal.js
  18. 120 0
      src/routes/Course/detail/lessonModal.less
  19. 79 0
      src/routes/Course/detail/resourceModal.js
  20. 61 0
      src/routes/Course/detail/resourceModal.less
  21. 87 0
      src/routes/Course/detail/supportModal.js
  22. 120 0
      src/routes/Course/detail/supportModal.less
  23. 135 0
      src/routes/Course/index.js
  24. 48 0
      src/routes/Course/search.js
  25. 101 0
      src/routes/Course/table.js
  26. 78 0
      src/routes/Course/table.less
  27. 1 20
      src/routes/Lesson/detail/index.js
  28. 348 0
      src/routes/Support/detail/index.js
  29. 109 0
      src/routes/Support/detail/resourceModal.js
  30. 127 0
      src/routes/Support/detail/resourceModal.less
  31. 87 0
      src/routes/Support/detail/supportModal.js
  32. 120 0
      src/routes/Support/detail/supportModal.less
  33. 135 0
      src/routes/Support/index.js
  34. 48 0
      src/routes/Support/search.js
  35. 101 0
      src/routes/Support/table.js
  36. 78 0
      src/routes/Support/table.less
  37. 1 1
      src/routes/Ware/detail/modal.js
  38. 32 0
      src/services/course.js
  39. 32 0
      src/services/support.js
  40. 4 0
      src/utils/api.js

+ 50 - 0
.roadhogrc.mock.js

@@ -8,6 +8,8 @@ import { groupList } from './mock/group';
 import { tagList } from './mock/tag';
 import { wareList } from './mock/ware';
 import { lessonList } from './mock/lesson';
+import { courseList } from './mock/course';
+import { supportList } from './mock/support';
 import { orderList } from './mock/order';
 import { signature } from './mock/signature';
 import * as api from './src/utils/api';
@@ -21,6 +23,8 @@ global.groupList = groupList;
 global.tagList = tagList;
 global.wareList = wareList;
 global.lessonList = lessonList;
+global.courseList = courseList;
+global.supportList = supportList;
 global.orderList = orderList;
 global.signature = signature;
 
@@ -268,6 +272,52 @@ const proxy = {
     const { id } = req.params;
     queryOne(global.lessonList, id, res);
   },
+  // 课程
+  [`POST ${api.course.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[POST][${api.course}]`, req.body);
+    res.send(create(global.courseList, req.body));
+  },
+  [`DELETE ${api.course}`]: (req, res) => {
+    console.log(`[DELETE][${api.course}]`, req.params);
+    const { id } = req.params;
+    remove(global.courseList, id, res);
+  },
+  [`PUT ${api.course.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[PUT][${api.course}]`, req.body);
+    res.send(update(global.courseList, req.body));
+  },
+  [`GET ${api.courses}`]: (req, res) => {
+    console.log(`[GET][${api.courses}]`, req.query);
+    res.send(query(global.courseList, req.query));
+  },
+  [`GET ${api.course}`]: (req, res) => {
+    console.log(`[GET][${api.course}]`, req.params);
+    const { id } = req.params;
+    queryOne(global.courseList, id, res);
+  },
+  // 周边
+  [`POST ${api.support.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[POST][${api.support}]`, req.body);
+    res.send(create(global.supportList, req.body));
+  },
+  [`DELETE ${api.support}`]: (req, res) => {
+    console.log(`[DELETE][${api.support}]`, req.params);
+    const { id } = req.params;
+    remove(global.supportList, id, res);
+  },
+  [`PUT ${api.support.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[PUT][${api.support}]`, req.body);
+    res.send(update(global.supportList, req.body));
+  },
+  [`GET ${api.supports}`]: (req, res) => {
+    console.log(`[GET][${api.supports}]`, req.query);
+    res.send(query(global.supportList, req.query));
+  },
+  [`GET ${api.support}`]: (req, res) => {
+    console.log(`[GET][${api.support}]`, req.params);
+    const { id } = req.params;
+    queryOne(global.supportList, id, res);
+  },
   // 订单
   [`POST ${api.order.replace('/:id', '')}`]: (req, res) => {
     console.log(`[POST][${api.order}]`, req.body);

+ 28 - 0
mock/course.js

@@ -0,0 +1,28 @@
+let courseList = [];
+const statuses = ['NORMAL', 'DEL'];
+for (let i = 1; i < 500; i++) {
+  courseList.push({
+    id: String(i * 3),
+    code: 'course-code-' + i,
+    name: '小学语文二年级上册' + i,
+    title: '小学语文二年级上册' + i,
+    digest: '这段是课程描述,很长很长很长很长很长很长很长很长很长很长很长很长...',
+    detail: '这段是课程详情,这门课程讲的是小学语文,有很多很多很多很多很多很多很多很多的故事...',
+    keyword: null,
+    cvImg: {
+      id: '10111',
+      url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/612102.jpg',
+    },
+    bgImg: {
+      id: '20111',
+      url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/612102.jpg',
+    },
+    cpId: null,
+    cpName: null,
+    status: statuses[i % 2],
+    gmtCreated: 1512981450000,
+    gmtModified: 1512981450000,
+  })
+}
+
+module.exports = { courseList };

+ 30 - 0
mock/soldProduct.js

@@ -0,0 +1,30 @@
+const soldProductList = [];
+const productType = ['COURSE', 'SUPPORT'];
+for (let i = 1; i < 500; i++) {
+  soldProductList.push({
+    uid: String(i),
+    userCode: '198712190818' + i,
+    userName: '湖南省-长沙市-天水区-安宁校区-教室' + i,
+    campusId: String(i * 100),
+    campusCode: '324589243945' + i,
+    campusName: '湖南省-长沙市-天水区-安宁校区',
+    orderId: 'order-id-' + i,
+    goodsId: 'goods-id-' + i,
+    pkgId: 'package-id-' + i,
+    pid: String(i * 200),
+    productType: productType[i % 2],
+    productCode: 'product-code-' + i,
+    productName: '小学一年级语文上册',
+    merchantId: 'merchant-id-' + i,
+    duration: 5878834,
+    cpId: 'cp-id-' + i,
+    cpPrice: 2000,
+    merchantPrice: 3000,
+    terminalPrice: 4000,
+    chargeUnit: '年',
+    quantity: 2,
+    gmtCreated: 15789789893,
+  })
+}
+
+module.exports = { soldProductList };

+ 21 - 0
mock/support.js

@@ -0,0 +1,21 @@
+let supportList = [];
+const statuses = ['NORMAL', 'DEL'];
+for (let i = 1; i < 500; i++) {
+  supportList.push({
+    id: String(i * 3),
+    code: 'support-code-' + i,
+    name: '小学语文二年级上册练习册' + i,
+    title: '小学语文二年级上册练习册' + i,
+    digest: '这段是周边描述,很长很长很长很长很长很长很长很长很长很长很长很长...',
+    detail: '这段是周边详情,这个周边是小学语文练习册,有很多很多很多很多很多很多很多很多的题目...',
+    imgList: ['http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/612102.jpg'],
+    aboutList: null,
+    cpId: null,
+    cpName: null,
+    status: statuses[i % 2],
+    gmtCreated: 1512981450000,
+    gmtModified: 1512981450000,
+  })
+}
+
+module.exports = { supportList };

+ 2 - 2
mock/ware.js

@@ -15,7 +15,7 @@ for (let i = 1; i < 401; i++) {
       name: '测试图片',
       size: 100,
       status: 'DEL',
-      type: 3,
+      type: '3',
       gmtCreated: (new Date()).getTime(),
       gmtModified: (new Date()).getTime(),
       url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/611901.jpg',
@@ -25,7 +25,7 @@ for (let i = 1; i < 401; i++) {
       name: '测试视频',
       size: 1024,
       status: 'NORMAL',
-      type: 0,
+      type: '0',
       gmtCreated: (new Date()).getTime(),
       gmtModified: (new Date()).getTime(),
       url: 'http://efunvideo.ai160.com/vs2m/015/01503009/01503009031/01503009031.m3u8',

+ 4 - 1
src/common/menu.js

@@ -35,8 +35,11 @@ const menuData = [
       name: '课程管理',
       path: 'course',
     },{
-      name: '周边配套',
+      name: '配套管理',
       path: 'support',
+    },{
+      name: '课程包管理',
+      path: 'package',
     }]
   },{
     name: '商品管理',

+ 22 - 0
src/common/router.js

@@ -103,6 +103,28 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['lesson/detail', 'ware/ware'], () => import('../routes/Lesson/detail')),
       name: '修改课',
     },
+    '/product/course': {
+      component: dynamicWrapper(app, ['course/course'], () => import('../routes/Course')),
+    },
+    '/product/course/add': {
+      component: dynamicWrapper(app, ['course/detail', 'resource', 'lesson/lesson'], () => import('../routes/Course/detail')),
+      name: '添加课程',
+    },
+    '/product/course/edit/:id': {
+      component: dynamicWrapper(app, ['course/detail', 'resource', 'lesson/lesson'], () => import('../routes/Course/detail')),
+      name: '修改课程',
+    },
+    '/product/support': {
+      component: dynamicWrapper(app, ['support/support'], () => import('../routes/Support')),
+    },
+    '/product/support/add': {
+      component: dynamicWrapper(app, ['support/detail', 'resource', 'support/support'], () => import('../routes/Support/detail')),
+      name: '添加配套',
+    },
+    '/product/support/edit/:id': {
+      component: dynamicWrapper(app, ['support/detail', 'resource', 'support/support'], () => import('../routes/Support/detail')),
+      name: '修改配套',
+    },
     '/order': {
       component: dynamicWrapper(app, ['order/order'], () => import('../routes/Order')),
     },

+ 246 - 0
src/components/CardValuation/index.js

@@ -0,0 +1,246 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Divider, Popconfirm, message, Button, Card, Table, Input } from 'antd';
+import styles from './index.less';
+
+export default class CardValuation extends PureComponent {
+  static propTypes = {
+    value: PropTypes.array.isRequired,
+    cardTitle: PropTypes.string,
+  };
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      data: props.value,
+    }
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if ('value' in nextProps) {
+      this.setState({
+        data: nextProps.value,
+      });
+    }
+  }
+
+  getRowByKey(key) {
+    return this.state.data.filter(item => item.key === key)[0];
+  }
+
+  index = 0;
+  cacheOriginData = {};
+  // 切换编辑状态
+  toggleEditable(e, key) {
+    e.preventDefault();
+    const target = getRowByKey(key);
+    if (target) {
+      // 进入编辑状态时缓存原始数据
+      if (!target.editable) {
+        this.cacheOriginData[key] = { ...target };
+      }
+      target.editable = !target.editable;
+      this.setState({ data: [...this.state.data] });
+    }
+  }
+
+  // 删除一行
+  remove(key) {
+    const newData = this.state.data.filter(item => item.key !== key);
+    this.setState({ data: newData });
+    this.props.onChange(newData);
+  }
+
+  // 表格新建一行
+  newMember = () => {
+    const newData = [...this.state.data];
+    newData.push({
+      key: `NEW_TEMP_ID_${this.index}`,
+      chargeUnit: '',
+      cpPrice: '',
+      merchantPrice: '',
+      terminalPrice: '',
+      editable: true,
+      isNew: true,
+    });
+    this.index += 1;
+    this.setState({ data: newData });
+  }
+
+  // 编辑状态,回车直接保存
+  handleKeyPress(e, key) {
+    if (e.key === 'Enter') {
+      this.saveRow(e, key);
+    }
+  }
+
+  // input输入值变化
+  handleFieldChange(e, fieldName, key) {
+    const newData = [...this.state.data];
+    const target = this.getRowByKey(key);
+    if (target) {
+      target[fieldName] = e.target.value;
+      this.setState({ data: newData });
+    }
+  }
+
+  // 保存
+  saveRow(e, key) {
+    e.persist();
+    // save field when blur input
+    setTimeout(() => {
+      if (document.activeElement.tagName === 'INPUT' &&
+          document.activeElement !== e.target) {
+        return;
+      }
+      if (this.clickedCancel) {
+        this.clickedCancel = false;
+        return;
+      }
+      const target = this.getRowByKey(key) || {};
+      // if (!target.chargeUnit || !target.terminalPrice || !target.department) {
+      //   message.error('请填写完整成员信息。');
+      //   e.target.focus();
+      //   return;
+      // }
+      delete target.isNew;
+      this.toggleEditable(e, key);
+      this.props.onChange(this.state.data);
+    }, 10);
+  }
+
+  // 取消编辑
+  cancel(e, key) {
+    this.clickedCancel = true;
+    e.preventDefault();
+    const target = this.getRowByKey(key);
+    if (this.cacheOriginData[key]) {
+      Object.assign(target, this.cacheOriginData[key]);
+      target.editable = false;
+      delete this.cacheOriginData[key];
+    }
+    this.setState({ data: [...this.state.data] });
+  }
+
+  render() {
+    const columns = [{
+      title: '计价单位',
+      dataIndex: 'chargeUnit',
+      key: 'chargeUnit',
+      render: (text, record) => {
+        if (record.editable) {
+          return (
+            <Input
+              value={text}
+              autoFocus
+              placeholder="输入计价单位,如:`季`/`月/`年`/`件`等"
+            />
+          );
+        }
+        return text;
+      },
+    },{
+      title: '供应商价格',
+      dataIndex: 'cpPrice',
+      key: 'cpPrice',
+      render: (text, record) => {
+        if (record.editable) {
+          return (
+            <Input
+              value={text}
+              autoFocus
+              placeholder="请输入价格"
+            />
+          );
+        }
+        return text;
+      }
+    },{
+      title: '渠道方价格',
+      dataIndex: 'merchantPrice',
+      key: 'merchantPrice',
+      render: (text, record) => {
+        if (record.editable) {
+          return (
+            <Input
+              value={text}
+              autoFocus
+              placeholder="请输入渠道价格"
+            />
+          );
+        }
+        return text;
+      },
+    },{
+      title: '终端价格',
+      dataIndex: 'terminalPrice',
+      key: 'terminalPrice',
+      render: (text, record) => {
+        if (record.editable) {
+          return (
+            <Input
+              value={text}
+              autoFocus
+              placeholder="请输入终端价格"
+            />
+          );
+        }
+      }
+    },{
+      title: '操作',
+      key: 'action',
+      render: (text, record) => {
+        if (record.editable) {
+          if (record.isNew) {
+            return (
+              <span>
+                <a>保存</a>
+                <Divider type="vertical" />
+                <Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
+                  <a>删除</a>
+                </Popconfirm>
+              </span>
+            );
+          }
+          return (
+            <span>
+              <a>保存</a>
+              <Divider type="vertical" />
+              <a onClick={e => this.cancel(e, record.key)}>取消</a>
+            </span>
+          );
+        }
+        return (
+          <span>
+            <a onClick={e => this.toggleEditable(e, record.key)}>编辑</a>
+            <Divider type="vertical" />
+            <Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
+              <a>删除</a>
+            </Popconfirm>
+          </span>
+        );
+      }
+    }];
+    return (
+      <Card title={cardTitle} bordered={false}>
+        <Table
+          bordered={true}
+          columns={columns}
+          pagination={false}
+          dataSource={[]}
+          rowClassName={(record) => {
+            return record.editable ? styles.editable : '';
+          }}
+        />
+        <Button
+          style={{ width: '100%', marginTop: 16, marginBottom: 8 }}
+          type="dashed"
+          icon="plus"
+          disabled={true}
+        >新增价格类型
+        </Button>
+      </Card>
+    );
+  }
+}

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

@@ -0,0 +1,8 @@
+@import "~antd/lib/style/themes/default.less";
+
+.editable {
+  td {
+    padding-top: 13px !important;
+    padding-bottom: 12.5px !important;
+  }
+}

+ 21 - 2
src/components/SelectModal/ForSelTable.js

@@ -16,6 +16,7 @@ export default class ForSelTable extends PureComponent {
 
   render() {
     const {
+      mode,
       rowKeyName,
       dataSource,
       loading,
@@ -28,7 +29,7 @@ export default class ForSelTable extends PureComponent {
       ...rest,
     } = this.props;
 
-    const addColumn = {
+    const multipleAddColumn = {
       title: '操作',
       dataIndex: 'add-btn',
       key: 'add-btn',
@@ -44,8 +45,26 @@ export default class ForSelTable extends PureComponent {
       ),
     };
 
+    const singleAddColumn = {
+      title: '操作',
+      dataIndex: 'add-btn',
+      key: 'add-btn',
+      render: (text, record) => (
+        <Button
+          onClick={() => onAdd(record)}
+          size="small"
+          type="primary"
+        >选取</Button>
+      ),
+    }
+
     const curSpaceColumns = cloneDeep(columns);
-    curSpaceColumns.push(addColumn);
+    if (mode === 'multiple') {
+      curSpaceColumns.push(multipleAddColumn);
+    }
+    if (mode === 'single') {
+      curSpaceColumns.push(singleAddColumn);
+    }
 
     return (
       <Table

+ 38 - 5
src/components/SelectModal/index.js

@@ -92,6 +92,12 @@ export default class SelectModal extends PureComponent {
     this.setState({ selTableData: arr });
   }
 
+  // 单选模式,选择一条立即关闭模态框
+  handleSingleOnAdd = (record) => {
+    const { onOk } = this.props;
+    onOk(record);
+  }
+
   // 从待选列表中删除一条数据
   handleOnDel = (record) => {
     const { selTableData } = this.state;
@@ -120,9 +126,14 @@ export default class SelectModal extends PureComponent {
   }
 
   handleOnOk = () => {
-    const { selTableData } = this.state;
-    const { onOk } = this.props;
-    onOk(selTableData);
+    const { onOk, mode, onCancel } = this.props;
+    if (mode === 'multiple') {
+      const { selTableData } = this.state;
+      onOk(selTableData);
+    }
+    if (mode === 'single') {
+      onCancel();
+    }
   }
 
   comboConverter = () => {
@@ -164,8 +175,29 @@ export default class SelectModal extends PureComponent {
             onOk={this.handleOnOk}
             {...modalProps}
           >
-            <DataSearch {...searchProps} />
-            <ForSelTable {...forSelTableProps}/>
+            <DataSearch
+              field={searchField}
+              keyword={searchKeyWord}
+              size={searchSize}
+              select={searchSelect}
+              selectProps={searchSelectProps}
+              selectOptions={searchSelectOptions}
+              onSearch={this.handleOnSearch}
+            />
+            <ForSelTable
+              mode={mode}
+              rowKeyName={rowKeyName}
+              className={fsTableClassName}
+              dataSource={fsTableDataSource}
+              columns={fsTableColumns}
+              loading={fsTableLoading}
+              pagination={fsTablePagination}
+              onChange={this.handleOnChange}
+              onAdd={this.handleSingleOnAdd}
+              style={{ marginTop: 10, marginBottom: 10 }}
+              scroll={{ y: 300 }}
+              selected={selTableData}
+            />
           </Modal>
         );
         break;
@@ -190,6 +222,7 @@ export default class SelectModal extends PureComponent {
               <Button type="primary" onClick={this.handleTabChange} ghost={tableTabLeft ? true : false}>{`已选[${selTableData.length}]`}</Button>
             </ButtonGroup>
             <ForSelTable
+              mode={mode}
               rowKeyName={rowKeyName}
               className={fsTableClassName}
               dataSource={fsTableDataSource}

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

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

+ 138 - 0
src/models/course/detail.js

@@ -0,0 +1,138 @@
+import { queryOne, create, update } from '../../services/course';
+import { message } from 'antd';
+import pathToRegexp from 'path-to-regexp';
+import { Codes } from '../../utils/config';
+
+export default {
+  namespace: 'courseDetail',
+
+  state: {
+    filters: {},
+    operType: 'create',
+    currentItem: {},
+    lessonModalVisible: false,
+    resourceModalVisible: false,
+    supportModalVisible: false,
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state, ...rest }) => {
+        const match = pathToRegexp('/product/course/edit/:id').exec(pathname);
+        if (match) {
+          dispatch({ type: 'query', payload: { id: match[1] } });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'update' } });
+        }
+        if (pathname === '/product/course/add') {
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'create' } });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { itemLoading: true } });
+      const { data, success } = yield call(queryOne, payload);
+      if (success) {
+        yield put({ type: 'querySuccess', payload: { ...data } });
+      }
+      yield put({ type: 'changeLoading', payload: { itemLoading: false } });
+    },
+    * create ({ payload, callback }, { call, put }) {
+      // 创建课程,默认状态为NORMAL
+      const { data, success } = yield call(create, { ...payload, status: Codes.CODE_NORMAL });
+      if (success) {
+        message.success('创建成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('更新成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    }
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    querySuccess(state, { payload }) {
+      return { ...state, currentItem: payload };
+    },
+
+    saveFilters(state, { payload: filters }) {
+      return { ...state, filters };
+    },
+
+    showLessonModal(state, { payload }) {
+      return { ...state, ...payload, lessonModalVisible: true };
+    },
+
+    hideLessonModal(state) {
+      return { ...state, lessonModalVisible: false };
+    },
+
+    showSupportModal(state, { payload }) {
+      return { ...state, ...payload, supportModalVisible: true };
+    },
+
+    hideSupportModal(state) {
+      return { ...state, supportModalVisible: false };
+    },
+
+    showResourceModal(state, { payload }) {
+      return { ...state, ...payload, resourceModalVisible: true };
+    },
+
+    hideResourceModal(state) {
+      return { ...state, resourceModalVisible: false };
+    },
+
+    saveOperType(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    saveCoverImg(state, { payload: { cvImg } }) {
+      const currentItem = { ...state.currentItem, cvImg };
+      return { ...state, resourceModalVisible: false, currentItem };
+    },
+
+    saveBackgroundImg(state, { payload: { bgImg } }) {
+      const currentItem = { ...state.currentItem, bgImg };
+      return { ...state, resourceModalVisible: false, currentItem };
+    },
+
+    saveLessonList(state, { payload: { lessonList } }) {
+      const currentItem = { ...state.currentItem, lessonList };
+      return { ...state, lessonModalVisible: false, currentItem };
+    },
+
+    saveSupportList(state, { payload: { supportList } }) {
+      const currentItem = { ...state.currentItem, supportList };
+      return { ...state, supportModalVisible: false, currentItem };
+    },
+
+    clearPage(state) {
+      return { ...state, currentItem: {}, itemLoading: false };
+    }
+  }
+}

+ 119 - 0
src/models/support/detail.js

@@ -0,0 +1,119 @@
+import { queryOne, create, update } from '../../services/support';
+import { message } from 'antd';
+import pathToRegexp from 'path-to-regexp';
+import { Codes } from '../../utils/config';
+
+export default {
+  namespace: 'supportDetail',
+
+  state: {
+    filters: {},
+    operType: 'create',
+    currentItem: {},
+    supportModalVisible: false,
+    resourceModalVisible: false,
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state, ...rest }) => {
+        const match = pathToRegexp('/product/support/edit/:id').exec(pathname);
+        if (match) {
+          dispatch({ type: 'query', payload: { id: match[1] } });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'update' } });
+        }
+        if (pathname === '/product/support/add') {
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'create' } });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { itemLoading: true } });
+      const { data, success } = yield call(queryOne, payload);
+      if (success) {
+        yield put({ type: 'querySuccess', payload: { ...data } });
+      }
+      yield put({ type: 'changeLoading', payload: { itemLoading: false } });
+    },
+    * create ({ payload, callback }, { call, put }) {
+      // 创建课程,默认状态为NORMAL
+      const { data, success } = yield call(create, { ...payload, status: Codes.CODE_NORMAL });
+      if (success) {
+        message.success('创建成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('更新成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    }
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    querySuccess(state, { payload }) {
+      return { ...state, currentItem: payload };
+    },
+
+    saveFilters(state, { payload: filters }) {
+      return { ...state, filters };
+    },
+
+    showSupportModal(state, { payload }) {
+      return { ...state, ...payload, supportModalVisible: true };
+    },
+
+    hideSupportModal(state) {
+      return { ...state, supportModalVisible: false };
+    },
+
+    showResourceModal(state, { payload }) {
+      return { ...state, ...payload, resourceModalVisible: true };
+    },
+
+    hideResourceModal(state) {
+      return { ...state, resourceModalVisible: false };
+    },
+
+    saveOperType(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    saveSupportList(state, { payload: { supportList } }) {
+      const currentItem = { ...state.currentItem, supportList };
+      return { ...state, supportModalVisible: false, currentItem };
+    },
+
+    saveImgList(state, { payload: { imgList } }) {
+      const currentItem = { ...state.currentItem, imgList };
+      return { ...state, resourceModalVisible: false, currentItem };
+    },
+
+    clearPage(state) {
+      return { ...state, currentItem: {}, itemLoading: false };
+    }
+  }
+}

+ 104 - 0
src/models/support/support.js

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

+ 393 - 0
src/routes/Course/detail/index.js

@@ -0,0 +1,393 @@
+import React, { PureComponent } from 'react';
+import { routerRedux } from 'dva/router';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { Spin, Popover, Badge, Table, Radio, Card, Form, Input, Icon, Button, Select } from 'antd';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import LessonSelectSortModal from './lessonModal';
+import ResourceSelectModal from './resourceModal';
+import SupportSelectSortModal from './supportModal';
+import { Codes } from '../../../utils/config';
+
+const FormItem = Form.Item;
+const Option = Select.Option;
+const { Meta } = Card;
+const { TextArea } = Input;
+
+@Form.create()
+@connect(state => ({
+  courseDetail: state.courseDetail,
+  resource: state.resource,
+  lesson: state.lesson,
+  support: state.support,
+}))
+export default class CourseDetail extends PureComponent {
+  state = {
+    curClickedBtn: null,       //记录点击的按钮
+  };
+
+  // 展示选择模态框 - 加载第一页数据
+  handleModalShow = (btnName) => {
+    this.setState({
+      curClickedBtn: btnName,
+    }, () => {
+      const { dispatch } = this.props;
+      if (btnName === 'lessonBtn') {
+        dispatch({ type: 'courseDetail/showLessonModal' });
+        dispatch({
+          type: 'lesson/query',
+          payload: {
+            pageNo: 1,
+            pageSize: 10,
+            status: Codes.CODE_NORMAL,
+          }
+        });
+      } else if (btnName === 'cvImgBtn' || btnName === 'bgImgBtn') {
+        dispatch({ type: 'courseDetail/showResourceModal' });
+        dispatch({
+          type: 'resource/query',
+          payload: {
+            pageNo: 1,
+            pageSize: 10,
+            status: Codes.CODE_NORMAL,
+            type: Codes.CODE_IMAGE,
+          }
+        });
+      } else if (btnName === 'supportBtn') {
+        dispatch({ type: 'courseDetail/showSupportModal' });
+        dispatch({
+          type: 'support/query',
+          payload: {
+            pageNo: 1,
+            pageSize: 10,
+            status: Codes.CODE_NORMAL,
+          }
+        });
+      }
+    })
+  }
+
+  // 取消/关闭 - 隐藏选择模态框
+  handleModalCancel = () => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    if (curClickedBtn === 'lessonBtn') {
+      dispatch({ type: 'courseDetail/hideLessonModal' });
+    } else if (curClickedBtn === 'cvImgBtn' || curClickedBtn === 'bgImgBtn') {
+      dispatch({ type: 'courseDetail/hideResourceModal' });
+    } else if (curClickedBtn === 'supportBtn') {
+      dispatch({ type: 'courseDetail/hideSupportModal' });
+    }
+  }
+
+  // 提交 - 保存选择和排序完的数据到model中
+  handleModalOk = (data) => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    if (curClickedBtn === 'lessonBtn') {
+      dispatch({
+        type: 'courseDetail/saveLessonList',
+        payload: { lessonList: data }
+      });
+    } else if (curClickedBtn === 'cvImgBtn') {
+      dispatch({
+        type: 'courseDetail/saveCoverImg',
+        payload: { cvImg: data },
+      });
+    } else if (curClickedBtn === 'bgImgBtn') {
+      dispatch({
+        type: 'courseDetail/saveBackgroundImg',
+        payload: { bgImg: data },
+      });
+    } else if (curClickedBtn === 'supportBtn') {
+      dispatch({
+        type: 'courseDetail/saveSupportList',
+        payload: { supportList: data }
+      });
+    }
+  }
+
+  // 搜索
+  handleModalSearch = (data) => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    const newData = { ...data };
+    if (newData.keyword) {
+      newData[newData.field] = newData.keyword;
+      delete newData.field;
+      delete newData.keyword;
+    } else {
+      delete newData.field;
+      delete newData.keyword;
+    }
+    if (curClickedBtn === 'lessonBtn') {
+      dispatch({
+        type: `lesson/query`,
+        payload: { ...newData, pageNo: 1, pageSize: 10, status: Codes.CODE_NORMAL },
+      });
+    } else if (curClickedBtn === 'cvImgBtn' || curClickedBtn === 'bgImgBtn') {
+      dispatch({
+        type: `resource/query`,
+        payload: { ...newData, pageNo: 1, pageSize: 10, status: Codes.CODE_NORMAL, type: Codes.CODE_IMAGE },
+      });
+    }
+  }
+
+  // 翻页 - 资源列表
+  handleModalTableChange = (pagination, filterArgs, filters) => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    const newFilters = { ...filters };
+    if (newFilters.keyword) {
+      newFilters[newFilters.field] = newFilters.keyword;
+      delete newFilters.field;
+      delete newFilters.keyword;
+    } else {
+      delete newFilters.field;
+      delete newFilters.keyword;
+    }
+    const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+    const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = { ...obj };
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const data = { ...newFilters, ...tableFilters, pageNo: pagination.current, pageSize: pagination.pageSize };
+    Object.keys(data).map(key => data[key] ? null : delete data[key]);
+
+    if (curClickedBtn === 'lessonBtn') {
+      dispatch({ type: `lesson/query`, payload: { ...data, status: Codes.CODE_NORMAL } });
+    } else if (curClickedBtn === 'cvImgBtn' || curClickedBtn === 'bgImgBtn') {
+      dispatch({ type: `resource/query`, payload: { ...data, status: Codes.CODE_NORMAL, type: Codes.CODE_IMAGE } });
+    } else if (curClickedBtn === 'supportBtn') {
+      dispatch({ type: `support/query`, payload: { ...data, status: Codes.CODE_NORMAL } });
+    }
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault()
+    const {
+      dispatch,
+      form: {
+        validateFields,
+        getFieldsValue,
+        resetFields
+      },
+      courseDetail: {
+        operType,
+        currentItem,
+        filters,
+      }
+    } = this.props;
+    validateFields((errors) => {
+      if (errors) { return; }
+      const data = {
+        ...currentItem,
+        ...getFieldsValue(),
+      };
+      dispatch({
+        type: `courseDetail/${operType}`,
+        payload: data,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname: '/product/course',
+              search: queryString.stringify(filters),
+            })
+          );
+        }
+      })
+      resetFields();
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch, courseDetail: { filters } } = this.props;
+    dispatch({ type: 'courseDetail/clearPage' });
+    dispatch(
+      routerRedux.push({
+        pathname: '/product/course',
+        search: queryString.stringify(filters),
+      })
+    );
+  }
+
+  render() {
+    const { dispatch, form: { getFieldDecorator }, courseDetail, lesson, resource, support } = this.props;
+    const { itemLoading, currentItem, filters, lessonModalVisible, supportModalVisible, resourceModalVisible } = courseDetail;
+    const { lessonList = [], supportList = [], name, code, digest, cvImg = {}, bgImg = {} } = currentItem;
+
+    // 待选表格去掉分页的跳转及变换页码
+    if (resource && resource.pagination) {
+      delete resource.pagination.showQuickJumper;
+      delete resource.pagination.showSizeChanger;
+    }
+
+    const lessonTableColumns = [{
+      title: '课编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '课名称',
+      dataIndex: 'name',
+      key: 'name',
+    }];
+
+    const supportTableColumns = [{
+      title: '配套编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '配套名称',
+      dataIndex: 'name',
+      key: 'name',
+    }]
+
+    const formItemLayout = {
+      labelCol: {
+        span: 7,
+      },
+      wrapperCol: {
+        span: 12,
+      },
+    };
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Spin spinning={itemLoading}>
+          <Card title="课程信息">
+            <Form layout="horizontal" onSubmit={this.handlePageSubmit}>
+              <FormItem label="课程编号:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('code', {
+                  rules: [{ required: true, type: 'string', message: "编号为必填项!" }],
+                  initialValue: code,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="课程名称:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('name', {
+                  rules: [{ required: true, type: 'string', message: "名称为必填项!" }],
+                  initialValue: name,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="课程简述:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('digest', {
+                  initialValue: digest,
+                })(<TextArea />)}
+              </FormItem>
+              <FormItem label="课程封面" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('cvImgBtn')} type="primary" icon="select" size="small">选择</Button>
+                {cvImg.url === undefined ? null :
+                  <Card
+                    hoverable
+                    bordered
+                    cover={<img alt="图片加载失败" src={cvImg.url} />}
+                    style={{ width: 240, marginTop: 20 }}
+                  >
+                  </Card>}
+              </FormItem>
+              <FormItem label="课程底图" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('bgImgBtn')} type="primary" icon="select" size="small">选择</Button>
+                {bgImg.url === undefined ? null :
+                  <Card
+                  hoverable
+                  bordered
+                  cover={<img alt="图片加载失败" src={bgImg.url} />}
+                  style={{ width: 240, marginTop: 20 }}
+                >
+                </Card>}
+              </FormItem>
+              <FormItem label="选择课" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('lessonBtn')} type="primary" size="small" icon="edit">编辑</Button>
+              </FormItem>
+              <FormItem wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      该课程下不包含任何课,请选择!</span>
+                  }}
+                  dataSource={lessonList}
+                  columns={lessonTableColumns}
+                  rowKey={record => record.id}
+                  bordered
+                  pagination={false}
+                />
+              </FormItem>
+              <FormItem label="相关配套" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('supportBtn')} type="primary" size="small" icon="edit">编辑</Button>
+              </FormItem>
+              <FormItem wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      该课程暂无相关配套,请选择!</span>
+                  }}
+                  dataSource={supportList}
+                  columns={supportTableColumns}
+                  rowKey={record => record.id}
+                  bordered
+                  pagination={false}
+                />
+              </FormItem>
+              <FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
+                <Button onClick={this.handlePageCancel}>取消</Button>
+                <Button type="primary" style={{ marginLeft: 35 }} htmlType="submit">提交</Button>
+              </FormItem>
+            </Form>
+            {/*课的模态选择框*/}
+            <LessonSelectSortModal
+              rowKeyName="id"
+              modalVisible={lessonModalVisible}
+              style={{ top: 20 }}
+              width={600}
+              onCancel={this.handleModalCancel}
+              onOk={this.handleModalOk}
+              onSearch={this.handleModalSearch}
+              selTableData={lessonList}
+              fsTableDataSource={lesson.list || []}
+              fsTableLoading={lesson.listLoading}
+              fsTablePagination={lesson.pagination}
+              fsTableOnChange={this.handleModalTableChange}
+            />
+            {/*图片资源的模态选择框*/}
+            <ResourceSelectModal
+              rowKeyName="id"
+              modalVisible={resourceModalVisible}
+              style={{ top: 20 }}
+              width={600}
+              onOk={this.handleModalOk}
+              onCancel={this.handleModalCancel}
+              onSearch={this.handleModalSearch}
+              fsTableDataSource={resource.list || []}
+              fsTableLoading={resource.listLoading}
+              fsTablePagination={resource.pagination}
+              fsTableOnChange={this.handleModalTableChange}
+            />
+            {/*周边的模态选择框*/}
+            <SupportSelectSortModal
+              rowKeyName="id"
+              modalVisible={supportModalVisible}
+              style={{ top: 20 }}
+              width={600}
+              onCancel={this.handleModalCancel}
+              onOk={this.handleModalOk}
+              onSearch={this.handleModalSearch}
+              selTableData={supportList}
+              fsTableDataSource={support.list || []}
+              fsTableLoading={support.listLoading}
+              fsTablePagination={support.pagination}
+              fsTableOnChange={this.handleModalTableChange}
+            />
+          </Card>
+        </Spin>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 87 - 0
src/routes/Course/detail/lessonModal.js

@@ -0,0 +1,87 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge, Popover, Icon } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import styles from './lessonModal.less';
+import { Codes, resourceType } from '../../../utils/config';
+
+export default class LessonSelectSortModal extends PureComponent {
+  static propTypes = {
+    selTableData: PropTypes.array.isRequired,
+    modalVisible: PropTypes.bool.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const { selTableData, modalVisible, rowKeyName, onCancel, onOk, onSearch, ...fsTableOpts } = this.props;
+
+    const modalProps = {
+      title: '选择课',
+      maskClosable: false,
+      visible: modalVisible,
+      onCancel,
+      onOk,
+    };
+
+    const searchProps = {
+      searchField: 'name',
+      searchKeyWord: '',
+      searchSize: 'default',
+      searchSelect: true,
+      searchSelectOptions: [{
+        value: 'name', name: '课名称', mode: 'input',
+      },{
+        value: 'code', name: '课编号', mode: 'input',
+      }],
+      searchSelectProps: {
+        defaultValue: 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    const selTableProps = {
+      operDel: true,
+      operSort: true,
+      tableClassName: styles.sTable,
+      tablePagination: false,
+      tableDataSource: selTableData,
+      rowKeyName: rowKeyName,
+      tableColumns: [{
+        title: '课编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '课名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+    };
+
+    //待选资源Table属性
+    const fsTableProps = {
+      fsTableClassName: styles.fsTable,
+      fsTableColumns: [{
+        title: '课编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '课名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+      ...fsTableOpts,
+    }
+
+    return (
+      <SelectModal
+        mode="multiple"
+        { ...searchProps }
+        { ...modalProps }
+        { ...selTableProps }
+        { ...fsTableProps }
+      />
+    );
+  }
+}

+ 120 - 0
src/routes/Course/detail/lessonModal.less

@@ -0,0 +1,120 @@
+.fsTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 40%;
+      }
+
+      &:nth-child(2) {
+        width: 40%;
+      }
+
+      &:nth-child(3) {
+        width: 20%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}
+
+
+.sTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 30%;
+      }
+
+      &:nth-child(2) {
+        width: 30%;
+      }
+
+      &:nth-child(3) {
+        width: 20%;
+      }
+
+      &:nth-child(4) {
+        width: 20%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}

+ 79 - 0
src/routes/Course/detail/resourceModal.js

@@ -0,0 +1,79 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge, Popover, Icon } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import styles from './resourceModal.less';
+import { Codes } from '../../../utils/config';
+
+export default class ResourceSelectModal extends PureComponent {
+  static propTypes = {
+    modalVisible: PropTypes.bool.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const { modalVisible, onCancel, onOk, onSearch, ...fsTableOpts } = this.props;
+
+    const modalProps = {
+      title: '选择图片',
+      maskClosable: false,
+      visible: modalVisible,
+      onCancel,
+      onOk,
+    };
+
+    const searchProps = {
+      searchField: 'name',
+      searchKeyWord: '',
+      searchSize: 'default',
+      searchSelect: true,
+      searchSelectOptions: [{
+        value: 'name', name: '图片名称', mode: 'input',
+      },{
+        value: 'code', name: '图片编号', mode: 'input',
+      }],
+      searchSelectProps: {
+        defaultValue: 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    //待选资源Table属性
+    const fsTableProps = {
+      fsTableClassName: styles.fsTable,
+      fsTableColumns: [{
+        title: '缩略图',
+        dataIndex: 'url',
+        key: 'url',
+        render: (text, record) => (
+          <Popover
+            content={<img alt="" src={record.url} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={record.url} width={70} />
+          </Popover>
+        ),
+      },{
+        title: '图片编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '图片名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+      ...fsTableOpts,
+    }
+
+    return (
+      <SelectModal
+        mode="single"
+        { ...searchProps }
+        { ...fsTableProps }
+        { ...modalProps }
+      />
+    );
+  }
+}

+ 61 - 0
src/routes/Course/detail/resourceModal.less

@@ -0,0 +1,61 @@
+.fsTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 27%;
+      }
+
+      &:nth-child(2) {
+        width: 27%;
+      }
+
+      &:nth-child(3) {
+        width: 27%;
+      }
+
+      &:nth-child(4) {
+        width: 19%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}

+ 87 - 0
src/routes/Course/detail/supportModal.js

@@ -0,0 +1,87 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge, Popover, Icon } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import styles from './supportModal.less';
+import { Codes } from '../../../utils/config';
+
+export default class SupportSelectSortModal extends PureComponent {
+  static propTypes = {
+    selTableData: PropTypes.array.isRequired,
+    modalVisible: PropTypes.bool.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const { selTableData, modalVisible, rowKeyName, onCancel, onOk, onSearch, ...fsTableOpts } = this.props;
+
+    const modalProps = {
+      title: '选则相关配套',
+      maskClosable: false,
+      visible: modalVisible,
+      onCancel,
+      onOk,
+    };
+
+    const searchProps = {
+      searchField: 'name',
+      searchKeyWord: '',
+      searchSize: 'default',
+      searchSelect: true,
+      searchSelectOptions: [{
+        value: 'name', name: '配套名称', mode: 'input',
+      },{
+        value: 'code', name: '配套编号', mode: 'input',
+      }],
+      searchSelectProps: {
+        defaultValue: 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    const selTableProps = {
+      operDel: true,
+      operSort: true,
+      tableClassName: styles.sTable,
+      tablePagination: false,
+      tableDataSource: selTableData,
+      rowKeyName: rowKeyName,
+      tableColumns: [{
+        title: '配套编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '配套名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+    };
+
+    //待选资源Table属性
+    const fsTableProps = {
+      fsTableClassName: styles.fsTable,
+      fsTableColumns: [{
+        title: '配套编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '配套名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+      ...fsTableOpts,
+    }
+
+    return (
+      <SelectModal
+        mode="multiple"
+        { ...searchProps }
+        { ...modalProps }
+        { ...selTableProps }
+        { ...fsTableProps }
+      />
+    );
+  }
+}

+ 120 - 0
src/routes/Course/detail/supportModal.less

@@ -0,0 +1,120 @@
+.fsTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 40%;
+      }
+
+      &:nth-child(2) {
+        width: 40%;
+      }
+
+      &:nth-child(3) {
+        width: 20%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}
+
+
+.sTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 30%;
+      }
+
+      &:nth-child(2) {
+        width: 30%;
+      }
+
+      &:nth-child(3) {
+        width: 20%;
+      }
+
+      &:nth-child(4) {
+        width: 20%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}

+ 135 - 0
src/routes/Course/index.js

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

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

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

+ 101 - 0
src/routes/Course/table.js

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

+ 78 - 0
src/routes/Course/table.less

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

+ 1 - 20
src/routes/Lesson/detail/index.js

@@ -139,7 +139,6 @@ export default class LessonDetail extends PureComponent {
     const { itemLoading, currentItem, filters, modalVisible } = lessonDetail;
     const { wareList, name, code, digest } = currentItem;
     const { list, listLoading, pagination } = ware;
-    console.log(wareList);
 
     // 待选表格去掉分页的跳转及变换页码
     if (pagination) {
@@ -194,24 +193,6 @@ export default class LessonDetail extends PureComponent {
                   initialValue: digest,
                 })(<TextArea />)}
               </FormItem>
-              {/*内容提供商暂不用选择
-              <FormItem label="提供商" hasFeedback {...formItemLayout}>
-                {getFieldDecorator('groupId', {
-                  rules: [{ required: true, type: 'string', message: "标签组为必选项!" }],
-                  initialValue: groupId,
-                })(
-                  <Select
-                    showSearch
-                    allowClear
-                    placeholder="请输入标签组编号或者名称进行筛选"
-                    optionFilterProp="children"
-                    filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
-                  >
-                    {list.map(item => <Option value={item.id} key={item.id}>{`${item.code}/${item.name}`}</Option>)}
-                  </Select>
-                )}
-              </FormItem>
-              */}
               <FormItem label="选择课件" {...formItemLayout}>
                 <Button onClick={this.handleModalShow} type="primary" size="small" icon="edit">编辑</Button>
               </FormItem>
@@ -219,7 +200,7 @@ export default class LessonDetail extends PureComponent {
                 <Table
                   locale={{
                     emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
-                      该课件下不包含任何内容,请选择图片或视频!</span>
+                      该课下不包含任何内容,请选择相关课件!</span>
                   }}
                   dataSource={wareList}
                   columns={subTableColumns}

+ 348 - 0
src/routes/Support/detail/index.js

@@ -0,0 +1,348 @@
+import React, { PureComponent } from 'react';
+import { routerRedux } from 'dva/router';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { Spin, Popover, Badge, Table, Radio, Card, Form, Input, Icon, Button, Select } from 'antd';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import SupportSelectSortModal from './supportModal';
+import ResourceSelectModal from './resourceModal';
+import { Codes } from '../../../utils/config';
+
+const FormItem = Form.Item;
+const Option = Select.Option;
+const { TextArea } = Input;
+
+@Form.create()
+@connect(state => ({
+  resource: state.resource,
+  support: state.support,
+  supportDetail: state.supportDetail,
+}))
+export default class SupportDetail extends PureComponent {
+  static propTypes = {
+    supportDetail: PropTypes.object,
+  };
+
+  state = {
+    curClickedBtn: null,       //记录点击的按钮
+  };
+
+  // 展示选择模态框 - 加载第一页数据
+  handleModalShow = (btnName) => {
+    this.setState({
+      curClickedBtn: btnName,
+    }, () => {
+      const { dispatch } = this.props;
+      if (btnName === 'supportBtn') {
+        dispatch({ type: 'supportDetail/showSupportModal' });
+        dispatch({
+          type: 'support/query',
+          payload: {
+            pageNo: 1,
+            pageSize: 10,
+            status: Codes.CODE_NORMAL,
+          }
+        });
+      }
+      if (btnName === 'imgBtn') {
+        dispatch({ type: 'supportDetail/showResourceModal' });
+        dispatch({
+          type: 'resource/query',
+          payload: {
+            pageNo: 1,
+            pageSize: 10,
+            status: Codes.CODE_NORMAL,
+            type: Codes.CODE_IMAGE,
+          }
+        });
+      }
+    })
+  }
+
+  // 取消/关闭 - 隐藏选择模态框
+  handleModalCancel = () => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    if (curClickedBtn === 'supportBtn') {
+      dispatch({ type: 'supportDetail/hideSupportModal' });
+    }
+    if (curClickedBtn === 'imgBtn') {
+      dispatch({ type: 'supportDetail/hideResourceModal' });
+    }
+  }
+
+  // 提交 - 保存选择和排序完的数据到model中
+  handleModalOk = (data) => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    if (curClickedBtn === 'supportBtn') {
+      dispatch({
+        type: 'supportDetail/saveSupportList',
+        payload: { supportList: data }
+      });
+    } else if (curClickedBtn === 'imgBtn') {
+      dispatch({
+        type: 'supportDetail/saveImgList',
+        payload: { imgList: data },
+      });
+    }
+  }
+
+  // 搜索
+  handleModalSearch = (data) => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    const newData = { ...data };
+    if (newData.keyword) {
+      newData[newData.field] = newData.keyword;
+      delete newData.field;
+      delete newData.keyword;
+    } else {
+      delete newData.field;
+      delete newData.keyword;
+    }
+    if (curClickedBtn === 'supportBtn') {
+      dispatch({
+        type: `support/query`,
+        payload: { ...newData, pageNo: 1, pageSize: 10, status: Codes.CODE_NORMAL },
+      });
+    } else if (curClickedBtn === 'imgBtn') {
+      dispatch({
+        type: `resource/query`,
+        payload: { ...newData, pageNo: 1, pageSize: 10, status: Codes.CODE_NORMAL, type: Codes.CODE_IMAGE },
+      });
+    }
+  }
+
+  // 翻页 - 资源列表
+  handleModalTableChange = (pagination, filterArgs, filters) => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    const newFilters = { ...filters };
+    if (newFilters.keyword) {
+      newFilters[newFilters.field] = newFilters.keyword;
+      delete newFilters.field;
+      delete newFilters.keyword;
+    } else {
+      delete newFilters.field;
+      delete newFilters.keyword;
+    }
+    const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+    const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = { ...obj };
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const data = { ...newFilters, ...tableFilters, pageNo: pagination.current, pageSize: pagination.pageSize };
+    Object.keys(data).map(key => data[key] ? null : delete data[key]);
+
+    if (curClickedBtn === 'supportBtn') {
+      dispatch({ type: `support/query`, payload: { ...data, status: Codes.CODE_NORMAL } });
+    } else if (curClickedBtn === 'imgBtn') {
+      dispatch({ type: `resource/query`, payload: { ...data, status: Codes.CODE_NORMAL, type: Codes.CODE_IMAGE } });
+    }
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault()
+    const {
+      dispatch,
+      form: {
+        validateFields,
+        getFieldsValue,
+        resetFields
+      },
+      supportDetail: {
+        operType,
+        currentItem,
+        filters,
+      }
+    } = this.props;
+    validateFields((errors) => {
+      if (errors) { return; }
+      const data = {
+        ...currentItem,
+        ...getFieldsValue(),
+      };
+      dispatch({
+        type: `supportDetail/${operType}`,
+        payload: data,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname: '/product/support',
+              search: queryString.stringify(filters),
+            })
+          );
+        }
+      })
+      resetFields();
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch, supportDetail: { filters } } = this.props;
+    dispatch({ type: 'supportDetail/clearPage' });
+    dispatch(
+      routerRedux.push({
+        pathname: '/product/support',
+        search: queryString.stringify(filters),
+      })
+    );
+  }
+
+  render() {
+    const { dispatch, form: { getFieldDecorator }, resource, support, supportDetail } = this.props;
+    const { itemLoading, currentItem, filters, supportModalVisible, resourceModalVisible } = supportDetail;
+    const { imgList = [], supportList = [], name, code, digest } = currentItem;
+
+    // 待选表格去掉分页的跳转及变换页码
+    if (resource && resource.pagination) {
+      delete resource.pagination.showQuickJumper;
+      delete resource.pagination.showSizeChanger;
+    }
+
+    const supportTableColumns = [{
+      title: '配套编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '配套名称',
+      dataIndex: 'name',
+      key: 'name',
+    }];
+
+    const imgTableColumns = [{
+      title: '缩略图',
+      dataIndex: 'url',
+      key: 'url',
+      render: (text, record) => (
+          <Popover
+            content={<img alt="" src={record.url} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={record.url} width={70} />
+          </Popover>
+      ),
+    },{
+      title: '图片编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '图片名称',
+      dataIndex: 'name',
+      key: 'name',
+    }];
+
+    const formItemLayout = {
+      labelCol: {
+        span: 7,
+      },
+      wrapperCol: {
+        span: 12,
+      },
+    };
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Spin spinning={itemLoading}>
+          <Card title="配套信息">
+            <Form layout="horizontal" onSubmit={this.handlePageSubmit}>
+              <FormItem label="配套编号:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('code', {
+                  rules: [{ required: true, type: 'string', message: "编号为必填项!" }],
+                  initialValue: code,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="配套名称:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('name', {
+                  rules: [{ required: true, type: 'string', message: "名称为必填项!" }],
+                  initialValue: name,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="配套简述:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('digest', {
+                  initialValue: digest,
+                })(<TextArea />)}
+              </FormItem>
+              <FormItem label="选择图片" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('imgBtn')} type="primary" size="small" icon="edit">编辑</Button>
+              </FormItem>
+              <FormItem wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      该配套下不包含任何图片,请选择!</span>
+                  }}
+                  dataSource={imgList}
+                  columns={imgTableColumns}
+                  rowKey={record => record.id}
+                  bordered
+                  pagination={false}
+                />
+              </FormItem>
+              <FormItem label="相关配套" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('supportBtn')} type="primary" size="small" icon="edit">编辑</Button>
+              </FormItem>
+              <FormItem wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      暂无与该配套相关的配套,请选择!</span>
+                  }}
+                  dataSource={supportList}
+                  columns={supportTableColumns}
+                  rowKey={record => record.id}
+                  bordered
+                  pagination={false}
+                />
+              </FormItem>
+              <FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
+                <Button onClick={this.handlePageCancel}>取消</Button>
+                <Button type="primary" style={{ marginLeft: 35 }} htmlType="submit">提交</Button>
+              </FormItem>
+            </Form>
+            {/*周边的模态选择框*/}
+            <SupportSelectSortModal
+              rowKeyName="id"
+              modalVisible={supportModalVisible}
+              style={{ top: 20 }}
+              width={600}
+              onCancel={this.handleModalCancel}
+              onOk={this.handleModalOk}
+              onSearch={this.handleModalSearch}
+              selTableData={supportList}
+              fsTableDataSource={support.list || []}
+              fsTableLoading={support.listLoading}
+              fsTablePagination={support.pagination}
+              fsTableOnChange={this.handleModalTableChange}
+            />
+            {/*图片资源的模态选择框*/}
+            <ResourceSelectModal
+              rowKeyName="id"
+              modalVisible={resourceModalVisible}
+              style={{ top: 20 }}
+              width={600}
+              onOk={this.handleModalOk}
+              onCancel={this.handleModalCancel}
+              onSearch={this.handleModalSearch}
+              selTableData={imgList}
+              fsTableDataSource={resource.list || []}
+              fsTableLoading={resource.listLoading}
+              fsTablePagination={resource.pagination}
+              fsTableOnChange={this.handleModalTableChange}
+            />
+          </Card>
+        </Spin>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 109 - 0
src/routes/Support/detail/resourceModal.js

@@ -0,0 +1,109 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge, Popover, Icon } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import styles from './resourceModal.less';
+import { Codes } from '../../../utils/config';
+
+export default class ResourceSelectModal extends PureComponent {
+  static propTypes = {
+    modalVisible: PropTypes.bool.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const { selTableData, modalVisible, onCancel, onOk, onSearch, ...fsTableOpts } = this.props;
+
+    const modalProps = {
+      title: '选择图片',
+      maskClosable: false,
+      visible: modalVisible,
+      onCancel,
+      onOk,
+    };
+
+    const searchProps = {
+      searchField: 'name',
+      searchKeyWord: '',
+      searchSize: 'default',
+      searchSelect: true,
+      searchSelectOptions: [{
+        value: 'name', name: '图片名称', mode: 'input',
+      },{
+        value: 'code', name: '图片编号', mode: 'input',
+      }],
+      searchSelectProps: {
+        defaultValue: 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    const selTableProps = {
+      operDel: true,
+      operSort: true,
+      tableClassName: styles.sTable,
+      tablePagination: false,
+      tableDataSource: selTableData,
+      tableColumns: [{
+        title: '缩略图',
+        dataIndex: 'url',
+        key: 'url',
+        render: (text, record) => (
+          <Popover
+            content={<img alt="" src={record.url} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={record.url} width={70} />
+          </Popover>
+        ),
+      },{
+        title: '图片编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '图片名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+    };
+
+    //待选资源Table属性
+    const fsTableProps = {
+      fsTableClassName: styles.fsTable,
+      fsTableColumns: [{
+        title: '缩略图',
+        dataIndex: 'url',
+        key: 'url',
+        render: (text, record) => (
+          <Popover
+            content={<img alt="" src={record.url} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={record.url} width={70} />
+          </Popover>
+        ),
+      },{
+        title: '图片编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '图片名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+      ...fsTableOpts,
+    }
+
+    return (
+      <SelectModal
+        mode="multiple"
+        { ...searchProps }
+        { ...selTableProps}
+        { ...fsTableProps }
+        { ...modalProps }
+      />
+    );
+  }
+}

+ 127 - 0
src/routes/Support/detail/resourceModal.less

@@ -0,0 +1,127 @@
+.fsTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 27%;
+      }
+
+      &:nth-child(2) {
+        width: 27%;
+      }
+
+      &:nth-child(3) {
+        width: 27%;
+      }
+
+      &:nth-child(4) {
+        width: 19%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}
+
+.sTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 20%;
+      }
+
+      &:nth-child(2) {
+        width: 25%;
+      }
+
+      &:nth-child(3) {
+        width: 25%;
+      }
+
+      &:nth-child(4) {
+        width: 20%;
+      }
+
+      &:nth-child(5) {
+        width: 10%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}

+ 87 - 0
src/routes/Support/detail/supportModal.js

@@ -0,0 +1,87 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge, Popover, Icon } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import styles from './supportModal.less';
+import { Codes } from '../../../utils/config';
+
+export default class SupportSelectSortModal extends PureComponent {
+  static propTypes = {
+    selTableData: PropTypes.array.isRequired,
+    modalVisible: PropTypes.bool.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const { selTableData, modalVisible, rowKeyName, onCancel, onOk, onSearch, ...fsTableOpts } = this.props;
+
+    const modalProps = {
+      title: '选则相关配套',
+      maskClosable: false,
+      visible: modalVisible,
+      onCancel,
+      onOk,
+    };
+
+    const searchProps = {
+      searchField: 'name',
+      searchKeyWord: '',
+      searchSize: 'default',
+      searchSelect: true,
+      searchSelectOptions: [{
+        value: 'name', name: '配套名称', mode: 'input',
+      },{
+        value: 'code', name: '配套编号', mode: 'input',
+      }],
+      searchSelectProps: {
+        defaultValue: 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    const selTableProps = {
+      operDel: true,
+      operSort: true,
+      tableClassName: styles.sTable,
+      tablePagination: false,
+      tableDataSource: selTableData,
+      rowKeyName: rowKeyName,
+      tableColumns: [{
+        title: '配套编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '配套名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+    };
+
+    //待选资源Table属性
+    const fsTableProps = {
+      fsTableClassName: styles.fsTable,
+      fsTableColumns: [{
+        title: '配套编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '配套名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+      ...fsTableOpts,
+    }
+
+    return (
+      <SelectModal
+        mode="multiple"
+        { ...searchProps }
+        { ...modalProps }
+        { ...selTableProps }
+        { ...fsTableProps }
+      />
+    );
+  }
+}

+ 120 - 0
src/routes/Support/detail/supportModal.less

@@ -0,0 +1,120 @@
+.fsTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 40%;
+      }
+
+      &:nth-child(2) {
+        width: 40%;
+      }
+
+      &:nth-child(3) {
+        width: 20%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}
+
+
+.sTable {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 50px;
+    }
+  }
+
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      &:nth-child(1) {
+        width: 30%;
+      }
+
+      &:nth-child(2) {
+        width: 30%;
+      }
+
+      &:nth-child(3) {
+        width: 20%;
+      }
+
+      &:nth-child(4) {
+        width: 20%;
+      }
+    }
+
+    .ant-table-thead {
+      & > tr {
+        transition: none;
+        display: block;
+
+        & > th {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+      }
+    }
+
+    .ant-table-tbody {
+      & > tr {
+        transition: none;
+        display: block;
+        border-bottom: 1px solid #f5f5f5;
+
+        & > td {
+          border-bottom: none;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+        &.ant-table-expanded-row-level-1 > td {
+          height: auto;
+        }
+      }
+    }
+  }
+}

+ 135 - 0
src/routes/Support/index.js

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

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

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

+ 101 - 0
src/routes/Support/table.js

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

+ 78 - 0
src/routes/Support/table.less

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

+ 1 - 1
src/routes/Ware/detail/modal.js

@@ -85,7 +85,7 @@ export default class ResourceSelectSortModal extends PureComponent {
         dataIndex: 'url',
         key: 'url',
         render: (text, record) => (
-          record.type !== Codes.CODE_IMAGE ? <Icon type="video-camera" style={{ fontSize: 40}} /> :
+          Number(record.type) !== Codes.CODE_IMAGE ? <Icon type="video-camera" style={{ fontSize: 40}} /> :
             <Popover
               content={<img alt="" src={record.url} width={350} />}
               title={record.name}

+ 32 - 0
src/services/course.js

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

+ 32 - 0
src/services/support.js

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

+ 4 - 0
src/utils/api.js

@@ -22,6 +22,10 @@ module.exports = {
   ware: `${config.apiHost}/ware/:id`,
   lessons: `${config.apiHost}/lesson/list`,
   lesson: `${config.apiHost}/lesson/:id`,
+  courses: `${config.apiHost}/course/list`,
+  course: `${config.apiHost}/course/:id`,
+  supports: `${config.apiHost}/support/list`,
+  support: `${config.apiHost}/support/:id`,
   orders: `${config.apiHost}/orders`,
   order: `${config.apiHost}/order/:id`,
 };