zhanghe 7 лет назад
Родитель
Сommit
4d62bee604

+ 31 - 1
.roadhogrc.mock.js

@@ -8,6 +8,7 @@ import { groupList } from './mock/group';
 import { tagList } from './mock/tag';
 import { wareList } from './mock/ware';
 import { lessonList } from './mock/lesson';
+import { orderList } from './mock/order';
 import { signature } from './mock/signature';
 import * as api from './src/utils/api';
 
@@ -20,6 +21,7 @@ global.groupList = groupList;
 global.tagList = tagList;
 global.wareList = wareList;
 global.lessonList = lessonList;
+global.orderList = orderList;
 global.signature = signature;
 
 // 操作成功响应内容
@@ -119,19 +121,24 @@ const remove = (dataset, id, res) => {
 const proxy = {
   // 资源
   [`POST ${api.resource.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[POST][${api.resource}]`, req.body);
     res.send(create(global.resourceList, req.body));
   },
   [`DELETE ${api.resource}`]: (req, res) => {
+    console.log(`[DELETE][${api.resource}]`, req.params);
     const { id } = req.params;
     remove(global.resourceList, id, res);
   },
   [`PUT ${api.resource.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[PUT][${api.resource}]`, req.body);
     res.send(update(global.resourceList, req.body));
   },
   [`GET ${api.resources}`]: (req, res) => {
+    console.log(`[GET][${api.resources}]`, req.query);
     res.send(query(global.resourceList, req.query));
   },
   [`GET ${api.signature}`]: (req, res) => {
+    console.log(`[GET][${api.signature}]`, req.query);
     res.send({ ...SUCCESS, data: global.signature });
   },
   // 校区
@@ -243,7 +250,7 @@ const proxy = {
     console.log(`[POST][${api.lesson}]`, req.body);
     res.send(create(global.lessonList, req.body));
   },
-  [`DELETE ${api.ware}`]: (req, res) => {
+  [`DELETE ${api.lesson}`]: (req, res) => {
     console.log(`[DELETE][${api.lesson}]`, req.params);
     const { id } = req.params;
     remove(global.lessonList, id, res);
@@ -261,6 +268,29 @@ const proxy = {
     const { id } = req.params;
     queryOne(global.lessonList, id, res);
   },
+  // 订单
+  [`POST ${api.order.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[POST][${api.order}]`, req.body);
+    res.send(create(global.orderList, req.body));
+  },
+  [`DELETE ${api.order}`]: (req, res) => {
+    console.log(`[DELETE][${api.order}]`, req.params);
+    const { id } = req.params;
+    remove(global.orderList, id, res);
+  },
+  [`PUT ${api.order.replace('/:id', '')}`]: (req, res) => {
+    console.log(`[PUT][${api.order}]`, req.body);
+    res.send(update(global.orderList, req.body));
+  },
+  [`GET ${api.orders}`]: (req, res) => {
+    console.log(`[GET][${api.orders}]`, req.query);
+    res.send(query(global.orderList, req.query));
+  },
+  [`GET ${api.order}`]: (req, res) => {
+    console.log(`[GET][${api.order}]`, req.params);
+    const { id } = req.params;
+    queryOne(global.orderList, id, res);
+  },
 };
 
 // 是否禁用代理

+ 22 - 0
mock/order.js

@@ -0,0 +1,22 @@
+let orderList = [];
+const orderStatus = ['0', '1', '2'];
+for (let i = 1; i < 300; i++) {
+  orderList.push({
+    id: String(i),
+    code: 'Order-20180111-' + i,
+    uid: '1122',                   //用户id
+    userCode: '66664301000101' + i,         //用户编码
+    provinceCode: '10',            //省份行政代码
+    cityName: '长沙',
+    zoneName: '淄博校区',
+    classroomName: '3年级2班教室',
+    finalPrice: 3000.85,
+    merchantPrice: 3500.11,
+    lingjiaoPrice: 2000,
+    status: orderStatus[i % 3],
+    gmtCreated: 1512981450000,
+    gmtModified: 1512981450000,
+  });
+}
+
+module.exports = { orderList };

