فهرست منبع

add lesson manage module;

zhanghe 7 سال پیش
والد
کامیت
ea17d093d2

+ 26 - 0
.roadhogrc.mock.js

@@ -7,6 +7,7 @@ import { resourceList } from './mock/resource';
 import { groupList } from './mock/group';
 import { tagList } from './mock/tag';
 import { wareList } from './mock/ware';
+import { lessonList } from './mock/lesson';
 import { signature } from './mock/signature';
 import * as api from './src/utils/api';
 
@@ -18,6 +19,7 @@ global.resourceList = resourceList;
 global.groupList = groupList;
 global.tagList = tagList;
 global.wareList = wareList;
+global.lessonList = lessonList;
 global.signature = signature;
 
 // 操作成功响应内容
@@ -219,6 +221,7 @@ const proxy = {
     res.send(create(global.wareList, req.body));
   },
   [`DELETE ${api.ware}`]: (req, res) => {
+    console.log(`[DELETE][${api.ware}]`, req.params);
     const { id } = req.params;
     remove(global.wareList, id, res);
   },
@@ -235,6 +238,29 @@ const proxy = {
     const { id } = req.params;
     queryOne(global.wareList, id, res);
   },
+  // 课
+  [`POST ${api.lesson.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[POST][${api.lesson}]`, req.body);
+    res.send(create(global.lessonList, req.body));
+  },
+  [`DELETE ${api.ware}`]: (req, res) => {
+    console.log(`[DELETE][${api.lesson}]`, req.params);
+    const { id } = req.params;
+    remove(global.lessonList, id, res);
+  },
+  [`PUT ${api.lesson.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[PUT][${api.lesson}]`, req.body);
+    res.send(update(global.lessonList, req.body));
+  },
+  [`GET ${api.lessons}`]: (req, res) => {
+    console.log(`[GET][${api.lessons}]`, req.query);
+    res.send(query(global.lessonList, req.query));
+  },
+  [`GET ${api.lesson}`]: (req, res) => {
+    console.log(`[GET][${api.lesson}]`, req.params);
+    const { id } = req.params;
+    queryOne(global.lessonList, id, res);
+  },
 };
 
 // 是否禁用代理

+ 1 - 1
mock/lesson.js

@@ -14,4 +14,4 @@ for (let i = 1; i < 100; i++) {
   });
 }
 
-module.exports = { lessongList };
+module.exports = { lessonList };

+ 11 - 0
src/common/router.js

@@ -92,6 +92,17 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['ware/detail', 'resource'], () => import('../routes/Ware/detail')),
       name: '修改课件',
     },
+    '/product/lesson': {
+      component: dynamicWrapper(app, ['lesson/lesson'], () => import('../routes/Lesson')),
+    },
+    '/product/lesson/add': {
+      component: dynamicWrapper(app, ['lesson/detail', 'ware/ware'], () => import('../routes/Lesson/detail')),
+      name: '添加课',
+    },
+    '/product/lesson/edit/:id': {
+      component: dynamicWrapper(app, ['lesson/detail', 'ware/ware'], () => import('../routes/Lesson/detail')),
+      name: '修改课',
+    },
     '/user': {
       component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')),
     },

+ 106 - 0
src/models/lesson/detail.js

@@ -0,0 +1,106 @@
+import { queryOne, create, update } from '../../services/lesson';
+import { message } from 'antd';
+import pathToRegexp from 'path-to-regexp';
+import { Codes } from '../../utils/config';
+
+export default {
+  namespace: 'lessonDetail',
+
+  state: {
+    filters: {},
+    operType: 'create',
+    currentItem: {},
+    modalVisible: false,
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state, ...rest }) => {
+        const match = pathToRegexp('/product/lesson/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/lesson/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 };
+    },
+
+    showModal(state) {
+      return { ...state, modalVisible: true };
+    },
+
+    hideModal(state) {
+      return { ...state, modalVisible: false };
+    },
+
+    saveOperType(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    saveSortResult(state, { payload: { wareList } }) {
+      console.log(wareList);
+      const currentItem = { ...state.currentItem, wareList };
+      return { ...state, modalVisible: false, currentItem };
+    },
+
+    clearPage(state) {
+      return { ...state, currentItem: {}, itemLoading: false };
+    }
+  }
+}

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

@@ -0,0 +1,113 @@
+import { query, create, update, remove } from '../../services/lesson';
+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: 'lesson',
+
+  state: {
+    currentItem: {},
+    itemLoading: false,
+    listLoading: false,
+    modalVisible: false,
+    modalType: 'create',
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen((location) => {
+        if (location.pathname === '/product/lesson') {
+          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 };
+    },
+  }
+})

+ 255 - 0
src/routes/Lesson/detail/index.js

