Bläddra i källkod

增加师训管理模块

zhanghe 6 år sedan
förälder
incheckning
2c5d904899

+ 3 - 0
src/common/menu.js

@@ -47,6 +47,9 @@ const menuData = [
       name: '配套管理',
       path: 'support',
     },{
+      name: '师训管理',
+      path: 'training',
+    },{
       name: '课程包管理',
       path: 'package',
     }]

+ 12 - 1
src/common/router.js

@@ -150,6 +150,17 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['support/detail', 'resource', 'support/support', 'merchant/merchant'], () => import('../routes/Support/Edit')),
       name: '修改配套',
     },
+    '/product/training': {
+      component: dynamicWrapper(app, ['training/training'], () => import('../routes/Training/List')),
+    },
+    '/product/training/add': {
+      component: dynamicWrapper(app, ['training/detail', 'resource', 'merchant/merchant'], () => import('../routes/Training/Edit')),
+      name: '创建师训',
+    },
+    '/product/training/edit/:id': {
+      component: dynamicWrapper(app, ['training/detail', 'resource', 'merchant/merchant'], () => import('../routes/Training/Edit')),
+      name: '修改师训',
+    },
     '/product/package': {
       component: dynamicWrapper(app, ['combo/combo'], () => import('../routes/Combo/List')),
     },
@@ -165,7 +176,7 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['mproduct/mproduct', 'merchant/merchant'], () => import('../routes/MProduct/List')),
     },
     '/goods/add': {
-      component: dynamicWrapper(app, ['course/course', 'support/support', 'combo/combo', 'merchant/merchant', 'mproduct/detail'], () => import('../routes/MProduct/Add')),
+      component: dynamicWrapper(app, ['course/course', 'training/training', 'support/support', 'combo/combo', 'merchant/merchant', 'mproduct/detail'], () => import('../routes/MProduct/Add')),
       name: '创建商品',
     },
     '/goods/edit': {

+ 3 - 2
src/models/resource.js

@@ -69,12 +69,13 @@ export default modelExtend(pageModel, {
       }
     },
     * update ({ payload, callback }, { call, put }) {
+      /*不支持修改
       const { data, success } = yield call(update, payload);
       if (success) {
         message.success('修改成功!');
         if (callback) callback();
-        yield put({ type: 'hideModal' });
-      }
+      }*/
+      yield put({ type: 'hideModal' });
     },
     * delete ({ payload, callback }, { call, put }) {
       const { data, success } = yield call(remove, payload);

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

@@ -0,0 +1,119 @@
+import { message } from 'antd';
+import { queryOne, create, update } from '../../services/product';
+import pathToRegexp from 'path-to-regexp';
+import { Codes } from '../../utils/config';
+
+export default {
+  namespace: 'trainingDetail',
+
+  state: {
+    filters: {},
+    operType: 'create',
+    currentItem: {},
+    resourceModalVisible: false,
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state, ...rest }) => {
+        const match = pathToRegexp('/product/training/edit/:pid').exec(pathname);
+        if (match) {
+          dispatch({ type: 'query', payload: { pid: match[1] } });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'update' } });
+        }
+        if (pathname == '/product/training/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 }) {
+      const { data, success } = yield call(create, payload);
+      if (success) {
+        message.success('创建成功!');
+        if (callback) callback();
+        yield put({ type: 'initState' });
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('修改成功!');
+        if (callback) callback();
+        yield put({ type: 'initState' });
+      }
+    }
+  },
+
+  reducers: {
+    changeLoading(state, action) {
+      return { ...state, ...action.payload };
+    },
+    querySuccess(state, action) {
+      return {
+        ...state,
+        currentItem: action.payload,
+      };
+    },
+    saveFilters(state, action) {
+      return {
+        ...state,
+        filters: action.payload.filters,
+      };
+    },
+    showResourceModal(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+        resourceModalVisible: true,
+      };
+    },
+    hideResourceModal(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+        resourceModalVisible: false,
+      };
+    },
+    saveCoverImg(state, action) {
+      return {
+        ...state,
+        resourceModalVisible: false,
+        currentItem: {
+          ...state.currentItem,
+          coverUrl: action.payload.coverUrl,
+        },
+      };
+    },
+    saveImgList(state, action) {
+      return {
+        ...state,
+        resourceModalVisible: false,
+        currentItem: {
+          ...state.currentItem,
+          imgList: action.payload.imgList,
+        }
+      };
+    },
+    saveOperType(state, action) {
+      return { ...state, ...action.payload };
+    },
+    initState(state) {
+      return { ...state, currentItem: {}, itemLoading: false };
+    }
+  }
+}