+ 3 - 3
mock/resource.js

@@ -7,7 +7,7 @@ for (let i = 1; i < 100; i++) {
       name: '破解表情密码01',
       size: 90,
       status: 'NORMAL',
-      type: 3,
+      type: '3',
       gmtCreated: (new Date()).getTime(),
       gmtModified: (new Date()).getTime(),
       url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/612102.jpg',
@@ -17,7 +17,7 @@ for (let i = 1; i < 100; 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',
@@ -27,7 +27,7 @@ for (let i = 1; i < 100; 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',

+ 1 - 1
src/common/menu.js

@@ -63,7 +63,7 @@ const menuData = [
   },{
     name: '订单管理',
     icon: 'trademark',
-    path: 'trade',
+    path: 'order',
   },{
     name: '销售统计',
     icon: 'area-chart',

+ 15 - 0
src/common/router.js

@@ -103,6 +103,21 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['lesson/detail', 'ware/ware'], () => import('../routes/Lesson/detail')),
       name: '修改课',
     },
+    '/order': {
+      component: dynamicWrapper(app, ['order/order'], () => import('../routes/Order')),
+    },
+    // '/order/add': {
+    //   component: dynamicWrapper(app, ['order/detail'], () => import('../routes/Order/detail')),
+    //   name: '新建订单',
+    // },
+    // '/order/edit/:id': {
+    //   component: dynamicWrapper(app, ['order/detail'], () => import('../routes/Order/detail')),
+    //   name: '修改订单',
+    // },
+    '/order/profile/:id': {
+      component: dynamicWrapper(app, ['order/detail'], () => import('../routes/Order/detail/orderProfile')),
+      name: '订单详情',
+    },
     '/user': {
       component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')),
     },

+ 17 - 0
src/components/DescriptionList/Description.js

@@ -0,0 +1,17 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Col } from 'antd';
+import styles from './index.less';
+import responsive from './responsive';
+
+const Description = ({ term, column, className, children, ...restProps }) => {
+  const clsString = classNames(styles.description, className);
+  return (
+    <Col className={clsString} {...responsive[column]} {...restProps}>
+      {term && <div className={styles.term}>{term}</div>}
+      {children && <div className={styles.detail}>{children}</div>}
+    </Col>
+  );
+};
+
+export default Description;

+ 21 - 0
src/components/DescriptionList/DescriptionList.js

@@ -0,0 +1,21 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Row } from 'antd';
+import styles from './index.less';
+
+export default ({ className, title, col = 3, layout = 'horizontal', gutter = 32,
+  children, size, ...restProps }) => {
+  const clsString = classNames(styles.descriptionList, styles[layout], className, {
+    [styles.descriptionListSmall]: size === 'small',
+    [styles.descriptionListLarge]: size === 'large',
+  });
+  const column = col > 4 ? 4 : col;
+  return (
+    <div className={clsString} {...restProps}>
+      {title ? <div className={styles.title}>{title}</div> : null}
+      <Row gutter={gutter}>
+        {React.Children.map(children, child => React.cloneElement(child, { column }))}
+      </Row>
+    </div>
+  );
+};

+ 35 - 0
src/components/DescriptionList/demo/basic.md

@@ -0,0 +1,35 @@
+---
+order: 0
+title: Basic
+---
+
+基本描述列表。
+
+````jsx
+import DescriptionList from 'ant-design-pro/lib/DescriptionList';
+
+const { Description } = DescriptionList;
+
+ReactDOM.render(
+  <DescriptionList size="large" title="title">
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+  </DescriptionList>
+, mountNode);
+````

+ 35 - 0
src/components/DescriptionList/demo/vertical.md

@@ -0,0 +1,35 @@
+---
+order: 1
+title: Vertical
+---
+
+垂直布局。
+
+````jsx
+import DescriptionList from 'ant-design-pro/lib/DescriptionList';
+
+const { Description } = DescriptionList;
+
+ReactDOM.render(
+  <DescriptionList size="large" title="title" layout="vertical">
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+    <Description term="Firefox">
+      A free, open source, cross-platform,
+      graphical web browser developed by the
+      Mozilla Corporation and hundreds of
+      volunteers.
+    </Description>
+  </DescriptionList>
+, mountNode);
+````