@@ -0,0 +1,255 @@
+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 WareSelectSortModal from './modal';
+import { Codes } from '../../../utils/config';
+
+const FormItem = Form.Item;
+const Option = Select.Option;
+const { TextArea } = Input;
+
+@Form.create()
+@connect(state => ({
+  lessonDetail: state.lessonDetail,
+  ware: state.ware,
+}))
+export default class LessonDetail extends PureComponent {
+  static propTypes = {
+    lessonDetail: PropTypes.object,
+  };
+
+  // 展示模态框 - 加载第一页数据
+  handleModalShow = () => {
+    const { dispatch } = this.props;
+    dispatch({ type: 'lessonDetail/showModal' });
+    dispatch({ type: 'ware/query', payload: { pageNo: 1, pageSize: 10 } });
+  }
+
+  // 取消/关闭 - 隐藏模态框
+  handleModalCancel = () => {
+    const { dispatch } = this.props;
+    dispatch({ type: 'lessonDetail/hideModal' });
+  }
+
+  // 提交 - 保存选择和排序完的数据到model中
+  handleModalOk = (data) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'lessonDetail/saveSortResult',
+      payload: { wareList: data }
+    });
+  }
+
+  // 搜索
+  handleModalSearch = (data) => {
+    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;
+    }
+    dispatch({
+      type: 'ware/query',
+      payload: { ...newData, pageNo: 1, pageSize: 10 },
+    });
+  }
+
+  // 翻页 - 资源列表
+  handleModalTableOnChange = (pagination, filterArgs, filters) => {
+    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]);
+    dispatch({ type: 'ware/query', payload: data });
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault()
+    const {
+      dispatch,
+      form: {
+        validateFields,
+        getFieldsValue,
+        resetFields
+      },
+      lessonDetail: {
+        operType,
+        currentItem,
+        filters,
+      }
+    } = this.props;
+    validateFields((errors) => {
+      if (errors) { return; }
+      const data = {
+        ...currentItem,
+        ...getFieldsValue(),
+      };
+      dispatch({
+        type: `lessonDetail/${operType}`,
+        payload: data,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname: '/product/lesson',
+              search: queryString.stringify(filters),
+            })
+          );
+        }
+      })
+      resetFields();
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch, lessonDetail: { filters } } = this.props;
+    dispatch({ type: 'lessonDetail/clearPage' });
+    dispatch(
+      routerRedux.push({
+        pathname: '/product/lesson',
+        search: queryString.stringify(filters),
+      })
+    );
+  }
+
+  render() {
+    const { dispatch, form: { getFieldDecorator }, lessonDetail, ware } = this.props;
+    const { itemLoading, currentItem, filters, modalVisible } = lessonDetail;
+    const { wareList, name, code, digest } = currentItem;
+    const { list, listLoading, pagination } = ware;
+    console.log(wareList);
+
+    // 待选表格去掉分页的跳转及变换页码
+    if (pagination) {
+      delete pagination.showQuickJumper;
+      delete pagination.showSizeChanger;
+    }
+
+    const subTableColumns = [{
+      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>
+            <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="提供商" 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>
+              <FormItem wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      该课件下不包含任何内容,请选择图片或视频!</span>
+                  }}
+                  dataSource={wareList}
+                  columns={subTableColumns}
+                  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>
+            <WareSelectSortModal
+              rowKeyName="id"
+              modalVisible={modalVisible}
+              style={{ top: 20 }}
+              width={600}
+              onCancel={this.handleModalCancel}
+              onOk={this.handleModalOk}
+              onSearch={this.handleModalSearch}
+              selTableData={wareList || []}
+              fsTableDataSource={list}
+              fsTableLoading={listLoading}
+              fsTablePagination={pagination}
+              fsTableOnChange={this.handleModalTableOnChange}
+            />
+          </Card>
+        </Spin>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 87 - 0
src/routes/Lesson/detail/modal.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 './modal.less';
+import { Codes, resourceType } from '../../../utils/config';
+
+export default class WareSelectSortModal 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/Lesson/detail/modal.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/Lesson/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 => ({
+  lesson: state.lesson,
+}))
+export default class Lesson extends PureComponent {
+  static propTypes = {
+    lesson: PropTypes.object,
+    location: PropTypes.object,
+    dispatch: PropTypes.func,
+  };
+
+  render() {
+    const { location, dispatch, lesson } = 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 } = lesson;
+
+    // 把携带的参数中空值项删除
+    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/lesson/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/lesson/edit/${item.id}`,
+            state: filters,
+          })
+        );
+      },
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'lesson/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+      onRecoverItem: (payload) => {
+        dispatch({
+          type: 'lesson/recover',
+          payload,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      }
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 48 - 0
src/routes/Lesson/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/Lesson/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与domain值为空,此时删除该参数
+    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/Lesson/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;
+}

+ 2 - 2
src/routes/Ware/index.js

@@ -12,9 +12,9 @@ import { Codes } from '../../utils/config';
 @connect(state => ({
   ware: state.ware,
 }))
-export default class Tag extends PureComponent {
+export default class Ware extends PureComponent {
   static propTypes = {
-    group: PropTypes.object,
+    ware: PropTypes.object,
     location: PropTypes.object,
     dispatch: PropTypes.func,
   };

+ 32 - 0
src/services/lesson.js

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

+ 3 - 1
src/utils/api.js

@@ -19,5 +19,7 @@ module.exports = {
   tags: `${config.apiHost}/tag/list`,
   tag: `${config.apiHost}/tag/:id`,
   wares: `${config.apiHost}/ware/list`,
-  ware: `${config.apiHost}/ware/:id`
+  ware: `${config.apiHost}/ware/:id`,
+  lessons: `${config.apiHost}/lesson/list`,
+  lesson: `${config.apiHost}/lesson/:id`,
 };