+ 59 - 0
src/models/training/training.js

@@ -0,0 +1,59 @@
+import { message } from 'antd';
+import { query, update, remove } from '../../services/product';
+import modelExtend from 'dva-model-extend';
+import queryString from 'query-string';
+import { pageModel } from '../common';
+import { pageSize } from '../../utils/config';
+import { checkSearchParams } from '../../utils/utils';
+import { Codes } from '../../utils/config';
+
+export default modelExtend(pageModel, {
+  namespace: 'training',
+
+  state: { listLoading: false },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen((location) => {
+        if (location.pathname == '/product/training') {
+          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, type: Codes.CODE_TRAINING });
+      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 }});
+    },
+    * delete ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(remove, payload);
+      if (success) {
+        message.success('删除成功!');
+        if (callback) callback();
+      }
+    },
+  },
+
+  reducers: {
+    changeLoading(state, action) {
+      return { ...state, ...action.payload };
+    },
+  }
+})

+ 49 - 2
src/routes/MProduct/Add/index.js

@@ -7,6 +7,7 @@ import { List, Switch, Table, Radio, Card, Form, Input, Icon, Button, Select } f
 import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
 import CourseSelectModal from './course';
 import SupportSelectModal from './support';
+import TrainingSelectModal from './training';
 import PackageSelectModal from './package';
 import { Codes, productType, pageSize } from '../../../utils/config';
 import styles from './index.less';
@@ -21,6 +22,7 @@ const { TextArea } = Input;
 @connect(state => ({
   course: state.course,
   support: state.support,
+  training: state.training,
   combo: state.combo,
   merchant: state.merchant,
   mproductDetail: state.mproductDetail,
@@ -31,6 +33,7 @@ export default class MerchantProductCreate extends PureComponent {
     curClickedBtn: null,
     courseModalVisible: false,
     supportModalVisible: false,
+    trainingModalVisible: false,
     packageModalVisible: false,
     selectItem: null,
   };
@@ -43,7 +46,6 @@ export default class MerchantProductCreate extends PureComponent {
         pageNo: 1,
         pageSize: 1000,
         status: Codes.CODE_NORMAL,
-        domain: Codes.CODE_PJ,
       }
     })
   }
@@ -74,6 +76,11 @@ export default class MerchantProductCreate extends PureComponent {
         type: 'support/query',
         payload: { ...newData, pageNo: 1, pageSize },
       });
+    } else if (curClickedBtn == Codes.CODE_TRAINING) {
+      dispatch({
+        type: 'training/query',
+        payload: { ...newData, pageNo: 1, pageSize },
+      })
     } else if (curClickedBtn == Codes.CODE_PACKAGE) {
       dispatch({
         type: 'combo/query',
@@ -95,6 +102,11 @@ export default class MerchantProductCreate extends PureComponent {
         selectItem: data,
         supportModalVisible: false,
       });
+    } else if(curClickedBtn == Codes.CODE_TRAINING) {
+      this.setState({
+        selectItem: data,
+        trainingModalVisible: false,
+      });
     } else if (curClickedBtn == Codes.CODE_PACKAGE) {
       this.setState({
         selectItem: data,
@@ -110,6 +122,8 @@ export default class MerchantProductCreate extends PureComponent {
       this.setState({ courseModalVisible: false });
     } else if (curClickedBtn == Codes.CODE_SUPPORT) {
       this.setState({ supportModalVisible: false });
+    } else if (curClickedBtn == Codes.CODE_TRAINING) {
+      this.setState({ trainingModalVisible: false });
     } else if (curClickedBtn == Codes.CODE_PACKAGE) {
       this.setState({ packageModalVisible: false });
     }
@@ -143,6 +157,19 @@ export default class MerchantProductCreate extends PureComponent {
           }
         });
       })