+ 22 - 0
src/components/DescriptionList/index.d.ts

@@ -0,0 +1,22 @@
+import * as React from "react";
+export interface DescriptionListProps {
+  layout?: "horizontal" | "vertical";
+  col?: number;
+  title: React.ReactNode;
+  gutter?: number;
+  size?: "large" | "small";
+}
+
+declare class Description extends React.Component<
+  {
+    term: React.ReactNode;
+  },
+  any
+> {}
+
+export default class DescriptionList extends React.Component<
+  DescriptionListProps,
+  any
+> {
+  static Description: typeof Description;
+}

+ 5 - 0
src/components/DescriptionList/index.js

@@ -0,0 +1,5 @@
+import DescriptionList from './DescriptionList';
+import Description from './Description';
+
+DescriptionList.Description = Description;
+export default DescriptionList;

+ 75 - 0
src/components/DescriptionList/index.less

@@ -0,0 +1,75 @@
+@import "~antd/lib/style/themes/default.less";
+
+.descriptionList {
+  // offset the padding-bottom of last row
+  :global {
+    .ant-row {
+      margin-bottom: -16px;
+      overflow: hidden;
+    }
+  }
+
+  .title {
+    font-size: 14px;
+    color: @heading-color;
+    font-weight: 500;
+    margin-bottom: 16px;
+  }
+
+  .term {
+    line-height: 22px;
+    padding-bottom: 16px;
+    margin-right: 8px;
+    color: @heading-color;
+    white-space: nowrap;
+    display: table-cell;
+
+    &:after {
+      content: ":";
+      margin: 0 8px 0 2px;
+      position: relative;
+      top: -.5px;
+    }
+  }
+
+  .detail {
+    line-height: 22px;
+    width: 100%;
+    padding-bottom: 16px;
+    color: @text-color;
+    display: table-cell;
+  }
+
+  &.vertical {
+
+    .term {
+      padding-bottom: 8px;
+      display: block;
+    }
+
+    .detail {
+      display: block;
+    }
+  }
+}
+
+.descriptionListSmall {
+  // offset the padding-bottom of last row
+  :global {
+    .ant-row {
+      margin-bottom: -8px;
+    }
+  }
+  .title {
+    margin-bottom: 12px;
+    color: @text-color;
+  }
+  .term, .detail {
+    padding-bottom: 8px;
+  }
+}
+.descriptionListLarge {
+  .title {
+    font-size: 16px;
+  }
+}

+ 39 - 0
src/components/DescriptionList/index.md

@@ -0,0 +1,39 @@
+---
+title:
+  en-US: DescriptionList
+  zh-CN: DescriptionList
+subtitle: 描述列表
+cols: 1
+order: 4
+---
+
+成组展示多个只读字段,常见于详情页的信息展示。
+
+## API
+
+### DescriptionList
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| layout    | 布局方式                                 | Enum{'horizontal', 'vertical'}  | 'horizontal' |
+| col       | 指定信息最多分几列展示,最终一行几列由 col 配置结合[响应式规则](/components/DescriptionList#响应式规则)决定          | number(0 < col <= 4)  | 3 |
+| title     | 列表标题                                 | ReactNode  | - |
+| gutter    | 列表项间距,单位为 `px`                    | number  | 32 |
+| size     | 列表型号,可以设置为 `large` `small`        | Enum{'large', 'small'}  | - |
+
+#### 响应式规则
+
+| 窗口宽度             | 展示列数                                      | 
+|---------------------|---------------------------------------------|
+| `≥768px`           |  `col`                                       |
+| `≥576px`           |  `col < 2 ? col : 2`                         |
+| `<576px`           |  `1`                                         |
+
+### DescriptionList.Description
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| term     | 列表项标题                                 | ReactNode  | - |
+
+
+

+ 6 - 0
src/components/DescriptionList/responsive.js

@@ -0,0 +1,6 @@
+export default {
+  1: { xs: 24 },
+  2: { xs: 24, sm: 12 },
+  3: { xs: 24, sm: 12, md: 8 },
+  4: { xs: 24, sm: 12, md: 6 },
+};

+ 25 - 0
src/components/DropOption/index.js

@@ -0,0 +1,25 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Dropdown, Button, Icon, Menu } from 'antd'
+
+const DropOption = ({ onMenuClick, menuOptions = [], buttonStyle, dropdownProps }) => {
+  const menu = menuOptions.map(item => <Menu.Item key={item.key}>{item.name}</Menu.Item>)
+  return (<Dropdown
+    overlay={<Menu onClick={onMenuClick}>{menu}</Menu>}
+    {...dropdownProps}
+  >
+    <Button style={{ border: 'none', ...buttonStyle }}>
+      <Icon style={{ marginRight: 2 }} type="bars" />
+      <Icon type="down" />
+    </Button>
+  </Dropdown>)
+}
+
+DropOption.propTypes = {
+  onMenuClick: PropTypes.func,
+  menuOptions: PropTypes.array.isRequired,
+  buttonStyle: PropTypes.object,
+  dropdownProps: PropTypes.object,
+}
+
+export default DropOption;