+    } else if (name == Codes.CODE_TRAINING) {
+      this.setState({
+        curClickedBtn: name,
+        trainingModalVisible: true,
+      }, () => {
+        dispatch({
+          type: 'training/query',
+          payload: {
+            pageNo: 1,
+            pageSize,
+          }
+        });
+      })
     } else if (name == Codes.CODE_PACKAGE) {
       this.setState({
         curClickedBtn: name,
@@ -184,6 +211,8 @@ export default class MerchantProductCreate extends PureComponent {
       dispatch({ type: 'course/query', payload: { ...data } });
     } else if (curClickedBtn == Codes.CODE_SUPPORT) {
       dispatch({ type: 'support/query', payload: { ...data } });
+    } else if (curClickedBtn == Codes.CODE_TRAINING) {
+      dispatch({ type: 'training/query', payload: { ...data } });
     } else if (curClickedBtn == Codes.CODE_PACKAGE) {
       dispatch({ type: 'combo/query', payload: { ...data } });
     }
@@ -248,6 +277,9 @@ export default class MerchantProductCreate extends PureComponent {
       case Codes.CODE_SUPPORT:
         return '选择配套';
         break;
+      case Codes.CODE_TRAINING:
+        return '选择师训';
+        break;
       case Codes.CODE_PACKAGE:
         return '选择课程包';
         break;
@@ -279,13 +311,15 @@ export default class MerchantProductCreate extends PureComponent {
       course,
       merchant,
       support,
-      combo
+      training,
+      combo,
     } = this.props;
     const {
       radioType,
       selectItem,
       courseModalVisible,
       supportModalVisible,
+      trainingModalVisible,
       packageModalVisible,
     } = this.state;
 
@@ -372,6 +406,19 @@ export default class MerchantProductCreate extends PureComponent {
             fsTablePagination={support.pagination}
             fsTableOnChange={this.handleModalTableChange}
           />
+          {/*查询师训的模态选择框*/}
+          <TrainingSelectModal
+            rowKeyName="id"
+            modalVisible={trainingModalVisible}
+            style={{ top: 30 }}
+            onOk={this.handleModalOk}
+            onCancel={this.handleModalCancel}
+            onSearch={this.handleModalSearch}
+            fsTableDataSource={training.list || []}
+            fsTableLoading={training.listLoading}
+            fsTablePagination={training.pagination}
+            fsTableOnChange={this.handleModalTableChange}
+          />
           {/*查询课程包的模态选择框*/}
           <PackageSelectModal
             rowKeyName="id"

+ 81 - 0
src/routes/MProduct/Add/training.js

@@ -0,0 +1,81 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge, Popover, Icon } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import { Codes } from '../../../utils/config';
+
+export default class TrainingSelectModal 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: 'code',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    //待选资源Table属性
+    const fsTableProps = {
+      fsTableColumns: [{
+        /*
+        title: '封面图',
+        dataIndex: 'url',
+        key: 'url',
+        render: (text, record) => (
+          <Popover
+            content={<img alt="" src={record.coverUrl} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={record.coverUrl} width={70} />
+          </Popover>
+        ),
+      },{
+      */
+        title: '师训编号',
+        dataIndex: 'code',
+        key: 'code',
+        width: '40%',
+      },{
+        title: '师训名称',
+        dataIndex: 'name',
+        key: 'name',
+        width: '40%',
+      }],
+      ...fsTableOpts,
+    }
+
+    return (
+      <SelectModal
+        mode="single"
+        { ...searchProps }
+        { ...fsTableProps }
+        { ...modalProps }
+      />
+    );
+  }
+}

+ 42 - 0
src/routes/MProduct/Edit/index.js

@@ -43,6 +43,24 @@ export default class MerchantProductEdit extends PureComponent {
       key: 'name',
       width: '60%',
     }];
+    const trainingTableColumns = [{
+      title: '位置',
+      dataIndex: 'sort',
+      key: 'sort',
+      render: (text, record, index) => index + 1,
+    },{
+      title: '图片',
+      dataIndex: 'path',
+      key: 'path',
+      render: (text, record) => (
+        <Popover
+            content={<img alt="" src={`${ossHost}/${text}`} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={`${ossHost}/${text}`} width={70} />
+          </Popover>
+      ),
+    }];
     const imgTableColumns = [{
       title: '位置',
       dataIndex: 'sort',
@@ -187,6 +205,30 @@ export default class MerchantProductEdit extends PureComponent {
                 </DescriptionList>
             </div>
         );
+    } else if (data.type === Codes.CODE_TRAINING) {
+        return (
+          <div>
+            <DescriptionList size="large" col={1}>
+              <Description term="师训编号">{data.code}</Description>
+              <Description term="师训主题">{data.title}</Description>
+              <Description term="活动时间">{data.dateDesc}</Description>
+              <Description term="详情图片">
+                <Table
+                  simple
+                  bordered
+                  columns={trainingTableColumns}
+                  dataSource={(data.imgList || []).map((item,index) => ({ id: index, path: item }))}
+                  pagination={false}
+                  rowKey={record => record.id}
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      该师训内暂无图片!</span>
+                  }}
+                />
+              </Description>
+            </DescriptionList>
+          </div>
+        );
     } else {
         return (
             <div>

+ 1 - 1
src/routes/Resource/gallery/index.js

@@ -118,7 +118,7 @@ export default class Gallery extends Component {
       maskClosable: false,
       signature,
       modalType,
-      title: `${modalType == 'create' ? '添加图片' : '编辑图片'}`,
+      title: `${modalType == 'create' ? '添加图片' : '查看图片'}`,
       wrapClassName: 'vertical-center-modal',
       onOk (data) {
         dispatch({

+ 4 - 6
src/routes/Resource/gallery/table.js

@@ -36,12 +36,12 @@ export default class TableList extends PureComponent {
       title: '图片编号',
       dataIndex: 'code',
       key: 'code',
-      width: '20%',
+      width: '22%',
     },{
       title: '图片名称',
       dataIndex: 'name',
       key: 'name',
-      width: '20%',
+      width: '22%',
     },{
       title: '图片大小(B)',
       dataIndex: 'size',
@@ -70,12 +70,10 @@ export default class TableList extends PureComponent {
       key: 'operation',
       render: (text, record) => (
         <div>
-          <a onClick={() => onEditItem(record)}>编辑</a>
-          <Divider type="vertical" />
-          <a onClick={() => this.handleDeleteItem(record)}>删除</a>
+          <a onClick={() => onEditItem(record)}>查看</a>
         </div>
       ),
-      width: '12%',
+      width: '8%',
     }];
 
     // 配置分页

+ 374 - 0
src/routes/Training/Edit/index.js

@@ -0,0 +1,374 @@
+import React, { PureComponent } from 'react';
+import moment from 'moment';
+import { routerRedux } from 'dva/router';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { Spin, Popover, Badge, Table, Radio, Card, Form, Input, Icon, Button, Select, DatePicker } from 'antd';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import ResourceSelectModal from './resource';
+import { Codes, ossHost } from '../../../utils/config';
+
+@Form.create()
+@connect(state => ({
+  merchant: state.merchant,
+  resource: state.resource,
+  trainingDetail: state.trainingDetail,
+}))
+export default class TrainingDetail extends PureComponent {
+  state = { curClickedBtn: null };
+
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'merchant/query',
+      payload: {
+        pageSize: 1000,
+        pageNo: 1,
+      }
+    })
+  }
+
+  // 展示选择模态框 - 加载第一页数据
+  handleModalShow = (btnName) => {
+    this.setState({
+      curClickedBtn: btnName,
+    }, () => {
+      const { dispatch } = this.props;
+      if (btnName == 'imgBtn' || btnName == 'cvImgBtn') {
+        dispatch({ type: 'trainingDetail/showResourceModal' });
+        dispatch({
+          type: 'resource/query',
+          payload: {
+            pageNo: 1,
+            pageSize: 10,
+            type: Codes.CODE_IMAGE,
+          }
+        });
+      }
+    })
+  }
+
+  // 取消/关闭 - 隐藏选择模态框
+  handleModalCancel = () => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    if (curClickedBtn == 'imgBtn' || curClickedBtn == 'cvImgBtn') {
+      dispatch({ type: 'trainingDetail/hideResourceModal' });
+    }
+  }
+
+  // 提交 - 保存选择和排序完的数据到model中
+  handleModalOk = (data) => {
+    const { curClickedBtn } = this.state;
+    const { dispatch } = this.props;
+    if (curClickedBtn == 'imgBtn') {
+      dispatch({
+        type: 'trainingDetail/saveImgList',
+        payload: { imgList: data.map(item => item.path) },
+      });
+    } else if (curClickedBtn == 'cvImgBtn') {
+      dispatch({
+        type: 'trainingDetail/saveCoverImg',
+        payload: { coverUrl: data.path },
+      });
+    }
+  }
+
+  // 搜索
+  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;
+
+    if (curClickedBtn == 'imgBtn' || curClickedBtn == 'cvImgBtn') {
+      dispatch({
+        type: 'resource/query',
+        payload: { ...newData, pageNo: 1, pageSize: 10, 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 == 'imgBtn' || curClickedBtn == 'cvImgBtn') {
+      dispatch({ type: `resource/query`, payload: { ...data, status: Codes.CODE_NORMAL, type: Codes.CODE_IMAGE } });
+    }
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault()
+    const {
+      dispatch,
+      form: {
+        validateFields,
+        getFieldsValue,
+        resetFields
+      },
+      trainingDetail: {
+        operType,
+        currentItem,
+        filters,
+      }
+    } = this.props;
+    validateFields((errors) => {
+      if (errors) { return; }
+
+      // 过滤掉gmtCreated, gmtModified, status三个字段
+      const { imgList, gmtCreated, gmtModified, ...restProps } = currentItem;
+      let newImgList;
+      if (Array.isArray(imgList) && imgList.length) {
+        newImgList = imgList;
+      } else {
+        newImgList = [];
+      }
+      const { title, openTime, closeTime } = getFieldsValue();
+      const openTimestamp = parseInt(moment(openTime).format('x'));
+      const closeTimestamp = parseInt(moment(closeTime).format('x'));
+
+      const data = {
+        ...getFieldsValue(),
+        name: title,
+        imgList: newImgList,
+        openTime: openTimestamp,
+        closeTime: closeTimestamp,
+        ...restProps,
+      };
+
+      // 其他参数
+      if (operType == 'create') {
+        data.type = Codes.CODE_TRAINING;
+        data.status = Codes.CODE_NORMAL;
+      }
+
+      dispatch({
+        type: `trainingDetail/${operType}`,
+        payload: data,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname: '/product/training',
+              search: queryString.stringify(filters),
+            })
+          );
+          resetFields();
+        }
+      })
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch, trainingDetail: { filters } } = this.props;
+    dispatch({ type: 'trainingDetail/initState' });
+    dispatch(
+      routerRedux.push({
+        pathname: '/product/training',
+        search: queryString.stringify(filters),
+      })
+    );
+  }
+
+  render() {
+    const {
+      merchant,
+      resource,
+      training,
+      trainingDetail,
+      form: {
+        getFieldDecorator
+      },
+    } = this.props;
+    const {
+      itemLoading,
+      currentItem,
+      filters,
+      trainingModalVisible,
+      resourceModalVisible
+    } = trainingDetail;
+    const {
+      cpId,
+      title,
+      code,
+      name,
+      coverUrl,
+      imgList,
+      dateDesc,
+      openTime,
+      closeTime,
+    } = currentItem;
+    const { curClickedBtn } = this.state;
+
+    // 待选表格去掉分页的跳转及变换页码
+    if (resource && resource.pagination) {
+      delete resource.pagination.showQuickJumper;
+      delete resource.pagination.showSizeChanger;
+    }
+
+    const imgTableColumns = [{
+      title: '位置',
+      dataIndex: 'sort',
+      key: 'sort',
+      render: (text, record, index) => index + 1,
+    },{
+      title: '缩略图',
+      dataIndex: 'path',
+      key: 'path',
+      render: (text, record) => (
+          <Popover
+            content={<img alt="" src={`${ossHost}/${text}`} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={`${ossHost}/${text}`} width={70} />
+          </Popover>
+      ),
+    }];
+
+    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}>
+              <Form.Item label="师训编号" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('code', {
+                  rules: [{ required: true, type: 'string', message: "编号为必填项!" }],
+                  initialValue: code,
+                })(<Input />)}
+              </Form.Item>
+              <Form.Item label="师训标题" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('title', {
+                  rules: [{ required: true, type: 'string', message: "标题为必填项!" }],
+                  initialValue: title,
+                })(<Input />)}
+              </Form.Item>
+              <Form.Item label="活动时间" {...formItemLayout}>
+                {getFieldDecorator('dateDesc', {
+                  rules: [{ required: true, type: 'string', message: "活动为必填项!" }],
+                  initialValue: dateDesc,
+                })(<Input placeholder="请填写师训活动时间范围" />)}
+              </Form.Item>
+              <Form.Item label="起始日期" {...formItemLayout}>
+                {getFieldDecorator('openTime', {
+                  rules: [{ required: true, message: '请选择起始日期' }],
+                  initialValue: openTime && moment(openTime),
+                })(
+                  <DatePicker
+                    showTime
+                    placeholder="选择日期时间"
+                    format="YYYY-MM-DD HH:mm:ss"
+                    style={{ width: '100%' }}
+                  />
+                )}
+              </Form.Item>
+              <Form.Item label="截止日期" {...formItemLayout}>
+                {getFieldDecorator('closeTime', {
+                  rules: [{ required: true, message: '请选择截止日期' }],
+                  initialValue: closeTime && moment(closeTime),
+                })(
+                  <DatePicker
+                    showTime
+                    placeholder="选择日期时间"
+                    format="YYYY-MM-DD HH:mm:ss"
+                    style={{ width: '100%' }}
+                  />
+                )}
+              </Form.Item>
+              <Form.Item label="所属供应商:" {...formItemLayout}>
+                {getFieldDecorator('cpId', {
+                  initialValue: cpId,
+                })(
+                  <Select placeholder="请选择">{merchant.list.map(item => <Select.Option value={item.id} key={item.id}>{item.name}</Select.Option>)}</Select>
+                )}
+              </Form.Item>
+              <Form.Item label="封面图片" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('cvImgBtn')} type="primary" icon="select" size="small">选择</Button>
+                {!coverUrl? null :
+                  <Card
+                    hoverable
+                    bordered
+                    cover={<img alt="" src={coverUrl.startsWith('http') ? coverUrl : `${ossHost}/${coverUrl}`} />}
+                    style={{ width: 240, marginTop: 20 }}
+                  >
+                  </Card>}
+              </Form.Item>
+              <Form.Item label="图片列表" {...formItemLayout}>
+                <Button onClick={() => this.handleModalShow('imgBtn')} type="primary" size="small" icon="edit">编辑</Button>
+              </Form.Item>
+              <Form.Item wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      该师训下不包含任何图片,请选择!</span>
+                  }}
+                  dataSource={(imgList || []).map((item,index) => ({ id: index, path: item }))}
+                  columns={imgTableColumns}
+                  rowKey={record => record.id}
+                  bordered
+                  pagination={false}
+                />
+              </Form.Item>
+              <Form.Item {...submitFormLayout} style={{ marginTop: 32 }}>
+                <Button onClick={this.handlePageCancel}>取消</Button>
+                <Button type="primary" style={{ marginLeft: 35 }} htmlType="submit">提交</Button>
+              </Form.Item>
+            </Form>
+            {/*图片资源的模态选择框*/}
+            <ResourceSelectModal
+              mode={curClickedBtn == 'imgBtn' ? 'multiple' : 'single'}
+              rowKeyName="id"
+              modalVisible={resourceModalVisible}
+              width={600}
+              onOk={this.handleModalOk}
+              onCancel={this.handleModalCancel}
+              onSearch={this.handleModalSearch}
+              selTableData={[]}
+              fsTableDataSource={resource.list || []}
+              fsTableLoading={resource.listLoading}
+              fsTablePagination={resource.pagination}
+              fsTableOnChange={this.handleModalTableChange}
+            />
+          </Card>
+        </Spin>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 103 - 0
src/routes/Training/Edit/resource.js