+ 99 - 0
src/models/order/detail.js

@@ -0,0 +1,99 @@
+import { queryOne, create, update } from '../../services/order';
+import { message } from 'antd';
+import pathToRegexp from 'path-to-regexp';
+import { Codes } from '../../utils/config';
+
+export default {
+  namespace: 'orderDetail',
+
+  state: {
+    filters: {},
+    operType: 'create',
+    currentItem: {},
+    modalVisible: false,
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state, ...rest }) => {
+        const match = pathToRegexp('/order/profile/:id').exec(pathname);
+        if (match) {
+          dispatch({ type: 'query', payload: { id: match[1] } });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'view' } });
+        }
+      });
+    }
+  },
+
+  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('创建成功!');
+        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 } }) {
+      const currentItem = { ...state.currentItem, wareList };
+      return { ...state, modalVisible: false, currentItem };
+    },
+
+    clearPage(state) {
+      return { ...state, currentItem: {}, itemLoading: false };
+    }
+  }
+}

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

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

+ 87 - 0
src/routes/Order/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/Order/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;
+        }
+      }
+    }
+  }
+}

+ 88 - 0
src/routes/Order/detail/orderProfile.js

@@ -0,0 +1,88 @@
+import React, { Component } from 'react';
+import { connect } from 'dva';
+import { Card, Badge, Table, Divider } from 'antd';
+import moment from 'moment';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import DescriptionList from '../../../components/DescriptionList';
+import { Codes, orderStatuses } from '../../../utils/config';
+import styles from './orderProfile.less';
+
+const { Description } = DescriptionList;
+
+@connect(state => ({ orderDetail: state.orderDetail }))
+export default class OrderProfile extends Component {
+  render() {
+    const { orderDetail } = this.props;
+    const { currentItem } = orderDetail;
+    const { code, gmtCreated, userCode, status, provinceCode, cityName, zoneName, classroomName } = currentItem;
+
+    const goodsTableColumns = [{
+      title: '商品编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '商品类型',
+      dataIndex: 'type',
+      key: 'type',
+    },{
+      title: '商品名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '领教定价',
+      dataIndex: 'lingjiaoPrice',
+      key: 'lingjiaoPrice',
+    },{
+      title: '渠道定价',
+      dataIndex: 'merchantPrice',
+      key: 'merchantPrice',
+    },{
+      title: '实际售价',
+      dataIndex: 'finalPrice',
+      key: 'finalPrice',
+    },{
+      title: '数量',
+      dataIndex: 'quality',
+      key: 'quality',
+    },{
+      title: '单位',
+      dataIndex: 'chargeUnit',
+      key: 'chargeUnit',
+    },{
+      title: '起始时间',
+      dataIndex: 'timeBegin',
+      key: 'timeBegin',
+    },{
+      title: '结束时间',
+      dataIndex: 'timeEnd',
+      key: 'timeEnd',
+    }];
+
+    return (
+      <PageHeaderLayout title="订单详情">
+        <Card bordered={false}>
+          <DescriptionList size="large" title="订单信息" col={2} style={{ marginBottom: 32 }}>
+            <Description term="订单编号">{code}</Description>
+            <Description term="创建时间">{moment(gmtCreated).format('YYYY-MM-DD HH:mm:ss')}</Description>
+            <Description term="终端编号">{userCode}</Description>
+            <Description term="终端名称">{`${provinceCode}-${cityName}-${zoneName}-${classroomName}`}</Description>
+            <Description term="支付状态">{orderStatuses[status]}</Description>
+          </DescriptionList>
+          <Divider style={{ marginBottom: 32 }} />
+          <DescriptionList size="large" title="收货信息" col={2} style={{ marginBottom: 32 }}>
+            <Description term="收货人">付小小</Description>
+            <Description term="联系电话">18100000000</Description>
+            <Description term="收货地址">浙江省杭州市西湖区万塘路18号</Description>
+            <Description term="备注">无</Description>
+          </DescriptionList>
+          <Divider style={{ marginBottom: 32 }} />
+          <div className={styles.title}>商品清单</div>
+          <Table
+            dataSource={[]}
+            columns={goodsTableColumns}
+          />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 8 - 0
src/routes/Order/detail/orderProfile.less

@@ -0,0 +1,8 @@
+@import "~antd/lib/style/themes/default.less";
+
+.title {
+  color: @heading-color;
+  font-size: 16px;
+  font-weight: 500;
+  margin-bottom: 16px;
+}

+ 145 - 0
src/routes/Order/index.js

@@ -0,0 +1,145 @@
+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 => ({
+  order: state.order,
+}))
+export default class Order extends PureComponent {
+  static propTypes = {
+    order: PropTypes.object,
+    location: PropTypes.object,
+    dispatch: PropTypes.func,
+  };
+
+  render() {
+    const { location, dispatch, order } = 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 } = order;
+
+    // 把携带的参数中空值项删除
+    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: '/order/add',
+            state: filters,
+          })
+        );
+      }
+    };
+
+    const listProps = {
+      pagination,
+      location,
+      dataSource: list,
+      loading: listLoading,
+      timeBegin: filters.timeBegin,
+      timeEnd: filters.timeEnd,
+      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,
+          }),
+        }));
+      },
+      onViewItem: (item) => {
+        dispatch(
+          routerRedux.push({
+            pathname: `/order/profile/${item.id}`,
+            state: filters,
+          })
+        );
+      },
+      onEditItem: (item) => {
+        dispatch(
+          routerRedux.push({
+            pathname: `/order/edit/${item.id}`,
+            state: filters,
+          })
+        );
+      },
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'order/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+      onRecoverItem: (payload) => {
+        dispatch({
+          type: 'order/recover',
+          payload,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      }
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 50 - 0
src/routes/Order/search.js

@@ -0,0 +1,50 @@
+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: 'campusName', name: '校区名称', mode: 'input',
+      },{
+        value: 'campusCode', name: '校区编号', mode: 'input',
+      },{
+        value: 'userCode', name: '终端编号', mode: 'input',
+      }],
+      selectProps: {
+        defaultValue: field || 'userCode',
+      },
+      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>
+    );
+  }
+}