@@ -0,0 +1,103 @@
+import React, { PureComponent } from 'react';
+import { Popover } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import styles from './resource.less';
+import { Codes, ossHost } from '../../../utils/config';
+
+export default class ResourceSelectModal extends PureComponent {
+  render() {
+    const { mode, 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: 'code',
+      },
+      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={`${ossHost}/${record.path}`} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={`${ossHost}/${record.path}`} 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={`${ossHost}/${record.path}`} width={350} />}
+            title={record.name}
+          >
+            <img alt="" src={`${ossHost}/${record.path}`} width={70} />
+          </Popover>
+        ),
+      },{
+        title: '图片编号',
+        dataIndex: 'code',
+        key: 'code',
+      },{
+        title: '图片名称',
+        dataIndex: 'name',
+        key: 'name',
+      }],
+      ...fsTableOpts,
+    }
+
+    return (
+      <SelectModal
+        mode={mode}
+        { ...searchProps }
+        { ...selTableProps}
+        { ...fsTableProps }
+        { ...modalProps }
+      />
+    );
+  }
+}

+ 127 - 0
src/routes/Training/Edit/resource.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;
+        }
+      }
+    }
+  }
+}

+ 110 - 0
src/routes/Training/List/index.js

@@ -0,0 +1,110 @@
+import React, { PureComponent } from 'react';
+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 => ({ training: state.training }))
+export default class Training extends PureComponent {
+  render() {
+    const { location, dispatch, training } = 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 } = training;
+
+    // 把携带的参数中空值项删除
+    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/training/add',
+            state: filters,
+          })
+        );
+      }
+    };
+
+    const listProps = {
+      pagination,
+      location,
+      dataSource: list,
+      loading: listLoading,
+      onChange: (pagination, filterArgs) => {
+        const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+        const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+          const newObj = { ...obj };
+          newObj[key] = getValue(filterArgs[key]);
+          return newObj;
+        }, {});
+
+        const data = { ...filters, ...tableFilters };
+        Object.keys(data).map(key => data[key] ? null : delete data[key]);
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...data,
+            pageNo: pagination.current,
+            pageSize: pagination.pageSize,
+          }),
+        }));
+      },
+      onEditItem: ({ pid }) => {
+        dispatch(
+          routerRedux.push({
+            pathname: `/product/training/edit/${pid}`,
+            state: filters,
+          })
+        );
+      },
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'training/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 36 - 0
src/routes/Training/List/search.js

@@ -0,0 +1,36 @@
+import react, { PureComponent } from 'react';
+import { Button, Row, Col, Icon } from 'antd';
+import DataSearch from '../../../components/DataSearch';
+
+export default class Search extends PureComponent {
+  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 || 'code',
+      },
+      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="primary" onClick={onAdd}><Icon type="plus-circle" />创建师训</Button>
+        </Col>
+      </Row>
+    );
+  }
+}

+ 75 - 0
src/routes/Training/List/table.js

@@ -0,0 +1,75 @@
+import React, { PureComponent } from 'react';
+import moment from 'moment';
+import queryString from 'query-string';
+import { Divider, Modal, Table, Icon, Badge } from 'antd';
+import { statuses, Codes } from '../../../utils/config';
+
+export default class TableList extends PureComponent {
+  handleDeleteItem = (record) => {
+    const { onDeleteItem } = this.props;
+    Modal.confirm({
+      title: `您确定要删除该师训?`,
+      okText: '确定',
+      cancelText: '取消',
+      onOk: () => onDeleteItem({ id: record.id }),
+    });
+  }
+
+  render() {
+    const { onDeleteItem, onEditItem, pagination, ...tableProps } = this.props;
+
+    const columns = [{
+      title: '师训编号',
+      dataIndex: 'code',
+      key: 'code',
+      width: '25%',
+    },{
+      title: '师训主题',
+      dataIndex: 'name',
+      key: 'name',
+      width: '25%',
+    }, {
+      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]} />);
+      },
+      width: '15%',
+    },{
+      title: '修改时间',
+      dataIndex: 'gmtModified',
+      key: 'gmtModified',
+      render: (text, record) => (
+        <div>{moment(text).format('YYYY-MM-DD HH:mm:ss')}</div>
+      ),
+      width: '20%',
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <div>
+          <a onClick={() => onEditItem(record)}>编辑</a>
+          <Divider type="vertical" />
+          <a onClick={() => this.handleDeleteItem(record)}>删除</a>
+        </div>
+      ),
+      width: '15%',
+    }];
+
+    // 配置分页
+    tableProps.pagination = !!pagination && { ...pagination, showSizeChanger: true, showQuickJumper: true, showTotal: total => `共 ${total} 条`};
+
+    return (
+      <Table
+        simple
+        bordered
+        { ...tableProps }
+        columns={columns}
+        rowKey={record => record.id}
+      />
+    );
+  }
+}

+ 2 - 0
src/utils/config.js

@@ -15,6 +15,7 @@ Codes.CODE_LESSON = 'LESSON';
 Codes.CODE_COURSE = 'COURSE';
 Codes.CODE_SUPPORT = 'SUPPORT';
 Codes.CODE_PACKAGE = 'PACKAGE';
+Codes.CODE_TRAINING = 'TRAINING';
 
 Codes.CODE_SALE = 'SALE';
 Codes.CODE_UNPAID = 'UNPAID';
@@ -76,6 +77,7 @@ module.exports = {
   productType: {
     [Codes.CODE_COURSE]: '课程',
     [Codes.CODE_SUPPORT]: '配套',
+    [Codes.CODE_TRAINING]: '师训',
     [Codes.CODE_PACKAGE]: '课程包',
   },
   // 平台代号