+ 220 - 0
src/routes/Order/table.js

@@ -0,0 +1,220 @@
+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, DatePicker, Table, Menu, Icon, Badge } from 'antd';
+import AnimTableBody from '../../components/Animation/AnimTableBody';
+import DropOption from '../../components/DropOption';
+import styles from './table.less';
+import { orderStatuses, Codes } from '../../utils/config';
+
+const { RangePicker } = DatePicker;
+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,
+  };
+
+  state = {
+    filtered: false,       //是否处于过滤状态
+    timeBegin: null,       //起始时间 - 默认当前时间戳
+    timeEnd: null,         //结束时间 - 默认时间戳
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // 如果父组件传进的属性中包含timeBegin和timeEnd并进行合法性校验,通过则更新state
+    const { timeBegin, timeEnd } = nextProps;
+    const nextTimeBegin = Number(timeBegin);
+    const nextTimeEnd = Number(timeEnd);
+    if (nextTimeBegin && nextTimeEnd && moment(nextTimeBegin).isValid() && moment(nextTimeEnd).isValid()) {
+      this.setState({
+        timeBegin: moment(nextTimeBegin),
+        timeEnd: moment(nextTimeEnd),
+        filtered: true,
+      });
+    }
+  }
+
+  // 选择了筛选的时间段,点击确定后触发
+  handleRangePickerOnOk = (value) => {
+    const timeBegin = value[0];
+    const timeEnd = value[1];
+    this.setState({ timeBegin, timeEnd, filtered: true });
+  }
+
+  // 点击清除选中的时间段
+  handleRangePickerOnChange = (value) => {
+    if (value && !value.length) {
+      this.setState({
+        filtered: false,
+        timeBegin: null,
+        timeEnd: null,
+      });
+    } else {
+      this.setState({
+        filtered: true,
+        timeBegin: value[0],
+        timeEnd: value[1],
+      })
+    }
+  }
+
+  // 点击空白区域,隐藏RangePicker,会触发一次查询
+  handleRangePickerFilter = (visible) => {
+    if (!visible) {
+      const { pagination, onChange } = this.props;
+      // 这里构造成数组类型是为了和table自带的过滤参数类型一致
+      const data = { timeBegin: [], timeEnd: [] };
+      if (this.state.timeBegin && this.state.timeEnd) {
+        data.timeBegin = [this.state.timeBegin.format('X')];
+        data.timeEnd = [this.state.timeEnd.format('X')];
+      }
+      onChange(pagination, data);
+    }
+  }
+
+  handleMenuClick = (record, e) => {
+    const { onDeleteItem, onViewItem, onEditItem, onRecoverItem } = this.props;
+    if (e.key === '1') {
+      onViewItem(record);
+    } else if (e.key === '2') {
+      console.log('enter into edit page...');
+    }else if (e.key === '3') {
+      confirm({
+        title: '你确定要作废该订单?',
+        onOk () {
+          console.log('deleting...');
+          // onDeleteItem(record.id)
+        },
+      });
+    } else if (e.key === '4') {
+      confirm({
+        title: '你确定要恢复该订单?',
+        onOk () {
+          console.log('recovering...');
+        }
+      });
+    }
+  }
+
+  // 根据订单状态确定下拉菜单的选项
+  renderOperationDropMenu = (record) => {
+    switch (Number(record.status)) {
+      case Codes.CODE_PAID:
+        return [{key: '1', name: '查看'}];
+        break;
+      case Codes.CODE_UNPAID:
+        return [{key: '1', name: '查看'}, {key: '2', name: '修改'}, {key: '3', name: '作废'}];
+        break;
+      case Codes.CODE_CANCEL:
+        return [{key: '1', name: '查看'}, {key: '4', name: '恢复'}];
+        break;
+      default:
+        break;
+    }
+  }
+
+  render() {
+    const { curStatus, onDeleteItem, onRecoverItem, onEditItem, location, pagination, ...tableProps } = this.props;
+    const { timeBegin, timeEnd, filtered } = this.state;
+
+    const columns = [{
+      title: '订单编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '终端编号',
+      dataIndex: 'userCode',
+      key: 'userCode',
+    },{
+      title: '校区',
+      dataIndex: 'campusName',
+      key: 'campusName',
+      render: (text, record) => (
+        <span>{`${record.provinceCode}-${record.cityName}-${record.zoneName}`}</span>
+      ),
+    },{
+      title: '领教定价',
+      dataIndex: 'lingjiaoPrice',
+      key: 'lingjiaoPrice',
+    },{
+      title: '渠道定价',
+      dataIndex: 'merchantPrice',
+      key: 'merchantPrice',
+    },{
+      title: '实际售价',
+      dataIndex: 'finalPrice',
+      key: 'finalPrice',
+    },{
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {[Codes.CODE_PAID]: 'success', [Codes.CODE_UNPAID]: 'processing', [Codes.CODE_CANCEL]: 'error'};
+        return (<Badge status={statusMap[record.status]} text={orderStatuses[record.status]} />);
+      },
+      filters: Object.keys(orderStatuses).map(key => ({ text: orderStatuses[key], value: key })),
+      filterMultiple: false,
+      filteredValue: [curStatus],
+    },{
+      title: '下单时间',
+      dataIndex: 'gmtCreated',
+      key: 'gmtCreated',
+      render: (text, record) => (
+        <div>{moment(text).format('YYYY-MM-DD HH:mm:ss')}</div>
+      ),
+      filterIcon: <Icon type="clock-circle-o" style={{ color: filtered ? '#108ee9' : '#aaa' }} />,
+      filterDropdown: (
+        <div className="custom-filter-dropdown">
+          <RangePicker
+            value={filtered ? [timeBegin, timeEnd] : []}
+            showTime={{ format: 'HH:mm:ss' }}
+            format="YYYY-MM-DD HH:mm:ss"
+            placeholder={["起始时间", "截止时间"]}
+            onChange={this.handleRangePickerOnChange}
+            onOk={this.handleRangePickerOnOk}
+            getCalendarContainer={() => document.querySelector('.custom-filter-dropdown')}
+          />
+        </div>
+      ),
+      onFilterDropdownVisibleChange: this.handleRangePickerFilter,
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <DropOption onMenuClick={e => this.handleMenuClick(record, e)} menuOptions={this.renderOperationDropMenu(record)} />
+      ),
+    }];
+
+    // 数据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}
+        scroll={{ x: 1300 }}
+        getBodyWrapper={getBodyWrapper}
+      />
+    );
+  }
+}

+ 94 - 0
src/routes/Order/table.less

@@ -0,0 +1,94 @@
+@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: 12%;
+        }
+
+        &:nth-child(2) {
+          width: 12%;
+        }
+
+        &:nth-child(3) {
+          width: 13%;
+        }
+
+        &:nth-child(4) {
+          width: 9%;
+        }
+
+        &:nth-child(5) {
+          width: 9%;
+        }
+
+        &:nth-child(6) {
+          width: 9%;
+        }
+
+        &:nth-child(7) {
+          width: 10%;
+        }
+
+        &:nth-child(8) {
+          width: 18%;
+        }
+
+        &:nth-child(9) {
+          width: 8%;
+        }
+      }
+
+      .ant-table-thead {
+        & > tr {
+          transition: none;
+          display: block;
+
+          & > th {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+
+      .ant-table-tbody {
+        & > tr {
+          transition: none;
+          display: block;
+          border-bottom: 1px solid #f5f5f5;
+
+          & > td {
+            border-bottom: none;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+
+          &.ant-table-expanded-row-level-1 > td {
+            height: auto;
+          }
+        }
+      }
+    }
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 32 - 0
src/services/order.js

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

+ 2 - 0
src/utils/api.js

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

+ 9 - 0
src/utils/config.js

@@ -10,6 +10,9 @@ Codes.CODE_DELETE = 'DEL';
 Codes.CODE_COURSE = 'COURSE';
 Codes.CODE_SUPPORT = 'SUPPORT';
 Codes.CODE_SALE = 'SALE';
+Codes.CODE_UNPAID = 0;
+Codes.CODE_PAID = 1;
+Codes.CODE_CANCEL = 2;
 
 module.exports = {
   // apiHost: 'http://lj.dev.cms.api.com:8500',
@@ -43,5 +46,11 @@ module.exports = {
   tagType: {
     [Codes.CODE_COURSE] : '课程',
     [Codes.CODE_SUPPORT]: '周边',
+  },
+  // 订单状态
+  orderStatuses: {
+    [Codes.CODE_UNPAID]: '未支付',
+    [Codes.CODE_PAID]: '已支付',
+    [Codes.CODE_CANCEL]: '已作废',
   }
 };