Browse Source

2B三方管理平台框架搭建

zhanghe 6 years ago
commit
e350d229df
100 changed files with 9137 additions and 0 deletions
  1. 16 0
      .editorconfig
  2. 53 0
      .eslintrc
  3. 3 0
      .ga
  4. 15 0
      .gitignore
  5. 23 0
      .roadhogrc
  6. 565 0
      .roadhogrc.mock.js
  7. 25 0
      .stylelintrc
  8. 96 0
      mock/item.js
  9. 128 0
      mock/product.js
  10. 65 0
      mock/resource.js
  11. 56 0
      mock/utils.js
  12. 111 0
      package.json
  13. 16 0
      public/index.html
  14. BIN
      src/assets/logo-tb.png
  15. BIN
      src/assets/yay.jpg
  16. 228 0
      src/common/ljNav.js
  17. 17 0
      src/components/DescriptionList/Description.js
  18. 21 0
      src/components/DescriptionList/DescriptionList.js
  19. 35 0
      src/components/DescriptionList/demo/basic.md
  20. 35 0
      src/components/DescriptionList/demo/vertical.md
  21. 5 0
      src/components/DescriptionList/index.js
  22. 75 0
      src/components/DescriptionList/index.less
  23. 37 0
      src/components/DescriptionList/index.md
  24. 6 0
      src/components/DescriptionList/responsive.js
  25. 202 0
      src/components/EditableTable/index.js
  26. 7 0
      src/components/EditableTable/index.less
  27. 21 0
      src/components/Exception/demo/403.md
  28. 14 0
      src/components/Exception/demo/404.md
  29. 14 0
      src/components/Exception/demo/500.md
  30. 33 0
      src/components/Exception/index.js
  31. 78 0
      src/components/Exception/index.less
  32. 19 0
      src/components/Exception/index.md
  33. 19 0
      src/components/Exception/typeConfig.js
  34. 33 0
      src/components/GlobalFooter/demo/basic.md
  35. 27 0
      src/components/GlobalFooter/index.js
  36. 29 0
      src/components/GlobalFooter/index.less
  37. 15 0
      src/components/GlobalFooter/index.md
  38. 255 0
      src/components/ModalSelectTable/index.js
  39. 16 0
      src/components/ModalSelectTable/index.less
  40. 137 0
      src/components/PageHeader/index.js
  41. 138 0
      src/components/PageHeader/index.less
  42. 26 0
      src/components/PageHeader/index.md
  43. 26 0
      src/components/StandardFormRow/index.js
  44. 71 0
      src/components/StandardFormRow/index.less
  45. 93 0
      src/components/StandardTable/index.js
  46. 18 0
      src/components/StandardTable/index.less
  47. 27 0
      src/index.js
  48. 20 0
      src/index.less
  49. 392 0
      src/layouts/BasicLayout.js
  50. 114 0
      src/layouts/BasicLayout.less
  51. 12 0
      src/layouts/PageHeaderLayout.js
  52. 11 0
      src/layouts/PageHeaderLayout.less
  53. 135 0
      src/models/course.js
  54. 41 0
      src/models/cp.js
  55. 76 0
      src/models/global.js
  56. 127 0
      src/models/image.js
  57. 9 0
      src/models/index.js
  58. 119 0
      src/models/item.js
  59. 134 0
      src/models/lesson.js
  60. 41 0
      src/models/merchant.js
  61. 143 0
      src/models/support.js
  62. 141 0
      src/models/tag.js
  63. 128 0
      src/models/ware.js
  64. 7 0
      src/polyfill.js
  65. 63 0
      src/router.js
  66. 7 0
      src/routes/Exception/403.js
  67. 7 0
      src/routes/Exception/404.js
  68. 7 0
      src/routes/Exception/500.js
  69. 217 0
      src/routes/Goods/ItemList.js
  70. 19 0
      src/routes/Goods/ItemList.less
  71. 233 0
      src/routes/Goods/ItemSave.js
  72. 13 0
      src/routes/Goods/ItemSave.less
  73. 237 0
      src/routes/Product/CourseList.js
  74. 19 0
      src/routes/Product/CourseList.less
  75. 161 0
      src/routes/Product/CourseSave.js
  76. 13 0
      src/routes/Product/CourseSave.less
  77. 224 0
      src/routes/Product/LessonList.js
  78. 19 0
      src/routes/Product/LessonList.less
  79. 538 0
      src/routes/Product/LessonSave.js
  80. 13 0
      src/routes/Product/LessonSave.less
  81. 217 0
      src/routes/Product/SupportList.js
  82. 19 0
      src/routes/Product/SupportList.less
  83. 378 0
      src/routes/Product/SupportSave.js
  84. 13 0
      src/routes/Product/SupportSave.less
  85. 217 0
      src/routes/Product/TagList.js
  86. 25 0
      src/routes/Product/TagList.less
  87. 188 0
      src/routes/Product/TagSave.js
  88. 7 0
      src/routes/Product/TagSave.less
  89. 262 0
      src/routes/Product/WareList.js
  90. 19 0
      src/routes/Product/WareList.less
  91. 636 0
      src/routes/Product/WareSave.js
  92. 7 0
      src/routes/Product/WareSave.less
  93. 398 0
      src/routes/Resource/ImageList.js
  94. 53 0
      src/routes/Resource/ImageList.less
  95. 140 0
      src/routes/Resource/VideoList.js
  96. 104 0
      src/routes/Resource/VideoList.less
  97. 11 0
      src/services/CPApi.js
  98. 43 0
      src/services/ItemAPI.js
  99. 11 0
      src/services/MerchantApi.js
  100. 0 0
      src/services/ProductApi.js

+ 16 - 0
.editorconfig

@@ -0,0 +1,16 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab

+ 53 - 0
.eslintrc

@@ -0,0 +1,53 @@
+{
+  "parser": "babel-eslint",
+  "extends": "airbnb",
+  "env": {
+    "browser": true,
+    "node": true,
+    "es6": true,
+    "mocha": true,
+    "jest": true,
+    "jasmine": true
+  },
+  "rules": {
+    "generator-star-spacing": [0],
+    "consistent-return": [0],
+    "react/forbid-prop-types": [0],
+    "react/jsx-filename-extension": [1, { "extensions": [".js"] }],
+    "global-require": [1],
+    "import/prefer-default-export": [0],
+    "react/jsx-no-bind": [0],
+    "react/prop-types": [0],
+    "react/prefer-stateless-function": [0],
+    "no-else-return": [0],
+    "no-restricted-syntax": [0],
+    "import/no-extraneous-dependencies": [0],
+    "no-use-before-define": [0],
+    "jsx-a11y/no-static-element-interactions": [0],
+    "jsx-a11y/no-noninteractive-element-interactions": [0],
+    "jsx-a11y/click-events-have-key-events": [0],
+    "jsx-a11y/anchor-is-valid": [0],
+    "no-nested-ternary": [0],
+    "arrow-body-style": [0],
+    "import/extensions": [0],
+    "no-bitwise": [0],
+    "no-cond-assign": [0],
+    "import/no-unresolved": [0],
+    "comma-dangle": ["error", {
+      "arrays": "always-multiline",
+      "objects": "always-multiline",
+      "imports": "always-multiline",
+      "exports": "always-multiline",
+      "functions": "ignore"
+    }],
+    "object-curly-newline": [0],
+    "function-paren-newline": [0],
+    "no-restricted-globals": [0],
+    "require-yield": [1]
+  },
+  "parserOptions": {
+    "ecmaFeatures": {
+      "experimentalObjectRestSpread": true
+    }
+  }
+}

+ 3 - 0
.ga

@@ -0,0 +1,3 @@
+{
+    "code":"UA-72788897-6"
+} 

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+# roadhog-api-doc ignore
+/src/utils/request-temp.js
+
+# production
+/dist
+
+# misc
+.DS_Store
+npm-debug.log*
+
+/coverage

+ 23 - 0
.roadhogrc

@@ -0,0 +1,23 @@
+{
+  "entry": "src/index.js",
+  "extraBabelPlugins": [
+    "transform-runtime",
+    "transform-decorators-legacy",
+    "transform-class-properties",
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }]
+  ],
+  "env": {
+    "development": {
+      "extraBabelPlugins": [
+        "dva-hmr"
+      ]
+    }
+  },
+  "externals": {
+    "g2": "G2",
+    "g-cloud": "Cloud",
+    "g2-plugin-slider": "G2.Plugin.slider"
+  },
+  "ignoreMomentLocale": true,
+  "theme": "./src/theme.js"
+}

+ 565 - 0
.roadhogrc.mock.js

@@ -0,0 +1,565 @@
+import mockjs from 'mockjs';
+import { format, delay } from 'roadhog-api-doc';
+import config from './src/utils/config';
+import Logger from './src/utils/logger';
+import { imageDatabase, videoDatabase } from './mock/resource';
+import { tagList, wareList, lessonList, courseList, supportList } from './mock/product';
+import { itemList } from './mock/item';
+
+const logger = Logger.getLogger('RoadhogMock');
+
+// 是否禁用代理
+const noProxy = process.env.NO_PROXY === 'true';
+
+// 测试API
+const OFFLINE_API_URL = 'http://192.168.199.119:8100';
+
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
+const proxy = {
+  [`GET ${config.api.resourceList}`]: (req, res) => {
+    let params = req.query;
+    if (!params || !params.type) {
+      res.send({
+        code: '401',
+        message: '请求参数不完整!'
+      });
+    }
+    else {
+      let dataset = [];
+      if (parseInt(params.type) === config.RESOURCE_TYPE_IMAGE) {
+        dataset = imageDatabase;
+      }
+      else if (parseInt(params.type) === config.RESOURCE_TYPE_VIDEO) {
+        dataset = videoDatabase;
+      }
+
+      let type = params.type;
+      let pageSize = params.pageSize ? parseInt(params.pageSize) : 10;
+      let pageNo = params.pageNo ? parseInt(params.pageNo) : 1;
+      let totalSize = dataset.length;
+      let totalNo = Math.floor(dataset.length / pageSize) + 1;
+      let start = pageNo * pageSize;
+      let list = dataset.slice(start, start + pageSize);
+
+      res.send({
+        code: '200',
+        message: 'Request success!',
+        data: { pageNo, pageSize, totalSize, list }
+      });
+    }
+  },
+
+  [`GET ${config.api.wareList}`]: (req, res) => {
+    let params = req.query;
+    let dataset = wareList;
+
+    logger.info('【WareList Request】: ', params);
+    let pageSize = params.pageSize ? parseInt(params.pageSize) : 10;
+    let pageNo = params.pageNo ? parseInt(params.pageNo) : 1;
+    let totalSize = dataset.length;
+    let start = pageNo * pageSize;
+    let list = dataset.slice(start, start + pageSize);
+
+    res.send({
+      code: '200',
+      message: 'Request success!',
+      data: { pageNo, pageSize, totalSize, list }
+    });
+  },
+
+  [`GET ${config.api.lessonList}`]: (req, res) => {
+    let params = req.query;
+    let dataset = lessonList;
+
+    logger.info('【LessonList Request】: ', params);
+    let pageSize = params.pageSize ? parseInt(params.pageSize) : 10;
+    let pageNo = params.pageNo ? parseInt(params.pageNo) : 1;
+    let totalSize = dataset.length;
+    let start = pageNo * pageSize;
+    let list = dataset.slice(start, start + pageSize);
+
+    res.send({
+      code: '200',
+      message: 'Request success!',
+      data: { pageNo, pageSize, totalSize, list }
+    });
+  },
+
+  [`GET ${config.api.lessonItem}`]: (req, res) => {
+    logger.info('【LessonItem】:', req.params);
+
+    res.send({
+      "code": 200,
+       "success": true,
+       "message": null,
+       "data": {
+           "id": 1511504034666794,
+           "code": "K-test-310",
+           "name": "小学310",
+           "digest": "霍冬冬是个大傻蛋,这是一句很中肯的话,不要狡辩!",
+           "sort": null,
+           "status": "NORMAL",
+           "gmtCreated": null,
+           "gmtModified": null,
+           "wareList": [{
+             "id": 1511425213814978,
+             "code": "J-test-310",
+             "name": "小学310",
+             "digest": null,
+             "type": "SINGLE",
+             "playUrl": null,
+             "imgUrls": [
+               "http://www.xxx.c/img.jpg",
+            ],
+            "sort": null,
+            "status": "NORMAL",
+            "gmtCreated": 1511264969000,
+            "gmtModified": 1511264969000
+          }]
+        }
+    });
+  },
+
+  [`POST ${config.api.lessonItemAdd}`]: (req, res) => {
+    logger.info('【LessonItemAdd】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '新建课成功',
+      data: {},
+    });
+  },
+
+  [`PUT ${config.api.lessonItemUpdate}`]: (req, res) => {
+    logger.info('【LessonItemUpdate】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '更新课成功',
+      data: {},
+    });
+  },
+
+  [`DELETE ${config.api.lessonItemDel}`]: (req, res) => {
+    logger.info('【LessonItemDelete】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '删除课成功',
+      data: {},
+    });
+  },
+
+  [`GET ${config.api.courseList}`]: (req, res) => {
+    let params = req.query;
+    let dataset = courseList;
+
+    logger.info('【CourseList Request】: ', params);
+    let pageSize = params.pageSize ? parseInt(params.pageSize) : 10;
+    let pageNo = params.pageNo ? parseInt(params.pageNo) : 1;
+    let totalSize = dataset.length;
+    let start = pageNo * pageSize;
+    let list = dataset.slice(start, start + pageSize);
+
+    res.send({
+      code: '200',
+      message: 'Request success!',
+      data: { pageNo, pageSize, totalSize, list }
+    });
+  },
+
+  [`GET ${config.api.courseItem}`]: (req, res) => {
+    logger.info('【CourseItem】:', req.params);
+
+    res.send({
+      "code": 200,
+      "success": true,
+      "message": null,
+      "data": {
+        "id": 1511767193919357,
+        "code": "C-test-311",
+        "name": "小学311",
+        "title": null,
+        "digest": null,
+        "detail": null,
+        "keyword": null,
+        "cvImgIds": null,
+        "bgImgIds": null,
+        "status": null,
+        "gmtCreated": null,
+        "gmtModified": null,
+        "subItemList": [{
+          "id": 1511509291305739,
+          "code": "K-test-311",
+          "name": "小学310",
+          "digest": null,
+          "type": "LESSON",
+          "status": null,
+          "gmtCreated": null,
+          "gmtModified": null
+        }],
+        "supportList": [{
+          "id": 1511835822190182,
+          "code": "S-test-310",
+          "name": "小学310",
+          "title": null,
+          "digest": null,
+          "detail": null,
+          "imgIds": null,
+          "status": null,
+          "gmtCreated": null,
+          "gmtModified": null
+        }]
+      }
+    })
+  },
+
+  [`POST ${config.api.courseItemAdd}`]: (req, res) => {
+    logger.info('【CourseItemAdd】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '新建课程成功',
+      data: {},
+    });
+  },
+
+  [`PUT ${config.api.courseItemUpdate}`]: (req, res) => {
+    logger.info('【CourseItemUpdate】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '更新课程成功',
+      data: {},
+    });
+  },
+
+  [`DELETE ${config.api.courseItemDel}`]: (req, res) => {
+    logger.info('【CourseItemDelete】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '删除课程成功',
+      data: {},
+    });
+  },
+
+  [`GET ${config.api.supportList}`]: (req, res) => {
+    let params = req.query;
+    let dataset = supportList;
+
+    logger.info('【SupportList Request】: ', params);
+    let pageSize = params.pageSize ? parseInt(params.pageSize) : 10;
+    let pageNo = params.pageNo ? parseInt(params.pageNo) : 1;
+    let totalSize = dataset.length;
+    let start = pageNo * pageSize;
+    let list = dataset.slice(start, start + pageSize);
+
+    res.send({
+      code: '200',
+      message: 'Request success!',
+      data: { pageNo, pageSize, totalSize, list }
+    });
+  },
+
+  [`GET ${config.api.supportItem}`]: (req, res) => {
+    logger.info('【SupportItem】:', req.params);
+
+    res.send({
+      "code": 200,
+       "success": true,
+       "message": null,
+       "data": {
+           "id": 1,
+           "code": "S-test-310",
+           "name": "先学急用的汉字1",
+           "digest": "周边描述周边描述周边描述周边描述周边描述周边描述周边描述周边描述周边描述",
+           "tags": [{tagId: 15, tagName:'EQ情商教育-15'}, {tagId:17, tagName:'EQ情商教育-2'}],
+           "status": "NORMAL",
+           "cpId": 1,
+           "cpName": "贝尔安亲",
+           "gmtCreated": null,
+           "gmtModified": null,
+           "playUrl": 'http://www.baidu.com',
+           "imgUrls": ['http://www.baidu.com'],
+           "supportArr": [{
+             "id": 2,
+             "code": "S-test-311",
+             "name": "先学急用的汉字2",
+             "digest": "周边描述周边描述周边描述周边描述周边描述周边描述周边描述周边描述周边描述",
+             "tags": [{15: 'EQ情商教育-15'}, {17: 'EQ情商教育-17'}],
+             "status": "NORMAL",
+             "cpId": 1,
+             "cpName": "贝尔安亲",
+             "playUrl": 'http://www.baidu.com',
+             "imgUrls": ['http://www.baidu.com'],
+             "gmtCreated": null,
+             "gmtModified": null,
+          }]
+        }
+    });
+  },
+
+  [`POST ${config.api.supportItemAdd}`]: (req, res) => {
+    logger.info('【SupportItemAdd】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '新建周边成功',
+      data: {},
+    });
+  },
+
+  [`PUT ${config.api.supportItemUpdate}`]: (req, res) => {
+    logger.info('【SupportItemUpdate】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '更新周边成功',
+      data: {},
+    });
+  },
+
+  [`DELETE ${config.api.supportItemDel}`]: (req, res) => {
+    logger.info('【SupportItemDelete】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '删除周边成功',
+      data: {},
+    });
+  },
+
+  [`GET ${config.api.itemList}`]: (req, res) => {
+    let params = req.query;
+    let dataset = itemList;
+
+    logger.info('【ItemList Request】: ', params);
+    let pageSize = params.pageSize ? parseInt(params.pageSize) : 10;
+    let pageNo = params.pageNo ? parseInt(params.pageNo) : 1;
+    let totalSize = dataset.length;
+    let start = pageNo * pageSize;
+    let list = dataset.slice(start, start + pageSize);
+
+    res.send({
+      code: '200',
+      message: 'Request success!',
+      data: { pageNo, pageSize, totalSize, list }
+    });
+  },
+
+  [`GET ${config.api.itemItem}`]: (req, res) => {
+    logger.info('【ItemItem Request】: ', req.params);
+    res.send({
+      code: 200,
+      message: null,
+      data: { ...itemList[0] },
+    });
+  },
+
+  [`POST ${config.api.itemItemAdd}`]: (req, res) => {
+    logger.info('【ItemItemAdd】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '新建商品成功',
+      data: {},
+    });
+  },
+
+  [`PUT ${config.api.itemItemUpdate}`]: (req, res) => {
+    logger.info('【ItemItemUpdate】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '更新商品信息成功',
+      data: {},
+    });
+  },
+
+  [`DELETE ${config.api.itemItemDel}`]: (req, res) => {
+    logger.info('【ItemItemDelete】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '删除商品成功',
+      data: {},
+    });
+  },
+
+  [`POST ${config.api.wareItemAdd}`]: (req, res) => {
+    logger.info('【WareItemAdd】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '新建课件成功',
+      data: {},
+    });
+  },
+
+  [`PUT ${config.api.wareItemUpdate}`]: (req, res) => {
+    logger.info('【WareItemUpdate】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '更新课件成功',
+      data: {},
+    });
+  },
+
+  [`DELETE ${config.api.wareItemDel}`]: (req, res) => {
+    logger.info('【WareItemDelete】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '删除课件成功',
+      data: {},
+    });
+  },
+
+  [`GET ${config.api.tagList}`]: (req, res) => {
+    let params = req.query;
+    let dataset = tagList;
+
+    logger.info('【TagList Request】: ', params);
+    let pageSize = params.pageSize ? parseInt(params.pageSize) : 10;
+    let pageNo = params.pageNo ? parseInt(params.pageNo) : 1;
+    let totalSize = dataset.length;
+    let start = pageNo * pageSize;
+    let list = dataset.slice(start, start + pageSize);
+
+    res.send({
+      code: '200',
+      message: 'Request success!',
+      data: { pageNo, pageSize, totalSize, list }
+    });
+  },
+
+  [`GET ${config.api.tagItem}`]: (req, res) => {
+    logger.info('【TagItem】:', req.params);
+
+    res.send({
+      "code": 200,
+      "success": true,
+      "message": null,
+      "data": {
+        "tagId": 1511767193919357,
+        "tagName": "EQ情商教育",
+        "status": null,
+        "type": 0,
+        "gmtCreated": null,
+        "gmtModified": null,
+        "merchantId": 0,
+        "merchantName": "贝尔安亲",
+      }
+    })
+  },
+
+  [`POST ${config.api.tagItemAdd}`]: (req, res) => {
+    logger.info('【tagItemAdd】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '新建标签成功',
+      data: {},
+    });
+  },
+
+  [`PUT ${config.api.tagItemUpdate}`]: (req, res) => {
+    logger.info('【TagItemUpdate】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '更新标签成功',
+      data: {},
+    });
+  },
+
+  [`DELETE ${config.api.tagItemDel}`]: (req, res) => {
+    logger.info('【TagItemDelete】:', req.body);
+
+    res.send({
+      code: 200,
+      message: '删除标签成功',
+      data: {},
+    });
+  },
+
+  [`GET ${config.api.merchantList}`]: {
+    code: 200,
+    message: 'Request success',
+    success: true,
+    data: {
+      list:[
+        {
+          merchantId: 0,
+          merchantName: '贝尔安亲',
+        },{
+          merchantId: 1,
+          merchantName: '昂乐教育',
+        },{
+          merchantId: 2,
+          merchantName: '好托管',
+        },{
+          merchantId: 3,
+          merchantName: '红黄蓝',
+        },{
+          merchantId: 4,
+          merchantName: '英乐教育',
+        }
+      ]
+    }
+  },
+
+  [`GET ${config.api.cpList}`]: {
+    code: 200,
+    message: 'Request success',
+    success: true,
+    data: {
+      list:[
+        {
+          cpId: 0,
+          cpName: '贝尔安亲',
+        },{
+          cpId: 1,
+          cpName: '昂乐教育',
+        },{
+          cpId: 2,
+          cpName: '鲨鱼公园',
+        }
+      ]
+    }
+  },
+
+  [`GET /api/v1/oss/upload`]: {
+    "code": 200,
+    "success": true,
+    "message": null,
+    "data": {
+        "accessid": "LTAIUFvd17IXLBQ4",
+        "policy": "eyJleHBpcmF0aW9uIjoiMjAxNy0xMS0yMlQwMzoyNTo0Mi41NjFaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJyZXNvdXJjZXMiXV19",
+        "signature": "YxqziyjItL6F4F0OzfxaBOTg6Ok=",
+        "expire": "1511321142",
+        "dir": "resources",
+        "path": "a/b/001.png",
+        // "host": "http://efunimgs.oss-cn-beijing-internal.aliyuncs.com"
+        "host": "http://localhost:8000/oss/upload"
+    }
+  },
+
+  [`GET /oss/upload`]: {
+    "code": 200,
+    "success": true,
+    "message": null,
+    "data": {
+      "fileName": "a-b-c.png",
+      "path": "user-dir",
+      "size": 80,
+    }
+  }
+};
+
+export default noProxy ? {} : delay(proxy, 1000);
+// export default {
+//   [`GET ${config.apiPrefix}/(.*)`]: `${OFFLINE_API_URL}`,
+// }

+ 25 - 0
.stylelintrc

@@ -0,0 +1,25 @@
+{
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "selector-pseudo-class-no-unknown": null,
+    "shorthand-property-no-redundant-values": null,
+    "at-rule-empty-line-before": null,
+    "at-rule-name-space-after": null,
+    "comment-empty-line-before": null,
+    "declaration-bang-space-before": null,
+    "declaration-empty-line-before": null,
+    "function-comma-newline-after": null,
+    "function-name-case": null,
+    "function-parentheses-newline-inside": null,
+    "function-max-empty-lines": null,
+    "function-whitespace-after": null,
+    "number-leading-zero": null,
+    "number-no-trailing-zeros": null,
+    "rule-empty-line-before": null,
+    "selector-combinator-space-after": null,
+    "selector-list-comma-newline-after": null,
+    "selector-pseudo-element-colon-notation": null,
+    "unit-no-unknown": null,
+    "value-list-max-empty-lines": null
+  }
+}

File diff suppressed because it is too large
+ 96 - 0
mock/item.js


+ 128 - 0
mock/product.js

@@ -0,0 +1,128 @@
+import { randomSelectOne, randomSelectN } from './utils';
+
+//mock wareListData
+let wareList = [];
+let state = [0, 1];
+let typeArr = [0, 1, 1, 1, 1];
+let cp = {0: '贝尔安亲', 1: '昂乐教育', 2: '鲨鱼公园'};
+let tagsArr = [{1: '幼小衔接'}, {2: 'EQ情商'}, {3: '中华教育'}];
+let imgsArr = [
+  {8: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/01/04/000008.jpg'},
+  {72:'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/09/700072.jpg'},
+  {73:'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/09/700073.jpg'},
+  {74:'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/09/700074.jpg'},
+  {75:'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/09/700075.jpg'},
+];
+
+for (let i = 1; i < 401; i++) {
+  let cpId = randomSelectOne([0, 1, 2]);
+  let cpName = cp[cpId];
+  wareList.push({
+    id: i,
+    code: 'J-001-' + i,
+    name: `第${i}课`,
+    type: randomSelectOne(typeArr),
+    tags: randomSelectN(tagsArr),
+    playUrl: `http://www.lj-2b.com/play/J-001-${i}`,
+    imgUrls: imgsArr,
+    cpId: cpId,
+    cpName: cpName,
+    state: randomSelectOne(state),
+    gmtCreated: (new Date()).getTime(),
+    gmtModified: (new Date()).getTime(),
+  });
+}
+
+// mock taglist
+let tagList = [];
+const tagTypeArr = ['课程', '师训', '周边'];
+const merchant = [{
+  merchantId: 0,
+  merchantName: '贝尔安亲',
+},{
+  merchantId: 1,
+  merchantName: '昂乐教育',
+},{
+  merchantId: 2,
+  merchantName: '好托管',
+},{
+  merchantId: 3,
+  merchantName: '红黄蓝',
+},{
+  merchantId: 4,
+  merchantName: '英乐教育',
+}];
+for (let i = 1; i < 300; i++) {
+  const merchantItem = randomSelectOne(merchant);
+  tagList.push({
+    tagId: i,
+    tagName: `EQ情商教育-${i}`,
+    tagType: randomSelectOne(tagTypeArr),
+    merchantName: merchantItem.merchantName,
+    merchantId: merchantItem.merchantId,
+    gmtModified: (new Date()).getTime(),
+  });
+}
+
+//mock lessonlist
+let lessonList = [];
+let status = ['NORMAL', 'DELETE'];
+for (let i = 1; i < 501; i++) {
+  lessonList.push({
+    id: 1511509291305739 + i,
+    code: `K-test-${i}`,
+    name: `第${i}课`,
+    digest: '这是一段描述,不知道描述个啥,但即便你是个描述,我也要写的很认真!',
+    sort: null,
+    status: randomSelectOne(status),
+    state: randomSelectOne(state),
+    gmtCreated: (new Date()).getTime(),
+    gmtModified: (new Date()).getTime(),
+  });
+}
+
+//mock courselist
+let courseList = [];
+for (let i = 1; i < 400; i++) {
+  courseList.push({
+    id: 151150929133433 + i,
+    code: `C-001-002-${i}`,
+    name: `小学${i}年级语文上册`,
+    title: `[${i}年级] 语文上册`,
+    digest: '顾名思义,这是交小学生语文的,哦不,不一定非得是小学生,400年级是什么鬼?',
+    detail: null,
+    cpId: 8000,
+    cpName: '鲨鱼公园',
+    status: status[i % 2],
+    gmtCreated: (new Date()).getTime(),
+    gmtModified: (new Date()).getTime(),
+    subItemList: [],
+  })
+}
+
+//mock supportlist
+let supportList = [];
+for (let i = 1; i < 400; i++) {
+  supportList.push({
+    id: 151150929133433 + i,
+    code: `S-001-002-${i}`,
+    name: `学生练习册[sub]急用先学的汉字${i}`,
+    title: `[学生练习册] 急用先学的汉字${i}`,
+    digest: '顾名思义,这是周边配套,周边配套,周边配套,周边配套,妈呀,累死我了!',
+    cpId: 8000,
+    tags: [{tagId: 1, tagName: '师训'}, {tagId:2, tagName: '幼小衔接'}],
+    cpName: '鲨鱼公园',
+    status: status[i % 2],
+    gmtCreated: (new Date()).getTime(),
+    gmtModified: (new Date()).getTime(),
+    supportArr: [],
+  })
+}
+
+module.exports = {
+  wareList,
+  tagList,
+  lessonList,
+  courseList,
+  supportList,
+}

+ 65 - 0
mock/resource.js

@@ -0,0 +1,65 @@
+import { randomSelectOne, randomSelectN } from './utils';
+
+const lodash = require('lodash');
+
+const Mock = require('mockjs');
+const Random = Mock.Random;
+
+let imageDatabase = [];        //图片资源库
+let videoDatabase = [];       //视频资源库
+
+const baseImageData = [{
+  id: 1,
+  code: 'J-02-01-612101',
+  name: '破解表情密码01',
+  size: 90,
+  status: 'NORMAL',
+  gmtCreated: Random.datetime('yyyy-MM-dd'),
+  gmtModified: Random.datetime('yyyy-MM-dd'),
+  url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/612102.jpg',
+},{
+  id: 2,
+  code: 'J-02-01-612102',
+  name: '破解表情秘码02',
+  size: 90,
+  status: 'NORMAL',
+  gmtCreated: Random.datetime('yyyy-MM-dd'),
+  gmtModified: Random.datetime('yyyy-MM-dd'),
+  url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/612101.jpg',
+},{
+  id: 3,
+  code: 'J-02-01-611901',
+  name: '看得见的旗语01',
+  size: 90,
+  status: 'NORMAL',
+  gmtCreated: Random.datetime('yyyy-MM-dd'),
+  gmtModified: Random.datetime('yyyy-MM-dd'),
+  url: 'http://efunimgs.oss-cn-beijing.aliyuncs.com/resources/J/02/01/611901.jpg',
+}];
+
+const baseVideoData = [{
+  id: 1,
+  code: 'J-03-02-600149',
+  name: '自制保险箱',
+  size: 90,
+  status: 'NORAML',
+  gmtCreated: Random.datetime('yyyy-MM-dd'),
+  gmtModified: Random.datetime('yyyy-MM-dd'),
+  url: 'http://efunvideo.ai160.com/vs2m/015/01503009/01503009031/01503009031.m3u8',
+}];
+
+for (let i = 1; i < 200; i++) {
+  let imageItem = randomSelectOne(baseImageData);
+  let videoItem = randomSelectOne(baseVideoData);
+  let imgTmp = lodash.cloneDeep(imageItem);
+  let videoTmp = lodash.cloneDeep(videoItem);
+  imgTmp.key = i;
+  videoTmp.key = i;
+  imageDatabase.push(imgTmp);
+  videoDatabase.push(videoTmp);
+}
+
+module.exports = {
+  imageDatabase,
+  videoDatabase,
+}

+ 56 - 0
mock/utils.js

@@ -0,0 +1,56 @@
+
+/**
+ * 随机选取数组中的一个值
+ */
+function randomSelectOne(arr) {
+  return arr[Math.floor(Math.random()*(arr.length -1))];
+}
+
+/**
+ * 随机选取数组中的N个值
+ */
+function randomSelectN(arr) {
+    let MAX = Math.floor(Math.random()*(arr.length - 1));
+    return arr.slice(0, MAX);
+}
+
+/**
+ * 解析url中参数
+ */
+export function getUrlParams(url) {
+  const d = decodeURIComponent;
+  let queryString = url ? url.split('?')[1] : window.location.search.slice(1);
+  const obj = {};
+  if (queryString) {
+    queryString = queryString.split('#')[0]; // eslint-disable-line
+    const arr = queryString.split('&');
+    for (let i = 0; i < arr.length; i += 1) {
+      const a = arr[i].split('=');
+      let paramNum;
+      const paramName = a[0].replace(/\[\d*\]/, (v) => {
+        paramNum = v.slice(1, -1);
+        return '';
+      });
+      const paramValue = typeof (a[1]) === 'undefined' ? true : a[1];
+      if (obj[paramName]) {
+        if (typeof obj[paramName] === 'string') {
+          obj[paramName] = d([obj[paramName]]);
+        }
+        if (typeof paramNum === 'undefined') {
+          obj[paramName].push(d(paramValue));
+        } else {
+          obj[paramName][paramNum] = d(paramValue);
+        }
+      } else {
+        obj[paramName] = d(paramValue);
+      }
+    }
+  }
+  return obj;
+}
+
+module.exports = {
+  randomSelectOne,
+  randomSelectN,
+  getUrlParams,
+}

+ 111 - 0
package.json

@@ -0,0 +1,111 @@
+{
+  "name": "ant-design-pro",
+  "version": "0.2.3-rc.3",
+  "description": "An out-of-box UI solution for enterprise applications",
+  "private": true,
+  "scripts": {
+    "precommit": "npm run lint-staged",
+    "start": "roadhog server",
+    "start:no-proxy": "cross-env NO_PROXY=true roadhog server",
+    "build": "roadhog build",
+    "site": "roadhog-api-doc static && gh-pages -d dist",
+    "analyze": "roadhog build --analyze",
+    "lint:style": "stylelint \"src/**/*.less\" --syntax less",
+    "lint": "eslint --ext .js src mock tests && npm run lint:style",
+    "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",
+    "lint-staged": "lint-staged",
+    "lint-staged:js": "eslint --ext .js",
+    "test": "jest",
+    "test:all": "node ./tests/run-tests.js"
+  },
+  "dependencies": {
+    "antd": "^3.0.0-beta.1",
+    "babel-runtime": "^6.9.2",
+    "classnames": "^2.2.5",
+    "core-js": "^2.5.1",
+    "dva": "^2.1.0",
+    "g-cloud": "^1.0.2-beta",
+    "g2": "^2.3.13",
+    "g2-plugin-slider": "^1.2.1",
+    "lodash": "^4.17.4",
+    "lodash-decorators": "^4.4.1",
+    "lodash.clonedeep": "^4.5.0",
+    "moment": "^2.19.1",
+    "numeral": "^2.0.6",
+    "path-to-regexp": "^2.1.0",
+    "prop-types": "^15.5.10",
+    "qs": "^6.5.0",
+    "react": "^16.0.0",
+    "react-container-query": "^0.9.1",
+    "react-document-title": "^2.0.3",
+    "react-dom": "^16.0.0",
+    "react-fittext": "^1.0.0"
+  },
+  "devDependencies": {
+    "babel-eslint": "^8.0.1",
+    "babel-jest": "^21.0.0",
+    "babel-plugin-dva-hmr": "^0.3.2",
+    "babel-plugin-import": "^1.2.1",
+    "babel-plugin-transform-class-properties": "^6.24.1",
+    "babel-plugin-transform-decorators-legacy": "^1.3.4",
+    "babel-plugin-transform-runtime": "^6.9.0",
+    "babel-preset-env": "^1.6.1",
+    "babel-preset-react": "^6.24.1",
+    "cross-env": "^5.1.1",
+    "cross-port-killer": "^1.0.1",
+    "enzyme": "^3.1.0",
+    "enzyme-adapter-react-16": "^1.0.2",
+    "eslint": "^4.8.0",
+    "eslint-config-airbnb": "^16.0.0",
+    "eslint-plugin-babel": "^4.0.0",
+    "eslint-plugin-import": "^2.2.0",
+    "eslint-plugin-jsx-a11y": "^6.0.0",
+    "eslint-plugin-markdown": "^1.0.0-beta.6",
+    "eslint-plugin-react": "^7.0.1",
+    "gh-pages": "^1.0.0",
+    "husky": "^0.14.3",
+    "jest": "^21.0.1",
+    "lint-staged": "^4.3.0",
+    "mockjs": "^1.0.1-beta3",
+    "pro-download": "^1.0.0",
+    "react-test-renderer": "^16.0.0",
+    "redbox-react": "^1.3.2",
+    "roadhog": "^1.3.1",
+    "roadhog-api-doc": "^0.2.5",
+    "stylelint": "^8.1.0",
+    "stylelint-config-standard": "^17.0.0"
+  },
+  "optionalDependencies": {
+    "nightmare": "^2.10.0"
+  },
+  "babel": {
+    "presets": [
+      "env",
+      "react"
+    ],
+    "plugins": [
+      "transform-decorators-legacy",
+      "transform-class-properties"
+    ]
+  },
+  "jest": {
+    "setupFiles": [
+      "<rootDir>/tests/setupTests.js"
+    ],
+    "testMatch": [
+      "**/?(*.)(spec|test|e2e).js?(x)"
+    ],
+    "setupTestFrameworkScriptFile": "<rootDir>/tests/jasmine.js",
+    "moduleFileExtensions": [
+      "js",
+      "jsx"
+    ],
+    "moduleNameMapper": {
+      "\\.(css|less)$": "<rootDir>/tests/styleMock.js"
+    }
+  },
+  "lint-staged": {
+    "**/*.{js,jsx}": "lint-staged:js",
+    "**/*.less": "stylelint --syntax less"
+  }
+}

+ 16 - 0
public/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>义方管理平台</title>
+  <link rel="icon" href="https://gw.alipayobjects.com/zos/rmsportal/IOtlElCiWVIOZqgDslYd.png" type="image/x-icon">
+  <link rel="stylesheet" href="/index.css" />
+</head>
+<body>
+  <div id="root"></div>
+  <script src="https://gw.alipayobjects.com/as/g/??datavis/g2/2.3.12/index.js,datavis/g-cloud/1.0.2/index.js,datavis/g2-plugin-slider/1.2.1/slider.js"></script>
+  <script src="/index.js"></script>
+</body>
+</html>

BIN
src/assets/logo-tb.png


BIN
src/assets/yay.jpg


+ 228 - 0
src/common/ljNav.js

@@ -0,0 +1,228 @@
+import BasicLayout from '../layouts/BasicLayout';
+
+import Exception403 from '../routes/Exception/403';
+import Exception404 from '../routes/Exception/404';
+import Exception500 from '../routes/Exception/500';
+
+import ImageList from '../routes/Resource/ImageList';
+import VideoList from '../routes/Resource/VideoList';
+
+import TagList from '../routes/Product/TagList';
+import TagSave from '../routes/Product/TagSave';
+
+import WareList from '../routes/Product/WareList';
+import WareSave from '../routes/Product/WareSave';
+
+import LessonList from '../routes/Product/LessonList';
+import LessonSave from '../routes/Product/LessonSave';
+
+import CourseList from '../routes/Product/CourseList';
+import CourseSave from '../routes/Product/CourseSave';
+
+import SupportList from '../routes/Product/SupportList';
+import SupportSave from '../routes/Product/SupportSave';
+
+import ItemList from '../routes/Goods/ItemList';
+import ItemSave from '../routes/Goods/ItemSave';
+
+const data = [
+  {
+    component: BasicLayout,
+    layout: 'BasicLayout',
+    name: '首页', // for breadcrumb
+    path: '/',
+    children: [
+      // 一级菜单:销售概览模块,
+      // filter为true为不在侧边菜单显示
+      {
+        name: '销售概览',
+        icon: 'dashboard',
+        path: 'sales',
+        children: [{
+          name: '概览',
+          path: 'view',
+          component: Exception404,
+        },{
+          name: '销售详情',
+          path: 'detail',
+          component: Exception404,
+        }],
+      },
+      // 一级菜单:资源管理模块
+      {
+        name: '资源管理',
+        icon: 'folder',
+        path: 'resource',
+        children: [{
+          name: '图库管理',
+          path: 'image',
+          component: ImageList,
+        },{
+          name: '视频管理',
+          path: 'video',
+          component: VideoList,
+        }],
+      },
+      // 一级菜单:课程及周边配套管理
+      {
+        name: '产品管理',
+        icon: 'appstore',
+        path: 'product',
+        children: [{
+          name: '标签管理',
+          path: 'tag',
+          component: TagList,
+          filter: true,
+          children: [{
+            name: '添加',
+            path: 'add',
+            component: TagSave,
+          },{
+            name: '编辑',
+            path: 'edit',
+            component: TagSave,
+          }],
+        },{
+          name: '课件管理',
+          path: 'ware',
+          component: WareList,
+          filter: true,
+          children: [{
+            name: '添加',
+            path: 'add',
+            component: WareSave,
+          },{
+            name: '编辑',
+            path: 'edit',
+            component: WareSave,
+          }],
+        },{
+          name: '课管理',
+          path: 'lesson',
+          component: LessonList,
+          filter: true,
+          children: [{
+            name: '添加',
+            path: 'add',
+            component: LessonSave,
+          },{
+            name: '编辑',
+            path: 'edit',
+            component: LessonSave,
+          }],
+        },{
+          name: '课程管理',
+          path: 'course',
+          component: CourseList,
+          filter: true,
+          children: [{
+            name: '添加',
+            path: 'add',
+            component: CourseSave,
+          },{
+            name: '编辑',
+            path: 'edit',
+            component: CourseSave,
+          }],
+        },{
+          name: '周边配套管理',
+          path: 'support',
+          component: SupportList,
+          filter: true,
+          children: [{
+            name: '添加',
+            path: 'add',
+            component: SupportSave,
+          },{
+            name: '编辑',
+            path: 'edit',
+            component: SupportSave,
+          }]
+        }],
+      },
+      // 一级菜单:产品管理模块
+      {
+        name: '商品管理',
+        icon: 'shopping-cart',
+        path: 'goods',
+        children: [{
+          name: '商品管理',
+          path: 'item',
+          component: ItemList,
+          filter: true,
+          children: [{
+            name: '添加',
+            path: 'add',
+            component: ItemSave,
+          },{
+            name: '编辑',
+            path: 'edit',
+            component: ItemSave,
+          }],
+        },{
+          name: '商品包管理',
+          path: 'combo',
+          component: Exception404,
+        }],
+      },
+      // 一级菜单:厂商管理
+      {
+        name: '厂商管理',
+        icon: 'team',
+        path: 'merchant',
+        children: [{
+          name: '供应商管理',
+          path: 'cp',
+          component: Exception404,
+        },{
+          name: '渠道方管理',
+          path: 'project',
+          component: Exception404,
+        }],
+      },
+      // 一级菜单:订单管理模块
+      {
+        name: '交易管理',
+        icon: 'trademark',
+        path: 'trade',
+        children: [{
+          name: '订单管理',
+          path: 'order',
+          component: Exception404,
+        }],
+      },
+      // 一级菜单:终端管理模块
+      {
+        name: '终端管理',
+        icon: 'desktop',
+        path: 'terminal',
+        children: [{
+          name: '终端用户',
+          path: 'user',
+          component: Exception404,
+        },{
+          name: '校区管理',
+          path: 'campus',
+          component: Exception404,
+        }],
+      },
+      // 一级菜单:行为统计模块
+      {
+        name: '行为统计',
+        icon: 'scan',
+        path: 'action',
+        children: [{
+          name: '使用记录',
+          path: 'usage',
+          component: Exception404,
+        }],
+      }
+    ],
+  },
+];
+
+export function getNavData() {
+  return data;
+}
+
+export default data;

+ 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);
+````

+ 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;
+  }
+}

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

@@ -0,0 +1,37 @@
+---
+title: 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`        | string  | - |
+
+#### 响应式规则
+
+| 窗口宽度             | 展示列数                                      | 
+|---------------------|---------------------------------------------|
+| `≥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 },
+};

+ 202 - 0
src/components/EditableTable/index.js

@@ -0,0 +1,202 @@
+import PropTypes from 'prop-types';
+import { cloneDeep } from 'lodash';
+import React, { PureComponent } from 'react';
+import { Form, Table, Input, InputNumber, Button, Select, Switch, Popconfirm } from 'antd';
+import styles from './index.less';
+import config from '../../utils/config';
+
+const FormItem = Form.Item;
+const InputGroup = Input.Group;
+const ButtonGroup = Button.Group;
+const { Option } = Select;
+
+const EditableCell = ({ editable, value, onChange }) => (
+  <div>
+    {editable
+      ? <Input style={{ margin: '-5px 0' }} value={value} onChange={e => onChange(e.target.value)} />
+      : value
+    }
+  </div>
+);
+
+
+class EditableTable extends PureComponent {
+  static defaultProps = {
+    dataSource: [],  //表格的数据源
+    merchants: [],   //渠道方信息
+    rowKeyName: '',  //表格行唯一key
+  };
+  static propTypes = {
+    dataSource: PropTypes.array,
+    rowKeyName: PropTypes.string,
+    merchants: PropTypes.array,
+  };
+  state = {
+    data: [],
+    cacheData: [],   //缓存数据
+  };
+
+  componentWillReceiveProps(nextProps) {
+    const { dataSource } = nextProps;
+    const cacheData = dataSource.map(item => ({...item}));
+    this.setState({
+      data: dataSource,
+      cacheData: cacheData,
+    });
+  }
+
+  renderColumns(text, record, column) {
+    return (
+      <EditableCell
+        editable={record.editable}
+        value={text}
+        onChange={value => this.handleChange(value, record.key, column)}
+      />
+    );
+  }
+
+  handleChange = (value, key, column) => {
+    const newData = [...this.state.data];
+    const target = newData.filter(item => key === item.key)[0];
+    if (target) {
+      target[column] = value;
+      this.setState({ data: newData });
+    }
+  }
+
+  handleSave = (key) => {
+    const newData = [...this.state.data];
+    const target = newData.filter(item => key === item.key)[0];
+    if (target) {
+      delete target.editable;
+      this.setState({
+        data: newData,
+        cacheData: newData.map(item => ({...item})),
+      });
+    }
+  }
+
+  handleCancel = (key) => {
+    const newData = [...this.state.data];
+    const target = newData.filter(item => key === item.key)[0];
+    if (target) {
+      Object.assign(target, this.state.cacheData.filter(item => key === item.key)[0]);
+      delete target.editable;
+      this.setState({
+        data: newData,
+      });
+    }
+  }
+
+  handleEdit = (key) => {
+    const newData = [...this.state.data];
+    const target = newData.filter(item => key === item.key)[0];
+    if (target) {
+      target.editable = true;
+      this.setState({
+        data: newData,
+      })
+    }
+  }
+
+  render() {
+    const { data } = this.state;
+    const { rowKeyName, merchants } = this.props;
+    const columns = [{
+      title: '渠道方平台',
+      dataIndex: 'merchantId',
+      width: '15%',
+      render: (text, record) => (
+        <Select
+          style={{ width: '100%' }}
+          disabled={!record.editable}
+          defaultValue={record.merchantId}
+        >
+          {
+            merchants.map(item =>
+              <Option key={item.merchantId} value={item.merchantId}>{item.merchantName}</Option>
+            )
+          }
+        </Select>
+      ),
+    },{
+      title: '价格类型',
+      dataIndex: 'name',
+      width: '15%',
+      render: (text, record) => (
+        <Select
+          style={{ width: '100%' }}
+          disabled={!record.editable}
+        >
+          {
+            config.PRICE_TYPE.map(item =>
+              <Option key={item.value} value={item.value}>{item.name}</Option>
+            )
+          }
+        </Select>
+      ),
+    },{
+      title: '供应商价格',
+      dataIndex: 'cpPrice',
+      width: '15%',
+      render: (text, record) => this.renderColumns(text, record, 'cpPrice'),
+    },{
+      title: '渠道方价格',
+      dataIndex: 'merchantPrice',
+      width: '15%',
+      render: (text, record) => this.renderColumns(text, record, 'merchantPrice'),
+    },{
+      title: '终端用户价格',
+      dataIndex: 'terminalPrice',
+      width: '15%',
+      render: (text, record) => this.renderColumns(text, record, 'terminalPrice'),
+    },{
+      title: '上架状态',
+      dataIndex: 'status',
+      width: '15%',
+      render: (text, record) => (
+        <Switch disabled={!record.editable} checkedChildren="上架" unCheckedChildren="下架" />
+      ),
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      width: '20%',
+      render: (text, record) => {
+        const { editable } = record;
+        return (
+          <div className={styles.editableRowOperations}>
+            {editable ?
+              <span>
+                <a onClick={() => this.handleSave(record.key)}>保存</a>
+                <Popconfirm title="确定取消吗?" onConfirm={() => this.handleCancel(record.key)}>
+                  <a>取消</a>
+                </Popconfirm>
+              </span>
+              :
+              <span>
+                <a onClick={() => this.handleEdit(record.key)}>编辑</a>
+                <Popconfirm title="确定删除吗?" onConfirm={() => this.handleCancel(record.key)}>
+                  <a>删除</a>
+                </Popconfirm>
+              </span>
+            }
+          </div>
+        );
+      }
+    }];
+
+    return (
+      <div>
+        <Button type="primary" className={styles.editableAddBtn}>添加</Button>
+        <Table
+          bordered
+          columns={columns}
+          dataSource={data}
+          rowKey={rowKeyName}
+        />
+      </div>
+    );
+  }
+}
+
+export default EditableTable;

+ 7 - 0
src/components/EditableTable/index.less

@@ -0,0 +1,7 @@
+.editableRowOperations a {
+  margin-right: 8px;
+}
+
+.editableAddBtn {
+  margin-bottom: 8px;
+}

+ 21 - 0
src/components/Exception/demo/403.md

@@ -0,0 +1,21 @@
+---
+order: 2
+title: 403
+---
+
+403 页面,配合自定义操作。
+
+````jsx
+import Exception from 'ant-design-pro/lib/Exception';
+import { Button } from 'antd';
+
+const actions = (
+  <div>
+    <Button type="primary">回到首页</Button>
+    <Button>查看详情</Button>
+  </div>
+);
+ReactDOM.render(
+  <Exception type="403" actions={actions} />
+, mountNode);
+````

+ 14 - 0
src/components/Exception/demo/404.md

@@ -0,0 +1,14 @@
+---
+order: 0
+title: 404
+---
+
+404 页面。
+
+````jsx
+import Exception from 'ant-design-pro/lib/Exception';
+
+ReactDOM.render(
+  <Exception type="404" />
+, mountNode);
+````

+ 14 - 0
src/components/Exception/demo/500.md

@@ -0,0 +1,14 @@
+---
+order: 1
+title: 500
+---
+
+500 页面。
+
+````jsx
+import Exception from 'ant-design-pro/lib/Exception';
+
+ReactDOM.render(
+  <Exception type="500" />
+, mountNode);
+````

+ 33 - 0
src/components/Exception/index.js

@@ -0,0 +1,33 @@
+import React, { createElement } from 'react';
+import classNames from 'classnames';
+import { Button } from 'antd';
+import config from './typeConfig';
+import styles from './index.less';
+
+export default ({ className, linkElement = 'a', type, title, desc, img, actions, ...rest }) => {
+  const pageType = type in config ? type : '404';
+  const clsString = classNames(styles.exception, className);
+  return (
+    <div className={clsString} {...rest}>
+      <div className={styles.imgBlock}>
+        <div
+          className={styles.imgEle}
+          style={{ backgroundImage: `url(${img || config[pageType].img})` }}
+        />
+      </div>
+      <div className={styles.content}>
+        <h1>{title || config[pageType].title}</h1>
+        <div className={styles.desc}>{desc || config[pageType].desc}</div>
+        <div className={styles.actions}>
+          {
+            actions ||
+              createElement(linkElement, {
+                to: '/',
+                href: '/',
+              }, <Button type="primary">返回首页</Button>)
+          }
+        </div>
+      </div>
+    </div>
+  );
+};

+ 78 - 0
src/components/Exception/index.less

@@ -0,0 +1,78 @@
+@import "~antd/lib/style/themes/default.less";
+@import "~antd/lib/style/mixins/clearfix.less";
+
+.exception {
+  display: flex;
+  align-items: center;
+  height: 100%;
+
+  .imgBlock {
+    flex: 0 0 62.5%;
+    width: 62.5%;
+    padding-right: 152px;
+    .clearfix();
+  }
+
+  .imgEle {
+    height: 360px;
+    width: 100%;
+    max-width: 430px;
+    float: right;
+    background-repeat: no-repeat;
+    background-position: 50% 50%;
+    background-size: 100% 100%;
+  }
+
+  .content {
+    flex: auto;
+
+    h1 {
+      color: #434e59;
+      font-size: 72px;
+      font-weight: 600;
+      line-height: 72px;
+      margin-bottom: 24px;
+    }
+
+    .desc {
+      color: @text-color-secondary;
+      font-size: 20px;
+      line-height: 28px;
+      margin-bottom: 16px;
+    }
+
+    .actions {
+      button:not(:last-child) {
+        margin-right: 8px;
+      }
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xl) {
+  .exception {
+    .imgBlock {
+      padding-right: 88px;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-sm) {
+  .exception {
+    display: block;
+    text-align: center;
+    .imgBlock {
+      padding-right: 0;
+      margin: 0 auto 24px;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .exception {
+    .imgBlock {
+      margin-bottom: -24px;
+      overflow: hidden;
+    }
+  }
+}

+ 19 - 0
src/components/Exception/index.md

@@ -0,0 +1,19 @@
+---
+title: Exception
+subtitle: 异常
+cols: 1
+order: 5
+---
+
+异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。
+
+## API
+
+| 参数         | 说明                                      | 类型         | 默认值 |
+|-------------|------------------------------------------|-------------|-------|
+| type        | 页面类型,若配置,则自带对应类型默认的 `title`,`desc`,`img`,此默认设置可以被 `title`,`desc`,`img` 覆盖 | Enum {'403', '404', '500'} | - |
+| title       | 标题     | ReactNode  | -    |
+| desc        | 补充描述    | ReactNode  | -    |
+| img         | 背景图片地址     | string  | -    |
+| actions     | 建议操作,配置此属性时默认的『返回首页』按钮不生效    | ReactNode  | -    |
+| linkElement | 定义链接的元素,默认为 `a` | string\|ReactElement | - |

+ 19 - 0
src/components/Exception/typeConfig.js

@@ -0,0 +1,19 @@
+const config = {
+  403: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
+    title: '403',
+    desc: '抱歉,你无权访问该页面',
+  },
+  404: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
+    title: '404',
+    desc: '抱歉,你访问的页面不存在',
+  },
+  500: {
+    img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
+    title: '500',
+    desc: '抱歉,服务器出错了',
+  },
+};
+
+export default config;

+ 33 - 0
src/components/GlobalFooter/demo/basic.md

@@ -0,0 +1,33 @@
+---
+order: 0
+title: 演示
+iframe: 400
+---
+
+基本页脚。
+
+````jsx
+import GlobalFooter from 'ant-design-pro/lib/GlobalFooter';
+import { Icon } from 'antd';
+
+const links = [{
+  title: '帮助',
+  href: '',
+}, {
+  title: '隐私',
+  href: '',
+}, {
+  title: '条款',
+  href: '',
+  blankTarget: true,
+}];
+
+const copyright = <div>Copyright <Icon type="copyright" /> 2017 蚂蚁金服体验技术部出品</div>;
+
+ReactDOM.render(
+  <div style={{ background: '#f5f5f5', overflow: 'hidden' }}>
+    <div style={{ height: 280 }} />
+    <GlobalFooter links={links} copyright={copyright} />
+  </div>
+, mountNode);
+````

+ 27 - 0
src/components/GlobalFooter/index.js

@@ -0,0 +1,27 @@
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export default ({ className, links, copyright }) => {
+  const clsString = classNames(styles.globalFooter, className);
+  return (
+    <div className={clsString}>
+      {
+        links && (
+          <div className={styles.links}>
+            {links.map(link => (
+              <a
+                key={link.title}
+                target={link.blankTarget ? '_blank' : '_self'}
+                href={link.href}
+              >
+                {link.title}
+              </a>
+            ))}
+          </div>
+        )
+      }
+      {copyright && <div className={styles.copyright}>{copyright}</div>}
+    </div>
+  );
+};

+ 29 - 0
src/components/GlobalFooter/index.less

@@ -0,0 +1,29 @@
+@import "~antd/lib/style/themes/default.less";
+
+.globalFooter {
+  padding: 0 16px;
+  margin: 16px 0 16px 0;
+  text-align: center;
+
+  .links {
+    margin-bottom: 8px;
+
+    a {
+      color: @text-color-secondary;
+      transition: all .3s;
+
+      &:not(:last-child) {
+        margin-right: 40px;
+      }
+
+      &:hover {
+        color: @text-color;
+      }
+    }
+  }
+
+  .copyright {
+    color: @text-color-secondary;
+    font-size: @font-size-base;
+  }
+}

+ 15 - 0
src/components/GlobalFooter/index.md

@@ -0,0 +1,15 @@
+---
+title: GlobalFooter
+subtitle: 全局页脚
+cols: 1
+order: 7
+---
+
+页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
+
+## API
+
+参数 | 说明 | 类型 | 默认值
+----|------|-----|------
+links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | -
+copyright | 版权信息 | ReactNode | -

+ 255 - 0
src/components/ModalSelectTable/index.js

@@ -0,0 +1,255 @@
+import PropTypes from 'prop-types';
+import { cloneDeep } from 'lodash';
+import React, { PureComponent } from 'react';
+import { Form, Table, Modal, Input, Button, Row, Col, Select } from 'antd';
+import styles from './index.less';
+
+const FormItem = Form.Item;
+const InputGroup = Input.Group;
+const ButtonGroup = Button.Group;
+const { Option } = Select;
+
+class ModalSelectTable extends PureComponent {
+  static defaultProps = {
+    modalTitle: 'SelectTable', //模态弹框标题
+    dataSource: [],            //待选列表数据源
+    selectedDataSource: [],    //已选择数据源
+    baseColumnSet: [],         //表格通用列配置
+    selectMode: 'multiple',    //选择模式(单选: single, 多选: multiple)
+    selectSort: false,         //已选列表是否支持排序
+    children: {},              //动态加载的子组件(这里指头部的搜索组件)
+    rowKeyName: 'id',          //数据表行的唯一key取自哪个字段
+    pagination: {},            //表格的分页配置
+    loading: false,            //待选列表数据加载状态
+    onChange: () => {},        //翻页回调函数
+    onSubmit: () => {},        //点击确定按钮时回调函数
+  };
+  static propTypes = {
+    dataSource: PropTypes.array,
+    selectedDataSource: PropTypes.array,
+    modalTitle: PropTypes.string,
+    baseColumnSet: PropTypes.array,
+    selectMode: PropTypes.string,
+    selectSort: PropTypes.bool,
+    children: PropTypes.object,
+    rowKeyName: PropTypes.string,
+    pagination: PropTypes.object,
+    loading: PropTypes.bool,
+    onChange: PropTypes.func,
+    onSubmit: PropTypes.func,
+  };
+  state = {
+    modalShow: false,            //模态框的显示与隐藏
+    tableTabLeft: true,          //待选已选tab切换
+    selectedDataStorage: [],     //已选择数据容器
+  };
+
+  //显示模态选择组件,由父组件通过refs调用触发
+  showModal = () => {
+    const { selectedDataSource } = this.props;
+    const { selectedDataStorage } = this.state;
+    let tmpArr = [];
+    selectedDataSource.map(item => tmpArr.push(item));
+    this.setState({
+      modalShow: true,
+      selectedDataStorage: tmpArr,
+    });
+  }
+
+  //模态框关闭时,状态重置
+  closeModal = () => {
+    this.setState({
+      modalShow: false,
+      tableTabLeft: true,
+      selectedDataStorage: [],
+    });
+  }
+
+  //点击提交按钮时触发
+  handleModalOk = () => {
+    const { selectMode } = this.props;
+    const { selectedDataStorage } = this.state;
+    this.props.onSubmit(selectedDataStorage);
+    this.closeModal();
+  }
+
+  //点击取消或关闭按钮时触发
+  handleModalCancel = () => {
+    this.closeModal();
+  }
+
+  //多选模式,切换待选已选tab逻辑
+  handleTableTabChange = () => {
+    const { tableTabLeft } = this.state;
+    this.setState({
+      tableTabLeft: !tableTabLeft,
+    });
+  }
+
+  //待选数据表的页码改变时触发onChange回调函数
+  handleModalTableChange = (pagination) => {
+    this.props.onChange(pagination);
+  }
+
+  handleToBeSelectedRowAdd = (record) => {
+    const { selectMode } = this.props;
+    const { selectedDataStorage } = this.state;
+    const tmp = cloneDeep(selectedDataStorage);
+
+    if (selectMode == 'multiple') {
+      tmp.push(record);
+      this.setState({
+        selectedDataStorage: tmp,
+      });
+    }
+    else if (selectMode == 'single') {
+      tmp[0] = record;
+      this.setState({
+        selectedDataStorage: tmp,
+      }, () => this.handleModalOk());
+    }
+    else {
+      console.log('模式选择错误');
+    }
+  }
+
+  handleSelectedRowDel = (record) => {
+    const { selectedDataStorage } = this.state;
+    const { rowKeyName } = this.props;
+    const list = selectedDataStorage.filter(item => item[rowKeyName] !== record[rowKeyName]);
+    this.setState({
+      selectedDataStorage: list,
+    });
+  }
+
+  handleSelectedRowSort = (record, sort_up) => {
+    const { selectedDataStorage } = this.state;
+    const { rowKeyName } = this.props;
+
+    const arr = cloneDeep(selectedDataStorage);
+
+    const index = arr.findIndex(item => item[rowKeyName] == record[rowKeyName]);
+
+    if (sort_up)
+    {
+      //第一个元素或者未找到元素不做操作
+      if (!index || -1 === index) return;
+      //与前一个元素进行位置互换
+      arr.splice(index, 1, ...arr.splice(index - 1, 1, arr[index]));
+    }
+    else
+    {
+      //最后一个元素或者未找到元素不做操作
+      if (index + 1 === arr.length || -1 === index) return;
+      //与后一个元素进行位置互换
+      arr.splice(index, 1, ...arr.splice(index + 1, 1, arr[index]));
+    }
+
+    this.setState({
+      selectedDataStorage: arr,
+    });
+  }
+
+  render() {
+    const {
+      dataSource, baseColumnSet, modalTitle, children, loading,
+      selectMode, selectSort, rowKeyName, pagination,
+    } = this.props;
+    const { modalShow, tableTabLeft, selectedDataStorage } = this.state;
+
+    //给已选列表增加排序序号字段
+    selectedDataStorage.map((item, index) => item['sortId'] = index + 1);
+
+    const sortNumberColumn = {
+      title: '序号',
+      dataIndex: 'sortId',
+      width: 50,
+      fixed: 'left',
+    };
+    const addColumn = {
+      title: '添加',
+      render: (text, record) => (
+        <Button
+          disabled={selectedDataStorage.filter(item => item[rowKeyName] == record[rowKeyName]).length}
+          size="small"
+          type="dashed"
+          icon="plus"
+          onClick={() => this.handleToBeSelectedRowAdd(record)}
+        >
+        </Button>
+      ),
+      width: 50,
+    };
+    const sortColumn = {
+      title: '排序',
+      render: (text, record) => (
+        <ButtonGroup>
+          <Button size="small" icon="arrow-up" onClick={() => this.handleSelectedRowSort(record, true)}></Button>
+          <Button size="small" icon="arrow-down" onClick={() => this.handleSelectedRowSort(record, false)}></Button>
+        </ButtonGroup>
+      ),
+      width: 80,
+    };
+    const delColumn = {
+      title: '删除',
+      render: (text, record) => (
+        <Button size="small" type="danger" icon="delete" onClick={() => this.handleSelectedRowDel(record)}></Button>
+      ),
+      width: 50,
+    };
+
+    const toBeSelectedColumns = [];
+    const selectedColumns = [];
+    baseColumnSet.map(item => {
+      toBeSelectedColumns.push(item);
+      selectedColumns.push(item);
+    });
+
+    toBeSelectedColumns.push(addColumn);
+
+    if (selectSort) {
+      selectedColumns.unshift(sortNumberColumn);
+    }
+
+    selectedColumns.push(sortColumn);
+    selectedColumns.push(delColumn);
+
+    return (
+      <Modal
+        title={modalTitle}
+        visible={modalShow}
+        maskClosable={false}
+        onOk={this.handleModalOk}
+        onCancel={this.handleModalCancel}
+      >
+        <div className={styles.search}>{children}</div>
+        <div className={styles.tableTab} style={selectMode == 'multiple' ? null : { display: 'none' }}>
+          <ButtonGroup>
+            <Button onClick={this.handleTableTabChange} type="primary" ghost={tableTabLeft ? false : true}>
+              待选
+            </Button>
+            <Button onClick={this.handleTableTabChange} type="primary" ghost={tableTabLeft ? true : false}>
+              {`已选[${selectedDataStorage.length}]`}
+            </Button>
+          </ButtonGroup>
+        </div>
+        <div className={styles.table}>
+          <Table
+            size="middle"
+            bordered={true}
+            columns={tableTabLeft ? toBeSelectedColumns : selectedColumns}
+            dataSource={tableTabLeft ? dataSource : selectedDataStorage}
+            rowKey={rowKeyName}
+            pagination={tableTabLeft ? pagination : false}
+            loading={loading}
+            onChange={this.handleModalTableChange}
+            scroll={{ y: 200 }}
+          >
+          </Table>
+        </div>
+      </Modal>
+    );
+  }
+}
+
+export default ModalSelectTable;

+ 16 - 0
src/components/ModalSelectTable/index.less

@@ -0,0 +1,16 @@
+@import "~antd/lib/style/themes/default.less";
+
+.search {
+  width: 100%;
+  padding-bottom: 10px;
+  margin-top: 0;
+}
+
+.tableTab {
+  width: 100%;
+  padding-bottom: 10px;
+}
+
+.table {
+
+}

+ 137 - 0
src/components/PageHeader/index.js

@@ -0,0 +1,137 @@
+import React, { PureComponent, createElement } from 'react';
+import PropTypes from 'prop-types';
+import { Breadcrumb, Tabs } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+import Logger from '../../utils/logger';
+
+const { TabPane } = Tabs;
+
+const logger = Logger.getLogger('BreadcrumbComponent');
+
+export default class PageHeader extends PureComponent {
+  static contextTypes = {
+    routes: PropTypes.array,
+    params: PropTypes.object,
+    location: PropTypes.object,
+    breadcrumbNameMap: PropTypes.object,
+  };
+  onChange = (key) => {
+    if (this.props.onTabChange) {
+      this.props.onTabChange(key);
+    }
+  };
+  getBreadcrumbProps = () => {
+    return {
+      routes: this.props.routes || this.context.routes,
+      params: this.props.params || this.context.params,
+      location: this.props.location || this.context.location,
+      breadcrumbNameMap: this.props.breadcrumbNameMap || this.context.breadcrumbNameMap,
+    };
+  };
+  itemRender = (route, params, routes, paths) => {
+    const { linkElement = 'a' } = this.props;
+    const last = routes.indexOf(route) === routes.length - 1;
+    return (last || !route.component)
+      ? <span>{route.breadcrumbName}</span>
+      : createElement(linkElement, {
+        href: paths.join('/') || '/',
+        to: paths.join('/') || '/',
+      }, route.breadcrumbName);
+  }
+  render() {
+    const { routes, params, location, breadcrumbNameMap } = this.getBreadcrumbProps();
+    const {
+      title, logo, action, content, extraContent,
+      breadcrumbList, tabList, className, linkElement = 'a',
+    } = this.props;
+    const clsString = classNames(styles.pageHeader, className);
+    let breadcrumb;
+    if (routes && params) {
+      breadcrumb = (
+        <Breadcrumb
+          className={styles.breadcrumb}
+          routes={routes.filter(route => route.breadcrumbName)}
+          params={params}
+          itemRender={this.itemRender}
+        />
+      );
+    } else if (location && location.pathname) {
+      const pathSnippets = location.pathname.split('/').filter(i => i);
+      const extraBreadcrumbItems = pathSnippets.map((_, index) => {
+        const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
+        return (
+          <Breadcrumb.Item key={url}>
+            {createElement(index === pathSnippets.length - 1 ? 'span' : linkElement, {
+              [linkElement === 'a' ? 'href' : 'to']: url,
+            }, breadcrumbNameMap[url] || breadcrumbNameMap[url.replace('/', '')] || url)}
+          </Breadcrumb.Item>
+        );
+      });
+      const breadcrumbItems = [(
+        <Breadcrumb.Item key="home">
+          {createElement(linkElement, {
+            [linkElement === 'a' ? 'href' : 'to']: '/',
+          }, '首页')}
+        </Breadcrumb.Item>
+      )].concat(extraBreadcrumbItems);
+      breadcrumb = (
+        <Breadcrumb className={styles.breadcrumb}>
+          {breadcrumbItems}
+        </Breadcrumb>
+      );
+    } else if (breadcrumbList && breadcrumbList.length) {
+      breadcrumb = (
+        <Breadcrumb className={styles.breadcrumb}>
+          {
+            breadcrumbList.map(item => (
+              <Breadcrumb.Item key={item.title}>
+                {item.href ? (
+                  createElement(linkElement, {
+                    [linkElement === 'a' ? 'href' : 'to']: item.href,
+                  }, '首页')
+                ) : item.title}
+              </Breadcrumb.Item>)
+            )
+          }
+        </Breadcrumb>
+      );
+    } else {
+      breadcrumb = null;
+    }
+
+    const tabDefaultValue = tabList && (tabList.filter(item => item.default)[0] || tabList[0]);
+
+    return (
+      <div className={clsString}>
+        {breadcrumb}
+        <div className={styles.detail}>
+          {logo && <div className={styles.logo}>{logo}</div>}
+          <div className={styles.main}>
+            <div className={styles.row}>
+              {title && <h1 className={styles.title}>{title}</h1>}
+              {action && <div className={styles.action}>{action}</div>}
+            </div>
+            <div className={styles.row}>
+              {content && <div className={styles.content}>{content}</div>}
+              {extraContent && <div className={styles.extraContent}>{extraContent}</div>}
+            </div>
+          </div>
+        </div>
+        {
+          tabList &&
+          tabList.length &&
+          <Tabs
+            className={styles.tabs}
+            defaultActiveKey={(tabDefaultValue && tabDefaultValue.key)}
+            onChange={this.onChange}
+          >
+            {
+              tabList.map(item => <TabPane tab={item.tab} key={item.key} />)
+            }
+          </Tabs>
+        }
+      </div>
+    );
+  }
+}

+ 138 - 0
src/components/PageHeader/index.less

@@ -0,0 +1,138 @@
+@import "~antd/lib/style/themes/default.less";
+
+.pageHeader {
+  background: @component-background;
+  padding: 16px 32px 0 32px;
+  border-bottom: @border-width-base @border-style-base @border-color-split;
+
+  .detail {
+    display: flex;
+  }
+
+  .row {
+    display: flex;
+  }
+
+  .breadcrumb {
+    margin-bottom: 16px;
+  }
+
+  .tabs {
+    margin: 0 0 -17px -8px;
+
+    :global {
+      .ant-tabs-bar {
+        border-bottom: @border-width-base @border-style-base @border-color-split;
+      }
+    }
+  }
+
+  .logo {
+    flex: 0 1 auto;
+    margin-right: 16px;
+    padding-top: 1px;
+    > img {
+      width: 28px;
+      height: 28px;
+      border-radius: @border-radius-base;
+      display: block;
+    }
+  }
+
+  .title {
+    font-size: 20px;
+    font-weight: 500;
+    color: @heading-color;
+  }
+
+  .action {
+    margin-left: 56px;
+    min-width: 266px;
+
+    :global {
+      .ant-btn-group:not(:last-child),
+      .ant-btn:not(:last-child) {
+        margin-right: 8px;
+      }
+
+      .ant-btn-group > .ant-btn {
+        margin-right: 0;
+      }
+    }
+  }
+
+  .title, .action, .content, .extraContent, .main {
+    flex: auto;
+  }
+
+  .title, .action {
+    margin-bottom: 16px;
+  }
+
+  .logo, .content, .extraContent {
+    margin-bottom: 16px;
+  }
+
+  .action, .extraContent {
+    text-align: right;
+  }
+
+  .extraContent {
+    margin-left: 88px;
+    min-width: 242px;
+  }
+}
+
+@media screen and (max-width: @screen-xl) {
+  .pageHeader {
+    .extraContent {
+      margin-left: 44px;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-lg) {
+  .pageHeader {
+    .extraContent {
+      margin-left: 20px;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-md) {
+  .pageHeader {
+    .row {
+      display: block;
+    }
+
+    .action, .extraContent {
+      margin-left: 0;
+      text-align: left;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-sm) {
+  .pageHeader {
+    .detail {
+      display: block;
+    }
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .pageHeader {
+    .action {
+      :global {
+        .ant-btn-group, .ant-btn {
+          display: block;
+          margin-bottom: 8px;
+        }
+        .ant-btn-group > .ant-btn {
+          display: inline-block;
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+}

+ 26 - 0
src/components/PageHeader/index.md

@@ -0,0 +1,26 @@
+---
+title: PageHeader
+subtitle: 页头
+cols: 1
+order: 11
+---
+
+页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。
+
+## API
+
+| 参数      | 说明                                      | 类型         | 默认值 |
+|----------|------------------------------------------|-------------|-------|
+| title | title 区域 | ReactNode | - |
+| logo | logo区域 | ReactNode | - |
+| action | 操作区,位于 title 行的行尾 | ReactNode | - |
+| content | 内容区 | ReactNode | - |
+| extraContent | 额外内容区,位于content的右侧 | ReactNode | - |
+| routes | 面包屑相关属性,router 的路由栈信息 | object[] | - |
+| params | 面包屑相关属性,路由的参数 | object | - |
+| breadcrumbList | 面包屑数据,配置了 `routes` `params` 时此属性无效 | array<{title: ReactNode, href?: string}> | - |
+| tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | -  |
+| onTabChange | 切换面板的回调 | (key) => void | -  |
+| linkElement | 定义链接的元素,默认为 `a`,可传入 react-router 的 Link | string\|ReactElement | - |
+
+> 面包屑的配置方式有两种,一是结合 `react-router`,通过配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router);二是直接配置 `breadcrumbList`。 你也可以将 `routes` 及 `params` 放到 context 中,`PageHeader` 组件会自动获取。

+ 26 - 0
src/components/StandardFormRow/index.js

@@ -0,0 +1,26 @@
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export default ({ title, children, last, block, grid, ...rest }) => {
+  const cls = classNames(styles.standardFormRow, {
+    [styles.standardFormRowBlock]: block,
+    [styles.standardFormRowLast]: last,
+    [styles.standardFormRowGrid]: grid,
+  });
+
+  return (
+    <div className={cls} {...rest}>
+      {
+        title && (
+          <div className={styles.label}>
+            <span>{title}</span>
+          </div>
+        )
+      }
+      <div className={styles.content}>
+        {children}
+      </div>
+    </div>
+  );
+};

+ 71 - 0
src/components/StandardFormRow/index.less

@@ -0,0 +1,71 @@
+@import "~antd/lib/style/themes/default.less";
+
+.standardFormRow {
+  border-bottom: 1px dashed @border-color-split;
+  padding-bottom: 16px;
+  margin-bottom: 16px;
+  display: flex;
+  :global {
+    .ant-form-item {
+      margin-right: 24px;
+    }
+    .ant-form-item-label label {
+      color: @text-color;
+      margin-right: 0;
+    }
+    .ant-form-item-label {
+      padding: 0;
+      line-height: 32px;
+    }
+  }
+  .label {
+    color: @heading-color;
+    font-size: @font-size-base;
+    margin-right: 24px;
+    flex: 0 0 auto;
+    text-align: right;
+    & > span {
+      display: inline-block;
+      height: 32px;
+      line-height: 32px;
+      &:after {
+        content: ':';
+      }
+    }
+  }
+  .content {
+    flex: 1 1 0;
+    :global {
+      .ant-form-item:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+}
+
+.standardFormRowLast {
+  border: none;
+  padding-bottom: 0;
+  margin-bottom: 0;
+}
+
+.standardFormRowBlock {
+  :global {
+    .ant-form-item,
+    div.ant-form-item-control-wrapper {
+      display: block;
+    }
+  }
+}
+
+.standardFormRowGrid {
+  :global {
+    .ant-form-item,
+    div.ant-form-item-control-wrapper {
+      display: block;
+    }
+    .ant-form-item-label {
+      float: left;
+    }
+  }
+}

+ 93 - 0
src/components/StandardTable/index.js

@@ -0,0 +1,93 @@
+import React, { PureComponent } from 'react';
+import { Button, Icon, Table, Alert } from 'antd';
+import Logger from '../../utils/logger';
+import styles from './index.less';
+
+const logger = Logger.getLogger('StandardTableComponent');
+
+class StandardTable extends PureComponent {
+  state = {
+    selectedRowKeys: [],
+    totalCallNo: 0,
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // clean state
+    if (nextProps.selectedRows.length === 0) {
+      this.setState({
+        selectedRowKeys: [],
+        totalCallNo: 0,
+      });
+    }
+  }
+
+  handleRowSelectChange = (selectedRowKeys, selectedRows) => {
+    const totalCallNo = selectedRows.reduce((sum, val) => {
+      return sum + parseFloat(val.callNo, 10);
+    }, 0);
+
+    if (this.props.onSelectRow) {
+      this.props.onSelectRow(selectedRows);
+    }
+
+    this.setState({ selectedRowKeys, totalCallNo });
+  }
+
+  handleTableChange = (pagination, filters, sorter) => {
+    this.props.onChange(pagination, filters, sorter);
+  }
+
+  cleanSelectedKeys = () => {
+    this.handleRowSelectChange([], []);
+  }
+
+  render() {
+    const { selectedRowKeys, totalCallNo } = this.state;
+    const { dataSource, pagination, columns, rowKeyName, loading } = this.props;
+
+    //配置分页器
+    const paginationProps = {
+      showSizeChanger: true,
+      showQuickJumper: true,
+      ...pagination,
+    };
+
+    //确定该行是否可选,object
+    const rowSelection = {
+      selectedRowKeys, //指定选中项的key数组,需要和onChange配合使用,string[]
+      onChange: this.handleRowSelectChange, //选中项发生变化时的回调
+      getCheckboxProps: record => ({ //选择框的默认属性配置,func,这里设置为所有均可选
+        // disabled: record.disabled,
+        disabled: false,
+      }),
+    };
+
+    return (
+      <div className={styles.standardTable}>
+        <div className={styles.tableAlert}>
+          <Alert
+            message={(
+              <p>
+                已选择 <a style={{ fontWeight: 600 }}>{selectedRowKeys.length}</a> 项
+                <a onClick={this.cleanSelectedKeys} style={{ marginLeft: 12 }}>清空</a>
+              </p>
+            )}
+            type="info"
+            showIcon
+          />
+        </div>
+        <Table
+          loading={loading}
+          rowKey={rowKeyName}
+          rowSelection={rowSelection}
+          dataSource={dataSource}
+          columns={columns}
+          pagination={paginationProps}
+          onChange={this.handleTableChange}
+        />
+      </div>
+    );
+  }
+}
+
+export default StandardTable;

+ 18 - 0
src/components/StandardTable/index.less

@@ -0,0 +1,18 @@
+@import "~antd/lib/style/themes/default.less";
+
+.standardTable {
+  :global {
+    .ant-table-pagination {
+      margin-top: 24px;
+    }
+  }
+
+  .tableAlert {
+    margin-bottom: 16px;
+
+    p {
+      margin-bottom: 0;
+    }
+  }
+
+}

+ 27 - 0
src/index.js

@@ -0,0 +1,27 @@
+import dva from 'dva';
+import models from './models';
+import 'moment/locale/zh-cn';
+import './index.less';
+import './polyfill';
+// import { browserHistory } from 'dva/router';
+import createHistory from 'history/createBrowserHistory';
+
+// 1. Initialize
+const app = dva({
+  history: createHistory(),
+});
+
+// 2. Plugins
+// app.use({});
+
+// 3. Model
+// app.model(require('./models/example'));
+models.map((m) => {
+  app.model(m)
+});
+
+// 4. Router
+app.router(require('./router'));
+
+// 5. Start
+app.start('#root');

+ 20 - 0
src/index.less

@@ -0,0 +1,20 @@
+@import '~antd/lib/style/v2-compatible-reset.less';
+
+html, body, :global(#root) {
+  height: 100%;
+}
+
+body {
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.globalSpin {
+  width: 100%;
+  margin: 40px 0 !important;
+}
+
+.notification-success {
+  width: 50px;
+}

+ 392 - 0
src/layouts/BasicLayout.js

@@ -0,0 +1,392 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Layout, Menu, Icon, Avatar, Dropdown, Tag, message, Spin } from 'antd';
+import DocumentTitle from 'react-document-title';
+import { connect } from 'dva';
+import { Link, routerRedux, Route, Redirect, Switch } from 'dva/router';
+import moment from 'moment';
+import groupBy from 'lodash/groupBy';
+import { ContainerQuery } from 'react-container-query';
+import classNames from 'classnames';
+import styles from './BasicLayout.less';
+// import HeaderSearch from '../components/HeaderSearch';
+// import NoticeIcon from '../components/NoticeIcon';
+import GlobalFooter from '../components/GlobalFooter';
+import { getNavData } from '../common/ljNav';
+import { getRouteData } from '../utils/utils';
+
+const { Header, Sider, Content } = Layout;
+const { SubMenu } = Menu;
+
+const query = {
+  'screen-xs': {
+    maxWidth: 575,
+  },
+  'screen-sm': {
+    minWidth: 576,
+    maxWidth: 767,
+  },
+  'screen-md': {
+    minWidth: 768,
+    maxWidth: 991,
+  },
+  'screen-lg': {
+    minWidth: 992,
+    maxWidth: 1199,
+  },
+  'screen-xl': {
+    minWidth: 1200,
+  },
+};
+
+class BasicLayout extends React.PureComponent {
+  static childContextTypes = {
+    location: PropTypes.object,
+    breadcrumbNameMap: PropTypes.object,
+  }
+  constructor(props) {
+    super(props);
+    // 把一级 Layout 的 children 作为菜单项
+    this.menus = getNavData().reduce((arr, current) => arr.concat(current.children), []);
+    this.state = {
+      openKeys: this.getDefaultCollapsedSubMenus(props),
+    };
+  }
+  getChildContext() {
+    const { location } = this.props;
+    const routeData = getRouteData('BasicLayout');
+    const menuData = getNavData().reduce((arr, current) => arr.concat(current.children), []);
+    const breadcrumbNameMap = {};
+    routeData.concat(menuData).forEach((item) => {
+      breadcrumbNameMap[item.path] = item.name;
+    });
+    return { location, breadcrumbNameMap };
+  }
+  componentDidMount() {
+    // this.props.dispatch({
+    //   type: 'user/fetchCurrent',
+    // });
+  }
+  componentWillUnmount() {
+    clearTimeout(this.resizeTimeout);
+  }
+  onCollapse = (collapsed) => {
+    this.props.dispatch({
+      type: 'global/changeLayoutCollapsed',
+      payload: collapsed,
+    });
+  }
+  // onMenuClick = ({ key }) => {
+  //   if (key === 'logout') {
+  //     this.props.dispatch({
+  //       type: 'login/logout',
+  //       payload: {
+  //         status: false,
+  //       },
+  //       callback: () => {
+  //         this.props.dispatch(routerRedux.push('/user/login'));
+  //       },
+  //     });
+  //   }
+  // }
+  getDefaultCollapsedSubMenus(props) {
+    const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys(props)];
+    currentMenuSelectedKeys.splice(-1, 1);
+    if (currentMenuSelectedKeys.length === 0) {
+      return ['resource'];
+    }
+    return currentMenuSelectedKeys;
+  }
+  getCurrentMenuSelectedKeys(props) {
+    const { location: { pathname } } = props || this.props;
+    const keys = pathname.split('/').slice(1);
+    if (keys.length === 1 && keys[0] === '') {
+      return [this.menus[0].key];
+    }
+    return keys;
+  }
+  getNavMenuItems(menusData, parentPath = '') {
+    if (!menusData) {
+      return [];
+    }
+    return menusData.map((item) => {
+      if (!item.name) {
+        return null;
+      }
+      let itemPath;
+      if (item.path.indexOf('http') === 0) {
+        itemPath = item.path;
+      } else {
+        itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
+      }
+      if (item.children && item.children.some(child => child.name) && !item.filter) {
+        return (
+          <SubMenu
+            title={
+              item.icon ? (
+                <span>
+                  <Icon type={item.icon} />
+                  <span>{item.name}</span>
+                </span>
+              ) : item.name
+            }
+            key={item.key || item.path}
+          >
+            {this.getNavMenuItems(item.children, itemPath)}
+          </SubMenu>
+        );
+      }
+      const icon = item.icon && <Icon type={item.icon} />;
+      return (
+        <Menu.Item key={item.key || item.path}>
+          {
+            /^https?:\/\//.test(itemPath) ? (
+              <a href={itemPath} target={item.target}>
+                {icon}<span>{item.name}</span>
+              </a>
+            ) : (
+              <Link to={itemPath} target={item.target}>
+                {icon}<span>{item.name}</span>
+              </Link>
+            )
+          }
+        </Menu.Item>
+      );
+    });
+  }
+  // getPageTitle() {
+  //   const { location } = this.props;
+  //   const { pathname } = location;
+  //   let title = 'Ant Design Pro';
+  //   getRouteData('UserLayout').forEach((item) => {
+  //     if (item.path === pathname) {
+  //       title = `${item.name} - Ant Design Pro`;
+  //     }
+  //   });
+  //   return title;
+  // }
+  // getNoticeData() {
+  //   const { notices = [] } = this.props;
+  //   if (notices.length === 0) {
+  //     return {};
+  //   }
+  //   const newNotices = notices.map((notice) => {
+  //     const newNotice = { ...notice };
+  //     if (newNotice.datetime) {
+  //       newNotice.datetime = moment(notice.datetime).fromNow();
+  //     }
+  //     // transform id to item key
+  //     if (newNotice.id) {
+  //       newNotice.key = newNotice.id;
+  //     }
+  //     if (newNotice.extra && newNotice.status) {
+  //       const color = ({
+  //         todo: '',
+  //         processing: 'blue',
+  //         urgent: 'red',
+  //         doing: 'gold',
+  //       })[newNotice.status];
+  //       newNotice.extra = <Tag color={color} style={{ marginRight: 0 }}>{newNotice.extra}</Tag>;
+  //     }
+  //     return newNotice;
+  //   });
+  //   return groupBy(newNotices, 'type');
+  // }
+  handleOpenChange = (openKeys) => {
+    const latestOpenKey = openKeys.find(key => this.state.openKeys.indexOf(key) === -1);
+    this.setState({
+      openKeys: latestOpenKey ? [latestOpenKey] : [],
+    });
+  }
+  toggle = () => {
+    const { collapsed } = this.props;
+    this.props.dispatch({
+      type: 'global/changeLayoutCollapsed',
+      payload: !collapsed,
+    });
+    this.resizeTimeout = setTimeout(() => {
+      const event = document.createEvent('HTMLEvents');
+      event.initEvent('resize', true, false);
+      window.dispatchEvent(event);
+    }, 600);
+  }
+  // handleNoticeClear = (type) => {
+  //   message.success(`清空了${type}`);
+  //   this.props.dispatch({
+  //     type: 'global/clearNotices',
+  //     payload: type,
+  //   });
+  // }
+  // handleNoticeVisibleChange = (visible) => {
+  //   if (visible) {
+  //     this.props.dispatch({
+  //       type: 'global/fetchNotices',
+  //     });
+  //   }
+  // }
+  render() {
+    const { collapsed } = this.props;
+
+    // const menu = (
+    //   <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
+    //     <Menu.Item disabled><Icon type="user" />个人中心</Menu.Item>
+    //     <Menu.Item disabled><Icon type="setting" />设置</Menu.Item>
+    //     <Menu.Divider />
+    //     <Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
+    //   </Menu>
+    // );
+    // const noticeData = this.getNoticeData();
+
+    // Don't show popup menu when it is been collapsed
+    const menuProps = collapsed ? {} : {
+      openKeys: this.state.openKeys,
+    };
+
+    const layout = (
+      <Layout>
+        <Sider
+          trigger={null}
+          collapsible
+          collapsed={collapsed}
+          breakpoint="md"
+          onCollapse={this.onCollapse}
+          width={200}
+          className={styles.sider}
+        >
+          <div className={styles.logo}>
+            <Link to="/">
+              <img src="https://gw.alipayobjects.com/zos/rmsportal/IOtlElCiWVIOZqgDslYd.png" alt="logo" />
+              <h1>义方管理平台</h1>
+            </Link>
+          </div>
+          <div className={styles.trigger}>
+            <Icon
+              type={collapsed ? 'menu-unfold' : 'menu-fold'}
+              onClick={this.toggle}
+              style={{ color: '	#f8f8ff' }}
+            />
+          </div>
+          <Menu
+            theme="dark"
+            mode="inline"
+            {...menuProps}
+            onOpenChange={this.handleOpenChange}
+            selectedKeys={this.getCurrentMenuSelectedKeys()}
+            style={{ width: '100%' }}
+          >
+            {this.getNavMenuItems(this.menus)}
+          </Menu>
+        </Sider>
+        <Layout>
+          <Header className={styles.header}>
+            {/* 头部先去掉站内搜索和通知功能和用户信息模块
+            <div className={styles.right}>
+              <HeaderSearch
+                className={`${styles.action} ${styles.search}`}
+                placeholder="站内搜索"
+                dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
+                onSearch={(value) => {
+                  console.log('input', value); // eslint-disable-line
+                }}
+                onPressEnter={(value) => {
+                  console.log('enter', value); // eslint-disable-line
+                }}
+              />
+              <NoticeIcon
+                className={styles.action}
+                count={currentUser.notifyCount}
+                onItemClick={(item, tabProps) => {
+                  console.log(item, tabProps); // eslint-disable-line
+                }}
+                onClear={this.handleNoticeClear}
+                onPopupVisibleChange={this.handleNoticeVisibleChange}
+                loading={fetchingNotices}
+                popupAlign={{ offset: [20, -16] }}
+              >
+                <NoticeIcon.Tab
+                  list={noticeData['通知']}
+                  title="通知"
+                  emptyText="你已查看所有通知"
+                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
+                />
+                <NoticeIcon.Tab
+                  list={noticeData['消息']}
+                  title="消息"
+                  emptyText="您已读完所有消息"
+                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+                />
+                <NoticeIcon.Tab
+                  list={noticeData['待办']}
+                  title="待办"
+                  emptyText="你已完成所有待办"
+                  emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
+                />
+              </NoticeIcon>
+              {currentUser.name ? (
+                <Dropdown overlay={menu}>
+                  <span className={`${styles.action} ${styles.account}`}>
+                    <Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
+                    {currentUser.name}
+                  </span>
+                </Dropdown>
+              ) : <Spin size="small" style={{ marginLeft: 8 }} />}
+            </div>
+              */}
+          </Header>
+          <Content style={{ margin: '24px 24px 0', height: '100%' }}>
+            <Switch>
+              {
+                getRouteData('BasicLayout').map(item =>
+                  (
+                    <Route
+                      exact={item.exact}
+                      key={item.path}
+                      path={item.path}
+                      component={item.component}
+                    />
+                  )
+                )
+              }
+              <Redirect to="/sales/view" />
+            </Switch>
+            <GlobalFooter
+              // links={[{
+              //   title: 'Pro 首页',
+              //   href: 'http://pro.ant.design',
+              //   blankTarget: true,
+              // }, {
+              //   title: 'GitHub',
+              //   href: 'https://github.com/ant-design/ant-design-pro',
+              //   blankTarget: true,
+              // }, {
+              //   title: 'Ant Design',
+              //   href: 'http://ant.design',
+              //   blankTarget: true,
+              // }]}
+              copyright={
+                <div>
+                  Copyright <Icon type="copyright" /> 2010-2017 北京义方天下教育科技有限公司
+                </div>
+              }
+            />
+          </Content>
+        </Layout>
+      </Layout>
+    );
+
+    return (
+      // <div>{layout}</div>
+      // <DocumentTitle title={this.getPageTitle()}>
+        <ContainerQuery query={query}>
+          {params => <div className={classNames(params)}>{layout}</div>}
+        </ContainerQuery>
+      // </DocumentTitle>
+    );
+  }
+}
+
+export default connect(state => ({
+  collapsed: state.global.collapsed,
+  // fetchingNotices: state.global.fetchingNotices,
+  // notices: state.global.notices,
+}))(BasicLayout);

+ 114 - 0
src/layouts/BasicLayout.less

@@ -0,0 +1,114 @@
+@import "~antd/lib/style/themes/default.less";
+@import "~antd/dist/antd.less";
+
+.header {
+  padding: 0 12px 0 0;
+  // background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
+  position: relative;
+}
+
+.logo {
+  height: 50px;
+  position: relative;
+  line-height: 50px;
+  padding-left: 20px;
+  transition: all .3s;
+  background: #373D41;
+  overflow: hidden;
+  img {
+    display: inline-block;
+    vertical-align: middle;
+    height: 30px;
+  }
+  h1 {
+    color: #fff;
+    display: inline-block;
+    vertical-align: middle;
+    font-size: 16px;
+    margin-left: 8px;
+    font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+    font-weight: 600;
+  }
+}
+
+:global(.ant-layout-sider-collapsed) .logo {
+  padding-left: 24px;
+  > a {
+    width: 30px;
+  }
+}
+
+.trigger {
+  font-size: 20px;
+  text-align: center;
+  line-height: 30px;
+  cursor: pointer;
+  transition: all .3s;
+  &:hover {
+    background: #42485B;
+  }
+}
+
+@media screen and (max-width: @screen-xs) {
+  .trigger {
+    display: none;
+  }
+}
+
+.right {
+  float: right;
+  height: 100%;
+  .action {
+    cursor: pointer;
+    padding: 0 12px;
+    display: inline-block;
+    transition: all .3s;
+    height: 100%;
+    > i {
+      font-size: 16px;
+      vertical-align: middle;
+    }
+    &:global(.ant-popover-open),
+    &:hover {
+      background: @primary-1;
+    }
+  }
+  .search {
+    padding: 0;
+    margin: 0 12px;
+    &:hover {
+      background: transparent;
+    }
+  }
+  .account {
+    .avatar {
+      margin: 20px 8px 20px 0;
+      color: @primary-color;
+      background: rgba(255, 255, 255, .85);
+      vertical-align: middle;
+    }
+  }
+}
+
+.menu {
+  :global(.anticon) {
+    margin-right: 8px;
+  }
+  :global(.ant-dropdown-menu-item) {
+    width: 160px;
+  }
+}
+
+:global {
+  .ant-layout {
+    overflow-x: hidden;
+  }
+}
+
+.sider {
+  min-height: 100vh;
+  box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
+  position: relative;
+  z-index: 10;
+}

+ 12 - 0
src/layouts/PageHeaderLayout.js

@@ -0,0 +1,12 @@
+import React from 'react';
+import { Link } from 'dva/router';
+import PageHeader from '../components/PageHeader';
+import styles from './PageHeaderLayout.less';
+
+export default ({ children, wrapperClassName, top, ...restProps }) => (
+  <div style={{ margin: '-24px -24px 0' }} className={wrapperClassName}>
+    {top}
+    <PageHeader {...restProps} linkElement={Link} />
+    {children ? <div className={styles.content}>{children}</div> : null}
+  </div>
+);

+ 11 - 0
src/layouts/PageHeaderLayout.less

@@ -0,0 +1,11 @@
+@import "~antd/lib/style/themes/default.less";
+
+.content {
+  margin-top: 2px;
+}
+
+@media screen and (max-width: @screen-sm) {
+  .content {
+    margin: 24px 0 0;
+  }
+}

+ 135 - 0
src/models/course.js

@@ -0,0 +1,135 @@
+import pathToRegexp from 'path-to-regexp';
+import { notification, message } from 'antd';
+import { getCourseList } from '../services/ProductApi';
+import { dateFormat } from '../utils/utils';
+import Logger from '../utils/logger';
+
+const logger = new Logger('CourseModel');
+
+// function CourseItemFormTemp() {
+//   return {
+//     code: '',
+//     name: '',
+//     digest: '',
+//     status: '',
+//     wareList: [],
+//   };
+// }
+//
+export default {
+  namespace: 'course',
+
+  state: {
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: false,
+    // lessonItem: lessonItemFormTemp(),
+  },
+
+  effects: {
+    *getCourseList({ payload }, { put, call }) {
+      yield put({ type: 'changeLoading', payload: { loading: true } });
+
+      const response = yield call(getCourseList, payload);
+
+      const data = {
+        list: dateFormat(response.data.list || []),
+        pagination: {
+          pageSize : response.data.pageSize || 15,
+          current  : response.data.pageNo || 1,
+          total    : response.data.totalSize || 0,
+        }
+      };
+
+      yield put({ type: 'saveCourseList', payload: { data } });
+      yield put({ type: 'changeLoading', payload: { loading: false }});
+    },
+    // *getLessonOne({ payload }, { put, call }) {
+    //   const response = yield call(getLessonItem, payload);
+    //
+    //   const data = response.data || {};
+    //
+    //   yield put({ type: 'saveItemData', payload: data });
+    // },
+    // *addLessonItem({ payload }, { put, call }) {
+    //   const response = yield call(addLessonItem, payload);
+    //   if (!response || response.code !== 200) {
+    //     notification.error({
+    //       message: '创建课失败!',
+    //       description: response.message,
+    //     });
+    //   }
+    //   else {
+    //     notification.success({
+    //       message: '创建课成功!',
+    //       description: '成功创建1个课',
+    //     });
+    //   }
+    // },
+    // *updateLessonItem({ payload }, { put, call }) {
+    //   const response = yield call(updateLessonItem, payload);
+    //   if (!response || response.code !== 200) {
+    //     notification.error({
+    //       message: '更新课失败!',
+    //       description: response.message,
+    //     });
+    //   }
+    //   else {
+    //     notification.success({
+    //       message: '更新课成功!',
+    //       description: '成功更新1个课',
+    //     });
+    //   }
+    // },
+    // *delLessonItem({ payload }, { put, call }) {
+    //   const response = yield call(delLessonItem, payload);
+    //   if (!response || response.code !== 200) {
+    //     notification.error({
+    //       message: '删除课失败!',
+    //       description: response.message,
+    //     });
+    //   }
+    //   else {
+    //     notification.success({
+    //       message: '删除课成功!',
+    //       description: '成功删除1个课',
+    //     });
+    //   }
+    // }
+  },
+
+  reducers: {
+    saveCourseList(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    // saveItemData(state, action) {
+    //   return {
+    //     ...state,
+    //     lessonItem: {
+    //       code: action.payload.code,
+    //       name: action.payload.name,
+    //       digest: action.payload.digest,
+    //       status: action.payload.status,
+    //       wareList: action.payload.wareList || [],
+    //     }
+    //   }
+    // },
+    // clearAll(state) {
+    //   return {
+    //     ...state,
+    //     lessonItem: lessonItemFormTemp(),
+    //   }
+    // }
+  },
+}

+ 41 - 0
src/models/cp.js

@@ -0,0 +1,41 @@
+import { getCPList } from '../services/CPApi';
+import Logger from '../utils/logger';
+import { dateFormat } from '../utils/utils';
+
+const logger = new Logger('CPModel');
+
+export default {
+  namespace: 'cp',
+
+  state: {
+    cpList: [],
+  },
+
+  effects: {
+    *getCPList({ payload }, { call, put }) {
+      const response = yield call(getCPList);
+
+      if (response.err) {
+        logger.error('getCPList request error: %o', response.err);
+        return;
+      }
+      else {
+        logger.debug('getCPList request success: %o', response);
+      }
+
+      yield put({
+        type: 'saveCPList',
+        payload: dateFormat(response.data.list || []),
+      });
+    },
+  },
+
+  reducers: {
+    saveCPList(state, action) {
+      return {
+        ...state,
+        cpList: action.payload,
+      }
+    },
+  },
+}

+ 76 - 0
src/models/global.js

@@ -0,0 +1,76 @@
+//import { queryNotices } from '../services/api';
+
+export default {
+  namespace: 'global',
+
+  state: {
+    collapsed: false,
+    notices: [],
+    fetchingNotices: false,
+  },
+
+  effects: {
+    // *fetchNotices(_, { call, put }) {
+    //   yield put({
+    //     type: 'changeNoticeLoading',
+    //     payload: true,
+    //   });
+    //   const data = yield call(queryNotices);
+    //   yield put({
+    //     type: 'saveNotices',
+    //     payload: data,
+    //   });
+    // },
+    // *clearNotices({ payload }, { put, select }) {
+    //   const count = yield select(state => state.global.notices.length);
+    //   yield put({
+    //     type: 'user/changeNotifyCount',
+    //     payload: count,
+    //   });
+    //
+    //   yield put({
+    //     type: 'saveClearedNotices',
+    //     payload,
+    //   });
+    // },
+  },
+
+  reducers: {
+    changeLayoutCollapsed(state, { payload }) {
+      return {
+        ...state,
+        collapsed: payload,
+      };
+    },
+    // saveNotices(state, { payload }) {
+    //   return {
+    //     ...state,
+    //     notices: payload,
+    //     fetchingNotices: false,
+    //   };
+    // },
+    // saveClearedNotices(state, { payload }) {
+    //   return {
+    //     ...state,
+    //     notices: state.notices.filter(item => item.type !== payload),
+    //   };
+    // },
+    // changeNoticeLoading(state, { payload }) {
+    //   return {
+    //     ...state,
+    //     fetchingNotices: payload,
+    //   };
+    // },
+  },
+
+  subscriptions: {
+    setup({ history }) {
+      // Subscribe history(url) change, trigger `load` action if pathname is `/`
+      return history.listen(({ pathname, search }) => {
+        if (typeof window.ga !== 'undefined') {
+          window.ga('send', 'pageview', pathname + search);
+        }
+      });
+    },
+  },
+};

+ 127 - 0
src/models/image.js

@@ -0,0 +1,127 @@
+import { getResourceList, getOssUploadParams, uploadToOss, submitOssUploadResult } from '../services/resource';
+import { Notification } from 'antd';
+
+export default {
+  namespace: 'resource',
+
+  state: {
+    oss: {
+      fileList: [],
+      ossParams: {},
+    },
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: true,
+    ossUploading: false,
+  },
+
+  effects: {
+    *fetch({ payload }, { put, call }) {
+      yield put({
+        type: 'changeLoading',
+        payload: {
+          loading: true
+        },
+      });
+
+      console.log('=====================');
+      console.log(payload);
+      console.log('=====================');
+      const response = yield call(getResourceList, payload);
+      const data = {
+        list: response.data.list || [],
+        pagination: {
+          pageSize: response.data.pageSize || 10,
+          current: response.data.pageNo || 1,
+          total: response.data.totalSize || 0,
+        }
+      };
+
+      yield put({
+        type: 'save',
+        payload: data,
+      });
+
+      yield put({
+        type: 'changeLoading',
+        payload: {
+          loading: false,
+        }
+      });
+    },
+
+    *resourceUpload({ payload, callback }, { put, call, select }) {
+      //Step-1 初始化上传状态为true
+      yield put({ type: 'ossUploading', payload: { ossUploading: true} });
+
+      //Step-2 进行oss上传参数有效期校验
+      let ossUploadArgs = yield select(state => state.resource.ossParams);
+      let cacheTimestamp = (Date.parse(new Date()) / 1000 + 5).toString();//加5s缓冲时间
+      if (!ossUploadArgs || !ossUploadArgs.expire || ossUploadArgs.expire <= cacheTimestamp) {
+        const ossArgsUpdateRes = yield call(getOssUploadParams);
+        if (!ossArgsUpdateRes.code || ossArgsUpdateRes.code !== 200) {
+          yield put({ type: 'ossUploading', payload: { ossUploading: false, fileList: [] } });
+          Notification.error({ message: '图片上传失败!' });
+          return;
+        }
+        yield put({ type: 'ossParamsSave', payload: ossArgsUpdateRes.data });
+      }
+
+      //Step-3 oss文件上传
+      const ossParams = yield select(state => state.resource.ossParams);
+      const fileList = yield select(state => state.resource.fileList);
+      const ossUploadRes = yield call(uploadToOss, {fileList, ossParams});
+
+      //Step-4 oss上传成功后,提交信息到后台
+      if (!ossUploadRes.code || ossUploadRes.code !== 200) {
+        yield put({ type: 'ossUploading', payload: { ossUploading: false, fileList: [] } });
+        Notification.error({ message: '图片上传失败!' });
+        return;
+      }
+      const submitRes  = yield call(submitOssUploadResult, ossUploadRes.data);
+      (!submitRes.code || submitRes.code !== 200 || !submitRes.success) ?
+        Notification.error({ message: '图片上传失败!' }) :
+        Notification.success({ message: '图片上传成功!' });
+      yield put({ type: 'ossUploading', payload: { ossUploading: false, fileList: [] } });
+    }
+  },
+  reducers: {
+    /* 保存请求回来的数据 */
+    save(state, action) {
+      return {
+        ...state,
+        data: action.payload,
+      };
+    },
+    /* 控制列表的加载状态 */
+    changeLoading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+    /* oss参数的保存,刷新页面后失效 */
+    ossParamsSave(state, action) {
+      return {
+        ...state,
+        ossParams: action.payload,
+      }
+    },
+    /* 改变文件的上传状态,清空上传文件列表 */
+    ossUploading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    /* 控制fileList内容 */
+    setFileList(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    }
+  }
+}

+ 9 - 0
src/models/index.js

@@ -0,0 +1,9 @@
+const context = require.context('./', false, /\.js$/);
+const keys = context.keys().filter(item => item !== './index.js');
+
+const models = [];
+for (let i = 0; i < keys.length; i += 1) {
+  models.push(context(keys[i]));
+}
+
+export default models;

+ 119 - 0
src/models/item.js

@@ -0,0 +1,119 @@
+import pathToRegexp from 'path-to-regexp';
+import { notification, message } from 'antd';
+import { getItemList, getItemItem, addItemItem, updateItemItem, delItemItem } from '../services/ItemAPI';
+import { dateFormat } from '../utils/utils';
+import Logger from '../utils/logger';
+
+const logger = new Logger('ItemModel');
+
+export default {
+  namespace: 'item',
+
+  state: {
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: false,
+    itemItem: {},
+  },
+
+  effects: {
+    *getItemList({ payload }, { put, call }) {
+      yield put({ type: 'changeLoading', payload: { loading: true } });
+
+      const response = yield call(getItemList, payload);
+
+      const data = {
+        list: dateFormat(response.data.list || []),
+        pagination: {
+          pageSize : response.data.pageSize || 15,
+          current  : response.data.pageNo || 1,
+          total    : response.data.totalSize || 0,
+        }
+      };
+
+      yield put({ type: 'saveItemList', payload: { data } });
+      yield put({ type: 'changeLoading', payload: { loading: false }});
+    },
+    *getItemOne({ payload }, { put, call }) {
+      const response = yield call(getItemItem, payload);
+
+      const data = response.data || {};
+
+      yield put({ type: 'saveItemData', payload: data });
+    },
+    *addItemItem({ payload }, { put, call }) {
+      const response = yield call(addItemItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '创建商品失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '创商品成功!',
+          description: '成功创建1个商品',
+        });
+      }
+    },
+    *updateItemItem({ payload }, { put, call }) {
+      const response = yield call(updateItemItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '更新商品失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '更新商品成功!',
+          description: '成功更新1个商品',
+        });
+      }
+    },
+    *delItemItem({ payload }, { put, call }) {
+      const response = yield call(delItemItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '删除商品失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '删除商品成功!',
+          description: '成功删除1个商品',
+        });
+      }
+    }
+  },
+
+  reducers: {
+    saveItemList(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    saveItemData(state, action) {
+      return {
+        ...state,
+        itemItem: { ...action.payload },
+      }
+    },
+    clearAll(state) {
+      return {
+        ...state,
+        itemItem: {},
+      }
+    }
+  },
+}

+ 134 - 0
src/models/lesson.js

@@ -0,0 +1,134 @@
+import { notification, message } from 'antd';
+import pathToRegexp from 'path-to-regexp';
+import { getLessonList, getLessonItem, addLessonItem, updateLessonItem, delLessonItem } from '../services/ProductApi';
+import { dateFormat } from '../utils/utils';
+import Logger from '../utils/logger';
+
+const logger = new Logger('LessonModel');
+
+function lessonItemFormTemp() {
+  return {
+    code: '',
+    name: '',
+    digest: '',
+    status: '',
+    wareList: [],
+  };
+}
+
+export default {
+  namespace: 'lesson',
+
+  state: {
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: false,
+    lessonItem: lessonItemFormTemp(),
+  },
+
+  effects: {
+    *getLessonList({ payload }, { put, call }) {
+      yield put({ type: 'changeLoading', payload: { loading: true } });
+      const response = yield call(getLessonList, payload);
+
+      const data = {
+        list: dateFormat(response.data.list || []),
+        pagination: {
+          pageSize : response.data.pageSize || 15,
+          current  : response.data.pageNo || 1,
+          total    : response.data.totalSize || 0,
+        }
+      };
+      logger.info('【Response Pagination】: %o', data);
+      yield put({ type: 'saveLessonList', payload: { data } });
+      yield put({ type: 'changeLoading', payload: { loading: false }});
+    },
+    *getLessonOne({ payload }, { put, call }) {
+      const response = yield call(getLessonItem, payload);
+
+      const data = response.data || {};
+
+      yield put({ type: 'saveItemData', payload: data });
+    },
+    *addLessonItem({ payload }, { put, call }) {
+      const response = yield call(addLessonItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '创建课失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '创建课成功!',
+          description: '成功创建1个课',
+        });
+      }
+    },
+    *updateLessonItem({ payload }, { put, call }) {
+      const response = yield call(updateLessonItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '更新课失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '更新课成功!',
+          description: '成功更新1个课',
+        });
+      }
+    },
+    *delLessonItem({ payload }, { put, call }) {
+      const response = yield call(delLessonItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '删除课失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '删除课成功!',
+          description: '成功删除1个课',
+        });
+      }
+    }
+  },
+
+  reducers: {
+    saveLessonList(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    saveItemData(state, action) {
+      return {
+        ...state,
+        lessonItem: {
+          code: action.payload.code,
+          name: action.payload.name,
+          digest: action.payload.digest,
+          status: action.payload.status,
+          wareList: action.payload.wareList || [],
+        }
+      }
+    },
+    clearAll(state) {
+      return {
+        ...state,
+        lessonItem: lessonItemFormTemp(),
+      }
+    }
+  },
+}

+ 41 - 0
src/models/merchant.js

@@ -0,0 +1,41 @@
+import { getMerchantList } from '../services/MerchantApi';
+import Logger from '../utils/logger';
+import { dateFormat } from '../utils/utils';
+
+const logger = new Logger('MerchantModel');
+
+export default {
+  namespace: 'merchant',
+
+  state: {
+    merchantList: [],
+  },
+
+  effects: {
+    *getAllMerchants({ payload }, { put, call }) {
+      const response = yield call(getMerchantList);
+
+      if (response.err) {
+        logger.error('getAllMerchants request error: %o', response.err);
+        return;
+      }
+      else {
+        logger.debug('getAllMerchants request success: %o', response);
+      }
+
+      yield put({
+        type: 'saveMerchantList',
+        payload: dateFormat(response.data.list || []),
+      });
+    },
+  },
+
+  reducers: {
+    saveMerchantList(state, action) {
+      return {
+        ...state,
+        merchantList: action.payload,
+      }
+    },
+  }
+}

+ 143 - 0
src/models/support.js

@@ -0,0 +1,143 @@
+import pathToRegexp from 'path-to-regexp';
+import { notification, message } from 'antd';
+import { getSupportList, getSupportItem, addSupportItem, updateSupportItem, delSupportItem } from '../services/ProductApi';
+import { dateFormat } from '../utils/utils';
+import Logger from '../utils/logger';
+
+const logger = new Logger('SupportModel');
+
+function SupportItemFormTemp() {
+  return {
+    id: '',
+    code: '',
+    name: '',
+    digest: '',
+    status: '',
+    cpId: '',
+    cpName: '',
+    tags: [],
+    supportArr: [],
+  };
+}
+
+export default {
+  namespace: 'support',
+
+  state: {
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: false,
+    supportItem: SupportItemFormTemp(),
+  },
+
+  effects: {
+    *getSupportList({ payload }, { put, call }) {
+      yield put({ type: 'changeLoading', payload: { loading: true } });
+
+      const response = yield call(getSupportList, payload);
+
+      const data = {
+        list: dateFormat(response.data.list || []),
+        pagination: {
+          pageSize : response.data.pageSize || 15,
+          current  : response.data.pageNo || 1,
+          total    : response.data.totalSize || 0,
+        }
+      };
+
+      yield put({ type: 'saveSupportList', payload: { data } });
+      yield put({ type: 'changeLoading', payload: { loading: false }});
+    },
+    *getSupportOne({ payload }, { put, call }) {
+      const response = yield call(getSupportItem, payload);
+
+      const data = response.data || {};
+
+      yield put({ type: 'saveItemData', payload: data });
+    },
+    *addSupportItem({ payload }, { put, call }) {
+      const response = yield call(addSupportItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '创建周边失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '创周边成功!',
+          description: '成功创建1个周边',
+        });
+      }
+    },
+    *updateSupportItem({ payload }, { put, call }) {
+      const response = yield call(updateSupportItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '更新周边失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '更新周边成功!',
+          description: '成功更新1个周边',
+        });
+      }
+    },
+    *delSupportItem({ payload }, { put, call }) {
+      const response = yield call(delSupportItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '删除周边失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '删除周边成功!',
+          description: '成功删除1个周边',
+        });
+      }
+    }
+  },
+
+  reducers: {
+    saveSupportList(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    saveItemData(state, action) {
+      return {
+        ...state,
+        supportItem: {
+          id: action.payload.id,
+          code: action.payload.code,
+          name: action.payload.name,
+          digest: action.payload.digest,
+          status: action.payload.status,
+          cpId: action.payload.cpId,
+          cpName: action.payload.cpName,
+          tags: action.payload.tags,
+          supportArr: action.payload.supportArr,
+        }
+      }
+    },
+    clearAll(state) {
+      return {
+        ...state,
+        supportItem: SupportItemFormTemp(),
+      }
+    }
+  },
+}

+ 141 - 0
src/models/tag.js

@@ -0,0 +1,141 @@
+import { notification } from 'antd';
+import { getTagList, getTagItem, addTagItem, updateTagItem, delTagItem } from '../services/ProductApi';
+import Logger from '../utils/logger';
+import { dateFormat } from '../utils/utils';
+
+const logger = new Logger('TagModel');
+
+function TagItemFormTemp() {
+  return {
+    tagId: '',
+    tagName: '',
+    type: '',
+    merchantId: '',
+    merchantName: '',
+  };
+}
+
+export default {
+  namespace: 'tag',
+
+  state: {
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: false,
+    tagItem: TagItemFormTemp(),
+  },
+
+  effects: {
+    *getTagList({ payload }, { put, call }) {
+      yield put({ type: 'changeLoading', payload: { loading: true } });
+      const response = yield call(getTagList, payload);
+
+      if (response.err)
+      {
+        logger.error('Tag list request error: %o', response.err);
+        return;
+      }
+      else {
+        logger.debug('Tag list request succ, response: %o', response);
+      }
+
+      const data = {
+        list: dateFormat(response.data.list || []),
+        pagination: {
+          pageSize : response.data.pageSize || 15,
+          current  : response.data.pageNo || 1,
+          total    : response.data.totalSize || 0,
+        }
+      };
+      yield put({ type: 'saveTagList', payload: { data } });
+      yield put({ type: 'changeLoading', payload: { loading: false }});
+    },
+    *getTagOne({ payload }, { put, call }) {
+      const response = yield call(getTagItem, payload);
+
+      const data = response.data || {};
+
+      yield put({ type: 'saveItemData', payload: data });
+    },
+    *addTagItem({ payload }, { put, call }) {
+      const response = yield call(addTagItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '创建标签失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '创标签成功!',
+          description: '成功标签1个周边',
+        });
+      }
+    },
+    *updateTagItem({ payload }, { put, call }) {
+      const response = yield call(updateTagItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '更新标签失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '更新标签成功!',
+          description: '成功更新1个标签',
+        });
+      }
+    },
+    *delTagItem({ payload }, { put, call }) {
+      const response = yield call(delTagItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '删除标签失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '删除标签成功!',
+          description: '成功删除1个标签',
+        });
+      }
+    }
+  },
+
+  reducers: {
+    saveTagList(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    saveItemData(state, action) {
+      return {
+        ...state,
+        tagItem: {
+          tagId: action.payload.tagId,
+          tagName: action.payload.tagName,
+          type: action.payload.type,
+          merchantId: action.payload.merchantId,
+          merchantName: action.payload.merchantName,
+        }
+      }
+    },
+    clearAll(state) {
+      return {
+        ...state,
+        tagItem: TagItemFormTemp(),
+      }
+    }
+  }
+}

+ 128 - 0
src/models/ware.js

@@ -0,0 +1,128 @@
+import { notification, message } from 'antd';
+import { getWareList, addWareItem, updateWareItem, delWareItem } from '../services/ProductApi';
+import { dateFormat } from '../utils/utils';
+import Logger from '../utils/logger';
+
+const logger = new Logger('WareModel');
+
+export default {
+  namespace: 'ware',
+
+  state: {
+    data: {
+      list: [],
+      pagination: {},
+    },
+    loading: false,
+    wareItem: {
+      actionType: 'ADD',
+      itemData: {},
+    },
+  },
+
+  effects: {
+    *getWareList({ payload }, { put, call }) {
+      yield put({ type: 'changeLoading', payload: { loading: true } });
+      const response = yield call(getWareList, payload);
+
+      if (response.err)
+      {
+        logger.error('Ware list request error: %o', response.err);
+        return;
+      }
+      else {
+        logger.debug('Ware list request succ, response: %o', response);
+      }
+
+      const data = {
+        list: dateFormat(response.data.list || []),
+        pagination: {
+          pageSize : response.data.pageSize || 15,
+          current  : response.data.pageNo || 1,
+          total    : response.data.totalSize || 0,
+        }
+      };
+      logger.info('【Response Pagination】: %o', data);
+      yield put({ type: 'saveWareList', payload: { data } });
+      yield put({ type: 'changeLoading', payload: { loading: false }});
+    },
+    *addWareItem({ payload }, { put, call }) {
+      const response = yield call(addWareItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '新建课件失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '新建课件成功!',
+          description: '成功创建1个课件',
+        });
+      }
+    },
+    *updateWareItem({ payload }, { put, call }) {
+      const response = yield call(updateWareItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '更新课件失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '更新课件成功!',
+          description: '成功更新1个课件',
+        });
+      }
+    },
+    *delWareItem({ payload }, { put, call }) {
+      const response = yield call(delWareItem, payload);
+      if (!response || response.code !== 200) {
+        notification.error({
+          message: '删除课件失败!',
+          description: response.message,
+        });
+      }
+      else {
+        notification.success({
+          message: '删除课件成功!',
+          description: '成功删除1个课件',
+        });
+      }
+    }
+  },
+
+  reducers: {
+    saveWareList(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    changeLoading(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      }
+    },
+    saveItemData(state, action) {
+      return {
+        ...state,
+        wareItem: {
+          itemData: action.payload,
+          actionType: 'EDIT',
+        }
+      }
+    },
+    clearAll(state) {
+      return {
+        ...state,
+        wareItem: {
+          actionType: 'ADD',
+          itemData: {},
+        }
+      }
+    }
+  },
+}

+ 7 - 0
src/polyfill.js

@@ -0,0 +1,7 @@
+import 'core-js/es6/map';
+import 'core-js/es6/set';
+
+global.requestAnimationFrame =
+  global.requestAnimationFrame || function requestAnimationFrame(callback) {
+    setTimeout(callback, 0);
+  };

+ 63 - 0
src/router.js

@@ -0,0 +1,63 @@
+import React from 'react';
+import { Router, Route, Switch, Redirect } from 'dva/router';
+import { LocaleProvider, Spin } from 'antd';
+import zhCN from 'antd/lib/locale-provider/zh_CN';
+import dynamic from 'dva/dynamic';
+import cloneDeep from 'lodash/cloneDeep';
+import { getNavData } from './common/ljNav';
+import { getPlainNode } from './utils/utils'
+import BasicLayout from './layouts/BasicLayout';
+
+import styles from './index.less';
+
+dynamic.setDefaultLoadingComponent(() => {
+  return <Spin size="large" className={styles.globalSpin} />;
+});
+
+function getRouteData(navData, path) {
+  if (!navData.some(item => item.layout === path) ||
+    !(navData.filter(item => item.layout === path)[0].children)) {
+    return null;
+  }
+  const route = cloneDeep(navData.filter(item => item.layout === path)[0]);
+  const nodeList = getPlainNode(route.children);
+  return nodeList;
+}
+
+function getLayout(navData, path) {
+  if (!navData.some(item => item.layout === path) ||
+    !(navData.filter(item => item.layout === path)[0].children)) {
+    return null;
+  }
+  const route = navData.filter(item => item.layout === path)[0];
+  return {
+    component: route.component,
+    layout: route.layout,
+    name: route.name,
+    path: route.path,
+  };
+}
+
+function RouterConfig({ history, app }) {
+  const navData = getNavData(app);
+  const BasicLayout = getLayout(navData, 'BasicLayout').component;
+  const passProps = {
+    app,
+    navData,
+    getRouteData: (path) => {
+      return getRouteData(navData, path);
+    },
+  }
+
+  return (
+    <LocaleProvider locale={zhCN}>
+      <Router history={history}>
+        <Switch>
+          <Route path="/" render={props => <BasicLayout {...props} {...passProps} />} />
+        </Switch>
+      </Router>
+    </LocaleProvider>
+  );
+}
+
+export default RouterConfig;

+ 7 - 0
src/routes/Exception/403.js

@@ -0,0 +1,7 @@
+import React from 'react';
+import { Link } from 'dva/router';
+import Exception from '../../components/Exception';
+
+export default () => (
+  <Exception type="403" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
+);

+ 7 - 0
src/routes/Exception/404.js

@@ -0,0 +1,7 @@
+import React from 'react';
+import { Link } from 'dva/router';
+import Exception from '../../components/Exception';
+
+export default () => (
+  <Exception type="404" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
+);

+ 7 - 0
src/routes/Exception/500.js

@@ -0,0 +1,7 @@
+import React from 'react';
+import { Link } from 'dva/router';
+import Exception from '../../components/Exception';
+
+export default () => (
+  <Exception type="500" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
+);

+ 217 - 0
src/routes/Goods/ItemList.js

@@ -0,0 +1,217 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { Link, routerRedux } from 'dva/router';
+import {
+  Popconfirm, Progress, notification, Badge, message, Card,
+  Icon, Button, Modal, Input, Select, Form, Row, Col
+} from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import styles from './ItemList.less';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const { Option } = Select;
+const FormItem = Form.Item;
+const ButtonGroup = Button.Group;
+
+const logger = Logger.getLogger('ItemList');
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  item: state.item,
+  cp: state.cp,
+}))
+@Form.create()
+export default class ItemList extends PureComponent {
+  state = {
+    selectedRows: [],   //记录选中的行
+    formValues: {},     //记录搜索表单值
+  };
+
+  _actionControlCenter(type, payload = {}) {
+    const { dispatch } = this.props;
+    //对空字段进行过滤
+    Object.keys(payload).map(key => {payload[key] ? null : delete payload[key]});
+    dispatch({ type, payload });
+  }
+
+  componentDidMount() {
+    this._actionControlCenter('item/getItemList');
+    this._actionControlCenter('cp/getCPList');
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+    const { form } = this.props;
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = {
+        ...fieldsValue,
+      };
+      this.setState({
+        formValues: {...fieldsValue},
+      });
+      this._actionControlCenter('item/getItemList', values);
+    });
+  }
+
+  handleReset = (e) => {
+    e.preventDefault();
+    const { form } = this.props;
+    form.resetFields();
+    this.setState({
+      formValues: {},
+    });
+    this._actionControlCenter('item/getItemList');
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  handleItemAdd = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/goods/item/add'));
+  }
+
+  handleItemEdit = (record) => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push({
+      pathname: '/goods/item/edit',
+      state: { code: record.code },       //进入编辑页面传递code参数
+    }));
+  }
+
+  handleItemDel = (record) => {
+    this._actionControlCenter('item/delItemItem', { code: record.code });
+    this._actionControlCenter('item/getItemList');
+  }
+
+  //配置分页器
+  handleStandardTableChange = (pagination, filterArgs, sorter) => {
+    const { formValues } = this.state;
+    const filters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = {...obj};
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+    this._actionControlCenter('item/getItemList', params);
+  }
+
+  render() {
+    const { selectedRows } = this.state;
+    const { dispatch, item, cp } = this.props;
+    const { getFieldDecorator } = this.props.form;
+
+    const columns = [{
+      title: '商品编号',
+      dataIndex: 'code',
+    },{
+      title: '商品名称',
+      dataIndex: 'name',
+    },{
+      title: '内容供应商',
+      dataIndex: 'cpName',
+      filters: cp.cpList.map(item => ({
+        text: item.cpName,
+        value: item.cpId,
+      })),
+    },{
+      title: '标签名称',
+      dataIndex: 'tagList',
+      render: (text, record) => (
+        <p>{record.tagList.map(item => item.name).join(',')}</p>
+      ),
+    },{
+      title: '使用状态',
+      dataIndex: 'status',
+      filters: config.ITEM_STATE.map(item => ({
+        text: item.name,
+        value: item.value,
+      })),
+      render: (text, record) => (
+        <span>
+          {(config.ITEM_STATE.filter(item => item.value === record.status)[0] || { name: '未知' }).name}
+        </span>
+      ),
+    },{
+      title: '修改时间',
+      dataIndex: 'gmtModified',
+    },{
+      title: '操作类型',
+      render: (record) => (
+        <span>
+          <Button size="small" shape="circle" icon="edit" onClick={() => this.handleItemEdit(record)}></Button>
+          <span className={styles.splitLine} />
+          <Popconfirm
+            placement="top"
+            okText="删除"
+            cancelText="取消"
+            title='确定删除?'
+            onConfirm={() => this.handleItemDel(record)}
+          >
+            <Button size="small" shape="circle" icon="delete"></Button>
+          </Popconfirm>
+        </span>
+      ),
+    }];
+
+    const standardTableProps = {
+      dataSource: item.data.list,
+      pagination: item.data.pagination,
+      loading: item.loading,
+      columns: columns,
+      rowKeyName: 'id',
+      selectedRows: selectedRows,
+    };
+
+    const pageHeaderSearch = (
+      <Form layout="inline" onSubmit={this.handleSearch}>
+        <FormItem label="商品编号">
+          {getFieldDecorator('code')(
+            <Input placeholder="请输入商品编号" />
+          )}
+        </FormItem>
+        <FormItem label="商品名称">
+          {getFieldDecorator('name')(
+            <Input placeholder="请输入商品名称" />
+          )}
+        </FormItem>
+        <FormItem>
+          <ButtonGroup>
+            <Button icon="search" type="primary" htmlType="submit">搜索</Button>
+            <Button icon="reload" type="danger" onClick={this.handleReset}>重置</Button>
+          </ButtonGroup>
+        </FormItem>
+      </Form>
+    );
+
+    return (
+      <PageHeaderLayout
+        content={pageHeaderSearch}
+      >
+        <div className={styles.content}>
+          <div className={styles.newButton}>
+            <Button type="primary" icon="plus" onClick={() => {this.handleItemAdd()}}>添加</Button>
+          </div>
+          <StandardTable
+            {...standardTableProps}
+            onChange={this.handleStandardTableChange}
+            onSelectRow={this.handleSelectRows}
+          />
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 19 - 0
src/routes/Goods/ItemList.less

@@ -0,0 +1,19 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 1px 15px 15px 15px;
+
+  .newButton {
+    margin: 10px 0 10px 0;
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 233 - 0
src/routes/Goods/ItemSave.js

@@ -0,0 +1,233 @@
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import React, { PureComponent } from 'react';
+import { Form, Input, Select, Card, Button, Table } from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import DescriptionList from '../../components/DescriptionList';
+import EditableTable from '../../components/EditableTable';
+import styles from './ItemSave.less';
+
+import { getParentPath } from '../../utils/utils';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const FormItem = Form.Item;
+const InputGroup = Input.Group;
+const { TextArea } = Input;
+const { Option } = Select;
+const { Description } = DescriptionList;
+
+const { ROUTER_MAP, ACTION_MAP } = config;
+const logger = Logger.getLogger('ItemSave');
+
+@connect(state => ({
+  item: state.item,
+}))
+@Form.create()
+export default class ItemSave extends PureComponent {
+  state = {
+    endPoint: '',          //操作类型,edit
+    parentPath: '',        //路径前缀
+    currentTab: 'basic',   //当前选中的tab
+  };
+
+  _actionControlCenter = (type, payload = {}) => {
+    const { dispatch } = this.props;
+    //对空字段进行过滤
+    Object.keys(payload).map(key => {payload[key] ? null : delete payload[key]});
+    //触发modal中action
+    dispatch({ type, payload });
+  }
+
+  _routerControlCenter = (router = '/', payload = null) => {
+    const { dispatch } = this.props;
+    if (payload) {
+      dispatch(routerRedux.push({ pathname: router, state: payload }));
+    }
+    else {
+      dispatch(routerRedux.push(router));
+    }
+  }
+
+  componentWillMount() {
+    const { location } = this.props;
+    const { endPoint, parentPath } = getParentPath(location);
+    if (endPoint === 'edit' && !location.state) {
+      this._routerControlCenter(parentPath);
+    }
+    else {
+      this.setState({ endPoint, parentPath });
+    }
+  }
+
+  componentDidMount() {
+    const { location } = this.props;
+
+    this._actionControlCenter(ACTION_MAP.itemItem, location.state);
+  }
+
+  componentWillUnmount(){
+    this._actionControlCenter(ACTION_MAP.itemClear);
+    this.setState({ endPoint: '', parentPath: '' });
+  }
+
+  handlePageCancel = () => {
+    routerControlCenter(ROUTER_MAP.itemList);
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+
+    const { endPoint } = this.state;
+    const { form, item, support: { supportItem } } = this.props;
+
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = { ...item.itemItem, ...fieldsValue};
+      if (endPoint === 'edit')
+      {
+        actionControlCenter(ACTION_MAP.itemItem, values);
+      }
+      else if (endPoint === 'add')
+      {
+        actionControlCenter(ACTION_MAP.itemItemAdd, values);
+      }
+      else
+      {
+        message.error('未知操作类型!', 5); //不可能走到这里
+      }
+      form.resetFields();
+      routerControlCenter(ROUTER_MAP.itemList);
+    });
+  }
+
+  handleTabChange = (key) => {
+    this.setState({ currentTab: key });
+  }
+
+  renderAboutList = (item) => {
+    const lessonColumns = [{
+      title: '课件编号',
+      dataIndex: 'code',
+    },{
+      title: '课件名称',
+      dataIndex: 'name',
+    },{
+      title: '课件封面',
+      dataIndex: 'url',
+    }];
+
+    const courseColumns = [{
+      title: '课编号',
+      dataIndex: 'code',
+    },{
+      title: '课名称',
+      dataIndex: 'name',
+    },{
+      title: '封面图片',
+      dataIndex: 'url',
+    }];
+
+    const supportColumns = [{
+      title: '周边编号',
+      dataIndex: 'code',
+    },{
+      title: '周边名称',
+      dataIndex: 'name',
+    },{
+      title: '封面图片',
+      dataIndex: 'url',
+    }];
+
+    switch (item.type) {
+      case 'LESSON':
+        return (
+          <Card title="课件列表" style={{ marginBottom: 24 }} bordered={true}>
+            <Table columns={lessonColumns} bordered={true} dataSource={item.subList || []}/>
+          </Card>
+        );
+      case 'COURSE':
+        return (
+          <Card title="课列表" style={{ marginBottom: 24 }} bordered={true}>
+            <Table columns={courseColumns} bordered={true} dataSource={item.subList || []}/>
+          </Card>
+        );
+      case 'SUPPORT':
+        return (
+          <Card title="周边列表" style={{ marginBottom: 24 }} bordered={true}>
+            <Table columns={supportColumns} bordered={true} dataSource={item.subList || []}/>
+          </Card>
+        );
+      default:
+        break;
+    }
+  }
+
+  render() {
+    const { cp, tag, item: { itemItem } } = this.props;
+    const { searchContent, currentTab } = this.state;
+    const { getFieldDecorator } = this.props.form;
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 7 },
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 12 },
+        md: { span: 12 },
+      },
+    };
+
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    const tabList = [{
+      key: 'basic',
+      tab: '产品信息',
+    },{
+      key: 'price',
+      tab: '产品定价',
+    }];
+
+    const aboutList = this.renderAboutList(itemItem);
+
+    return(
+      <PageHeaderLayout
+      >
+        <div className={styles.content}>
+          <Card title="基础信息" style={{ marginBottom: 24 }} bordered={true}>
+            <DescriptionList
+              style={{ marginBottom: 24 }}
+              col={1}
+              size="large"
+            >
+              <Description term="产品编号">{itemItem.code}</Description>
+              <Description term="产品名称">{itemItem.name}</Description>
+              <Description term="产品类型"></Description>
+              <Description term="产品标签"></Description>
+              <Description term="产品概要">{itemItem.digest}</Description>
+              <Description term="产品详情">{itemItem.digest}</Description>
+              <Description term="封面图片"></Description>
+            </DescriptionList>
+          </Card>
+          {aboutList}
+          <Card title="产品价格" style={{ marginBottom: 24 }} bordered={true}>
+            <EditableTable
+              dataSource={itemItem.priceList}
+              rowKeyName={"id"}
+            />
+          </Card>
+          <Button>取消</Button>
+          <Button style={{ marginLeft: 8 }} type="primary">提交</Button>
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 13 - 0
src/routes/Goods/ItemSave.less

@@ -0,0 +1,13 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 30px 15px 15px 15px;
+}
+
+:global {
+  .ant-table-pagination {
+    margin-top: 10px;
+  }
+}

+ 237 - 0
src/routes/Product/CourseList.js

@@ -0,0 +1,237 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { Link, routerRedux } from 'dva/router';
+import {
+  Popconfirm, Progress, notification, Badge, message, Card,
+  Icon, Button, Modal, Input, Select, Form, Row, Col
+} from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import styles from './CourseList.less';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const logger = Logger.getLogger('CourseList');
+
+const { Option } = Select;
+const FormItem = Form.Item;
+const ButtonGroup = Button.Group;
+
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  course: state.course,
+  cp: state.cp,
+}))
+@Form.create()
+export default class CourseList extends PureComponent {
+  state = {
+    selectedRows: [],
+    formValues: {},
+  }
+
+  componentDidMount() {
+    this.tableListRefresh();
+    this.props.dispatch({
+      type: 'cp/getCPList',
+      payload: {},
+    });
+  }
+
+  tableListRefresh(params = {}) {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'course/getCourseList',
+      payload: params,
+    });
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+
+    const { form } = this.props;
+
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = {
+        ...fieldsValue,
+      };
+      this.tableListRefresh(values);
+    });
+  }
+
+  handleReset = (e) => {
+    e.preventDefault();
+
+    const { form } = this.props;
+
+    form.resetFields();
+
+    this.tableListRefresh();
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  // handleItemAdd = () => {
+  //   const { dispatch } = this.props;
+  //   dispatch(routerRedux.push('/product/lesson/add'));
+  // }
+  //
+  // handleItemEdit = (record) => {
+  //   const { dispatch } = this.props;
+  //   dispatch(routerRedux.push({
+  //     pathname: '/product/lesson/edit',
+  //     state: { code: record.code },
+  //   }));
+  // }
+  //
+  // handleItemDel = (record) => {
+  //   const { dispatch } = this.props;
+  //   dispatch({
+  //     type: 'lesson/delLessonItem',
+  //     payload: { code: record.code },
+  //   });
+  //   dispatch({
+  //     type: 'lesson/getLessonList',
+  //     payload: {},
+  //   });
+  // }
+  //
+  //配置分页器
+  handleStandardTableChange = (pagination, filterArgs, sorter) => {
+    const { formValues } = this.state;
+
+    const filters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = {...obj};
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+    if (sorter.field) {
+      params.sorter = `${sorter.field}_${sorter.order}`;
+    }
+
+    this.tableListRefresh(params);
+  }
+
+  render() {
+    const { selectedRows } = this.state;
+
+    const { dispatch, course, cp: { cpList } } = this.props;
+
+    const { getFieldDecorator } = this.props.form;
+
+    const columns = [{
+      title: '课程编号',
+      dataIndex: 'code',
+    },{
+      title: '课程名称',
+      dataIndex: 'name',
+    },{
+      title: '供应商名称',
+      dataIndex: 'cpName',
+      filters: cpList.map(item => ({
+        text: item.cpName,
+        value: item.cpId,
+      })),
+    },{
+      title: '使用状态',
+      dataIndex: 'status',
+      filters: config.COURSE_STATE.map(item => ({
+        text: item.name,
+        value: item.value,
+      })),
+      render: (text, record) => (
+        <p>
+          {(config.COURSE_STATE.filter(item => item.value === record.status)[0] || { name: '未知' }).name}
+        </p>
+      ),
+    },{
+      title: '修改时间',
+      dataIndex: 'gmtModified',
+    },{
+      title: '操作类型',
+      render: (record) => (
+        <p>
+          <Button size="small" shape="circle" icon="edit" onClick={() => this.handleItemEdit(record)}></Button>
+          <span className={styles.splitLine} />
+          <Popconfirm
+            placement="top"
+            okText="删除"
+            cancelText="取消"
+            title={`确定删除${record.name}?`}
+            onConfirm={() => this.handleItemDel(record)}
+          >
+            <Button size="small" shape="circle" icon="delete"></Button>
+          </Popconfirm>
+        </p>
+      ),
+    }];
+
+    const standardTableProps = {
+      dataSource: course.data.list,
+      pagination: course.data.pagination,
+      loading: course.loading,
+      columns: columns,
+      rowKeyName: 'code',
+      selectedRows: selectedRows,
+    };
+
+    const pageHeaderSearch = (
+      <Form layout="inline" onSubmit={this.handleSearch}>
+        <Row gutter={24}>
+          <Col md={9} sm={24}>
+            <FormItem label="课程编号">
+              {getFieldDecorator('code')(
+                <Input placeholder="请输入课程编号" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={10} sm={24}>
+            <FormItem label="课程名称">
+              {getFieldDecorator('name')(
+                <Input placeholder="请输入课程名称" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={5} sm={24} push={1}>
+            <FormItem>
+              <ButtonGroup>
+                <Button icon="search" type="primary" htmlType="submit">搜索</Button>
+                <Button icon="reload" type="danger" onClick={this.handleReset}>重置</Button>
+              </ButtonGroup>
+            </FormItem>
+          </Col>
+        </Row>
+      </Form>
+    );
+
+    return (
+      <PageHeaderLayout
+        content={pageHeaderSearch}
+      >
+        <div className={styles.content}>
+          <div className={styles.newButton}>
+            <Button type="primary" icon="plus" onClick={() => {this.handleItemAdd()}}>新建课程</Button>
+          </div>
+          <StandardTable
+            {...standardTableProps}
+            onChange={this.handleStandardTableChange}
+            onSelectRow={this.handleSelectRows}
+          />
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 19 - 0
src/routes/Product/CourseList.less

@@ -0,0 +1,19 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 1px 15px 15px 15px;
+
+  .newButton {
+    margin: 10px 0 10px 0;
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 161 - 0
src/routes/Product/CourseSave.js

@@ -0,0 +1,161 @@
+import React, { PureComponent } from 'react';
+import { Row, Col, Form, Button, Input, Select } from 'antd';
+import { connect } from 'dva/router';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import ModalTable from '../../components/ModalSelectTable';
+import styles from './CourseSave.less';
+
+const FormItem = Form.Item;
+
+@Form.create()
+export default class CourseSave extends PureComponent {
+  state = {
+    endPoint: '',
+    parentPath: '',
+    modalVisible: false,
+  }
+
+  componentWillMount() {
+    const { dispatch, location } = this.props;
+    let pathSnippets = location.pathname.split('/');
+    const endPoint = pathSnippets.pop();
+    const parentPath = pathSnippets.join('/') || '/';
+    if (endPoint === 'edit' && !location.state) {
+      dispatch(routerRedux.push(parentPath));
+    }
+    else {
+      this.setState({ endPoint, parentPath });
+    }
+  }
+
+  componentDidMount() {
+    const { form, dispatch, location } = this.props;
+    const { endPoint, parentPath } = this.state;
+
+    if (endPoint !== 'add' && endPoint !== 'edit') {
+      dispatch(routerRedux.push(parentPath));
+      return;
+    }
+
+    if (endPoint === 'edit') {
+      dispatch({
+        type: 'lesson/getLessonOne',
+        payload: location.state,
+      });
+    }
+  }
+
+  render() {
+    const { getFieldDecorator } = this.props.form;
+
+    return (
+      <PageHeaderLayout>
+        <div className={styles.content}>
+          <Form onSubmit={this.handlePageSubmit}>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课程编号"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('code', {
+                      rules: [{ required: true, type: 'string', message: '课程编号必须填写!' }],
+                      initialValue: lessonItem.code,
+                  })(<Input placeholder="请输入课程编号" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课程名称"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('name', {
+                    rules: [{ required: true, type: 'string', message: '课程名称必须填写!' }],
+                    initialValue: lessonItem.name,
+                  })(<Input placeholder="请输入课程名称" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课程描述"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('digest', {
+                    initialValue: lessonItem.digest,
+                  })(<TextArea rows={4} placeholder="请添加课程描述" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="状态"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('status', {
+                    initialValue: lessonItem.status,
+                  })(
+                    <Select>
+                      {
+                        config.LESSON_STATE.map(item =>
+                          <Option key={item.value} value={item.value}>{item.name}</Option>
+                        )
+                      }
+                    </Select>
+                  )}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="选取课"
+                  {...formItemLayout}
+                >
+                  <Button
+                    icon="edit"
+                    type="primary"
+                    onClick={() => {this.handleEditWareBtnClick()}}
+                  >
+                    编辑课列表
+                  </Button>
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  {...submitFormLayout}
+                >
+                  <Table
+                    columns={tableColumns}
+                    dataSource={lessonItem.wareList}
+                    scroll={{ y: 200 }}
+                    pagination={{ pageSize: 50 }}
+                  >
+                  </Table>
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  {...submitFormLayout}
+                >
+                  <Button onClick={this.handlePageCancel}>取消</Button>
+                  <Button style={{ marginLeft: 8 }} type="primary" htmlType="submit">提交</Button>
+                </FormItem>
+              </Col>
+            </Row>
+          </Form>
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+
+}

+ 13 - 0
src/routes/Product/CourseSave.less

@@ -0,0 +1,13 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 30px 15px 15px 15px;
+}
+
+:global {
+  .ant-table-pagination {
+    margin-top: 10px;
+  }
+}

+ 224 - 0
src/routes/Product/LessonList.js

@@ -0,0 +1,224 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { Link, routerRedux } from 'dva/router';
+import { Popconfirm, Progress, notification, Badge, message, Card, Icon, Button,
+  Modal, Input, Select, Form, Row, Col, Tooltip } from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import styles from './LessonList.less';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const logger = Logger.getLogger('LessonList');
+
+const { Option } = Select;
+const FormItem = Form.Item;
+const ButtonGroup = Button.Group;
+
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  lesson: state.lesson,
+}))
+@Form.create()
+export default class LessonList extends PureComponent {
+  state = {
+    selectedRows: [],
+    formValues: {},
+  }
+
+  componentDidMount() {
+    const { dispatch, lesson: { data } } = this.props
+    dispatch({
+      type: 'lesson/getLessonList',
+      payload: {},
+    })
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = {
+        ...fieldsValue,
+      }
+      logger.info('【Search Values】: %o', values);
+      dispatch({
+        type: 'lesson/getLessonList',
+        payload: values,
+      });
+    });
+  }
+
+  handleReset = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.resetFields();
+    dispatch({
+      type: 'lesson/getLessonList',
+      payload: {},
+    });
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  handleItemAdd = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/lesson/add'));
+  }
+
+  handleItemEdit = (record) => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push({
+      pathname: '/product/lesson/edit',
+      state: { code: record.code },
+    }));
+  }
+
+  handleItemDel = (record) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'lesson/delLessonItem',
+      payload: { code: record.code },
+    });
+    dispatch({
+      type: 'lesson/getLessonList',
+      payload: {},
+    });
+  }
+
+  //配置分页器
+  handleStandardTableChange = (pagination, filterArgs, sorter) => {
+    const { dispatch } = this.props;
+    const { formValues } = this.state;
+
+    logger.info('【TableChangePagination】: %o', pagination);
+    const filters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = {...obj};
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+    if (sorter.field) {
+      params.sorter = `${sorter.field}_${sorter.order}`;
+    }
+
+    dispatch({
+      type: 'lesson/getLessonList',
+      payload: params,
+    })
+  }
+
+  render() {
+    const { selectedRows } = this.state;
+
+    const { dispatch, lesson } = this.props;
+
+    const { getFieldDecorator } = this.props.form;
+
+    const columns = [{
+      title: '课编号',
+      dataIndex: 'code',
+    },{
+      title: '课名称',
+      dataIndex: 'name',
+    },{
+      title: '使用状态',
+      dataIndex: 'status',
+      render: (text, record) => (
+        <p>
+          {(config.LESSON_STATE.filter(item => item.value === record.status)[0] || { name: '未知' }).name}
+        </p>
+      ),
+    },{
+      title: '修改时间',
+      dataIndex: 'gmtModified',
+      width: 200,
+    },{
+      title: '操作类型',
+      render: (record) => (
+        <p>
+          <Button size="small" shape="circle" icon="edit" onClick={() => this.handleItemEdit(record)}></Button>
+          <span className={styles.splitLine} />
+          <Popconfirm
+            placement="top"
+            okText="删除"
+            cancelText="取消"
+            title={`确定删除${record.name}?`}
+            onConfirm={() => this.handleItemDel(record)}
+          >
+            <Button size="small" shape="circle" icon="delete"></Button>
+          </Popconfirm>
+        </p>
+      ),
+      width: 100,
+    }];
+
+    const standardTableProps = {
+      dataSource: lesson.data.list,
+      pagination: lesson.data.pagination,
+      loading: lesson.loading,
+      columns: columns,
+      rowKeyName: 'code',
+      selectedRows: selectedRows,
+    };
+
+    const pageHeaderSearch = (
+      <Form layout="inline" onSubmit={this.handleSearch}>
+        <Row gutter={24}>
+          <Col md={9} sm={24}>
+            <FormItem label="课编号">
+              {getFieldDecorator('code')(
+                <Input placeholder="请输入课编号" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={10} sm={24}>
+            <FormItem label="课名称">
+              {getFieldDecorator('name')(
+                <Input placeholder="请输入课名称" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={5} sm={24} push={1}>
+            <FormItem>
+              <ButtonGroup>
+                <Button icon="search" type="primary" htmlType="submit">搜索</Button>
+                <Button icon="reload" type="danger" onClick={this.handleReset}>重置</Button>
+              </ButtonGroup>
+            </FormItem>
+          </Col>
+        </Row>
+      </Form>
+    );
+
+    return (
+      <PageHeaderLayout
+        content={pageHeaderSearch}
+      >
+        <div className={styles.content}>
+          <div className={styles.newButton}>
+            <Button type="primary" icon="plus" onClick={() => {this.handleItemAdd()}}>新建课</Button>
+          </div>
+          <StandardTable
+            {...standardTableProps}
+            onChange={this.handleStandardTableChange}
+            onSelectRow={this.handleSelectRows}
+          />
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 19 - 0
src/routes/Product/LessonList.less

@@ -0,0 +1,19 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 1px 15px 15px 15px;
+
+  .newButton {
+    margin: 10px 0 10px 0;
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 538 - 0
src/routes/Product/LessonSave.js

@@ -0,0 +1,538 @@
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { cloneDeep } from 'lodash';
+import React, { PureComponent } from 'react';
+import { message, Radio, Popover, Table, Row, Col, Card, Icon, Button, Form, Input, Select, Spin, Modal } from 'antd';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import styles from './LessonSave.less';
+
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const { Option } = Select;
+const { TextArea } = Input;
+const InputGroup = Input.Group;
+const ButtonGroup = Button.Group;
+const RadioGroup = Radio.Group;
+const RadioButton = Radio.Button;
+const FormItem = Form.Item;
+
+const logger = Logger.getLogger('WareEditPage');
+
+@connect(state => ({
+  ware: state.ware,
+  lesson: state.lesson,
+}))
+@Form.create()
+export default class LessonSave extends PureComponent {
+    state = {
+      modalVisible: false,
+      modalSearchContent: { key: 'name', value: '' },
+      wareSelectedArr: [],
+      tabSelected: 'WAIT',
+      endPoint: '',
+      parentPath: '',
+    }
+
+  componentWillMount() {
+    const { dispatch, location } = this.props;
+    let pathSnippets = location.pathname.split('/');
+    const endPoint = pathSnippets.pop();
+    const parentPath = pathSnippets.join('/') || '/';
+    if (endPoint === 'edit' && !location.state) {
+      dispatch(routerRedux.push(parentPath));
+    }
+    else {
+      this.setState({ endPoint, parentPath });
+    }
+  }
+
+  componentDidMount() {
+    const { form, dispatch, location } = this.props;
+    const { endPoint, parentPath } = this.state;
+
+    if (endPoint !== 'add' && endPoint !== 'edit') {
+      dispatch(routerRedux.push(parentPath));
+      return;
+    }
+
+    if (endPoint === 'edit') {
+      dispatch({
+        type: 'lesson/getLessonOne',
+        payload: location.state,
+      });
+    }
+  }
+
+  componentWillUnmount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'lesson/clearAll',
+    });
+    this.setState({
+      modalVisible: false,
+      modalSearchContent: { key: 'name', value: '' },
+      wareSelectedArr: [],
+      tabSelected: 'WAIT',
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/lesson'));
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+
+    const { endPoint } = this.state;
+    const { form, dispatch, lesson: { lessonItem } } = this.props;
+
+    form.validateFields((err) => {
+      if (err) return;
+
+      if (endPoint === 'edit')
+      {
+        dispatch({
+          type: 'lesson/updateLessonItem',
+          payload: lessonItem,
+        });
+      }
+      else if (endPoint === 'add')
+      {
+        dispatch({
+          type: 'lesson/addLessonItem',
+          payload: lessonItem,
+        });
+      }
+      else
+      {
+        message.error('未知操作类型!', 5);  //不可能走到这里
+      }
+
+      form.resetFields();
+      dispatch(routerRedux.push('/product/lesson'));
+    });
+  }
+
+  handleEditWareBtnClick = () => {
+    this.handleModalVisible(true);
+    this.props.dispatch({
+      type: 'ware/getWareList',
+      payload: {},
+    });
+  }
+
+  handleModalVisible = (flag) => {
+    this.setState({
+      modalVisible: !!flag
+    });
+  }
+
+  handleModalCancel = () => {
+    this.setState({
+      modalVisible: false,
+      modalSearchContent: { key: 'name', value: '' },
+      tabSelected: 'WAIT',
+    });
+  }
+
+  handleModalOk = () => {
+    this.setState({
+      modalVisible: false,
+      modalSearchContent: { key: 'name', value: '' },
+      tabSelected: 'WAIT',
+    });
+  }
+
+  handleModalSearchItemChange = (value) => {
+    this.setState({
+      modalSearchContent: { ...this.state.modalSearchContent, key: value },
+    });
+  }
+
+  handleModalInputValueChange = (e) => {
+    const value = e.target.value;
+    this.setState({
+      modalSearchContent: { ...this.state.modalSearchContent, value: value},
+    });
+  }
+
+  handleModalSearchBtnClick = () => {
+    const { dispatch } = this.props;
+
+    const { modalSearchContent: { key, value } } = this.state;
+
+    const payload = {};
+    payload[key] = value;
+
+    dispatch({
+      type: 'ware/getWareList',
+      payload: payload,
+    });
+  }
+
+  handleModalResetBtnClick = () => {
+    const { dispatch } = this.props;
+
+    this.setState({
+      modalSearchContent: { ...this.state.modalSearchContent, value: '' },
+    });
+
+    dispatch({
+      type: 'ware/getWareList',
+      payload: {},
+    });
+  }
+
+  handleModalTableChange = (pagination) => {
+    const { dispatch } = this.props;
+
+    const { modalSearchContent: { key, value } } = this.state;
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+    };
+
+    params[key] = value;
+
+    dispatch({
+      type: 'ware/getWareList',
+      payload: params,
+    })
+  }
+
+  handleModalTableAdd = (record) => {
+    const { dispatch, lesson: { lessonItem } } = this.props;
+    let wareList = lessonItem.wareList || [];
+    wareList.push(record);
+    dispatch({
+      type: 'lesson/saveItemData',
+      payload: {...lessonItem, wareList},
+    });
+  }
+
+  handleModalTabChange = (e) => {
+    this.setState({
+      tabSelected: e.target.value,
+    });
+  }
+
+  handleModalSelectedItemDel = (record) => {
+    const { dispatch, lesson: { lessonItem }} = this.props;
+    const  beforeDel = lessonItem.wareList || [];
+    const wareList = beforeDel.filter(item => item.id !== record.id);
+    dispatch({
+      type: 'lesson/saveItemData',
+      payload: {...lessonItem, wareList},
+    });
+  }
+
+  handleModalSort = (record, sort_up) => {
+    const { dispatch, lesson: { lessonItem } } = this.props;
+
+    const arr = cloneDeep(lessonItem.wareList);
+
+    const index = arr.findIndex(item => item.id === record.id);
+
+    if (sort_up)
+    {
+      //第一个元素或者未找到元素不做操作
+      if (!index || -1 === index) return;
+      //与前一个元素进行位置互换
+      arr.splice(index, 1, ...arr.splice(index - 1, 1, arr[index]));
+    }
+    else
+    {
+      //最后一个元素或者未找到元素不做操作
+      if (index + 1 === arr.length || -1 === index) return;
+      //与后一个元素进行位置互换
+      arr.splice(index, 1, ...arr.splice(index + 1, 1, arr[index]));
+    }
+
+    const wareList = arr;
+
+    dispatch({
+      type: 'lesson/saveItemData',
+      payload: {...lessonItem, wareList},
+    });
+  }
+
+  render() {
+    const { modalVisible, modalSearchContent, tabSelected } = this.state;
+
+    const { lesson: { lessonItem }, ware: { data: { list, pagination }, loading } } = this.props;
+
+    const { getFieldDecorator } = this.props.form;
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 7 },
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 12 },
+        md: { span: 10 },
+      },
+    };
+
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    const tableColumns = [{
+      title: '课件编号',
+      dataIndex: 'code',
+      width: '50%',
+    },{
+      title: '课件名称',
+      dataIndex: 'name',
+      width: '50%',
+    }];
+
+    const modalWaitSelectTableColumns = [{
+      title: '课件编号',
+      dataIndex: 'code',
+      width: '40%',
+    },{
+      title: '课件名称',
+      dataIndex: 'name',
+      width: '45%'
+    },{
+      title: '操作',
+      render: (text, record) => (
+        <Button
+          disabled={lessonItem.wareList.filter(item => item.id === record.id).length}
+          size="small"
+          type="dashed"
+          icon="plus"
+          onClick={() => {this.handleModalTableAdd(record)}}
+        >
+        </Button>
+      ),
+      width: '15%',
+    }];
+
+    const modalSelectedTableColumns = [{
+      title: '课件编号',
+      dataIndex: 'code',
+      width: '30%',
+    },{
+      title: '课件名称',
+      dataIndex: 'name',
+      width: '35%',
+    },{
+      title: '排序',
+      render: (text, record) => (
+        <ButtonGroup>
+          <Button size="small" icon="up" onClick={() => {this.handleModalSort(record, true)}}></Button>
+          <Button size="small" icon="down" onClick={() => {this.handleModalSort(record, false)}}></Button>
+        </ButtonGroup>
+      ),
+      width: '20%',
+    },{
+      title: '删除',
+      render: (record) => (
+        <Button size="small" icon="delete" onClick={() => this.handleModalSelectedItemDel(record)}></Button>
+      ),
+      width: '15%',
+    }];
+
+    return (
+      <PageHeaderLayout>
+        <div className={styles.content}>
+          <Form onSubmit={this.handlePageSubmit}>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课编号"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('code', {
+                      rules: [{ required: true, type: 'string', message: '课编号必须填写!' }],
+                      initialValue: lessonItem.code,
+                  })(<Input placeholder="请输入课编号" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课名称"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('name', {
+                    rules: [{ required: true, type: 'string', message: '课名称必须填写!' }],
+                    initialValue: lessonItem.name,
+                  })(<Input placeholder="请输入课名称" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课描述"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('digest', {
+                    initialValue: lessonItem.digest,
+                  })(<TextArea rows={4} placeholder="请添加课描述" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="状态"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('status', {
+                    initialValue: lessonItem.status,
+                  })(
+                    <Select>
+                      {
+                        config.LESSON_STATE.map(item =>
+                          <Option key={item.value} value={item.value}>{item.name}</Option>
+                        )
+                      }
+                    </Select>
+                  )}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="选取课件"
+                  {...formItemLayout}
+                >
+                  <Button
+                    icon="edit"
+                    type="primary"
+                    onClick={() => {this.handleEditWareBtnClick()}}
+                  >
+                    编辑课件
+                  </Button>
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  {...submitFormLayout}
+                >
+                  <Table
+                    columns={tableColumns}
+                    dataSource={lessonItem.wareList}
+                    scroll={{ y: 200 }}
+                    pagination={{ pageSize: 50 }}
+                  >
+                  </Table>
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  {...submitFormLayout}
+                >
+                  <Button onClick={this.handlePageCancel}>取消</Button>
+                  <Button style={{ marginLeft: 8 }} type="primary" htmlType="submit">提交</Button>
+                </FormItem>
+              </Col>
+            </Row>
+          </Form>
+        </div>
+        {modalVisible ? (
+          <Modal
+            title="选择课件"
+            width={600}
+            maskClosable={false}
+            visible={modalVisible}
+            onOk={this.handleModalOk}
+            onCancel={this.handleModalCancel}
+          >
+            <Row gutter={24}>
+              <Col md={20}>
+                <InputGroup compact>
+                  <Select
+                    defaultValue="name"
+                    style={{ width: '25%' }}
+                    onChange={this.handleModalSearchItemChange}
+                  >
+                    <Option value="name">课件名称</Option>
+                    <Option value="code">课件编号</Option>
+                  </Select>
+                  <Input
+                    placeholder="请输入搜索内容"
+                    value={modalSearchContent.value}
+                    onChange={this.handleModalInputValueChange}
+                    onPressEnter={this.handleModalSearchBtnClick}
+                    style={{ width: '75%' }}
+                  />
+                </InputGroup>
+              </Col>
+              <Col md={2}>
+                <Button
+                  type="primary"
+                  icon="search"
+                  shape="circle"
+                  onClick={this.handleModalSearchBtnClick}
+                >
+                </Button>
+              </Col>
+              <Col md={2}>
+                <Button
+                  icon="reload"
+                  shape="circle"
+                  onClick={this.handleModalResetBtnClick}
+                >
+                </Button>
+              </Col>
+            </Row>
+            <Row>
+              <RadioGroup size="small" defaultValue="WAIT" onChange={this.handleModalTabChange} style={{ marginTop: 10, marginBottom: 10 }}>
+                <RadioButton value="WAIT">待选</RadioButton>
+                <RadioButton value="SELECTED">{`已选【${lessonItem.wareList.length}】`}</RadioButton>
+              </RadioGroup>
+            </Row>
+            <Row>
+              <Col>
+                <Table
+                  columns={modalWaitSelectTableColumns}
+                  rowKey={record => record.id}
+                  dataSource={list}
+                  loading={loading}
+                  onChange={this.handleModalTableChange}
+                  pagination={pagination}
+                  scroll={{ y: 200 }}
+                  style={tabSelected === 'WAIT' ? null : { display: 'none' }}
+                >
+                </Table>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <Table
+                  style={tabSelected === 'SELECTED' ? null : { display: 'none' }}
+                  columns={modalSelectedTableColumns}
+                  rowKey={record => record.id}
+                  dataSource={lessonItem.wareList}
+                  pagination={{ pageSize: 50 }}
+                  scroll={{ y: 200 }}
+                >
+                </Table>
+              </Col>
+            </Row>
+          </Modal>
+          ) : null
+        }
+      </PageHeaderLayout>
+    )
+  }
+}

+ 13 - 0
src/routes/Product/LessonSave.less

@@ -0,0 +1,13 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 30px 15px 15px 15px;
+}
+
+:global {
+  .ant-table-pagination {
+    margin-top: 10px;
+  }
+}

+ 217 - 0
src/routes/Product/SupportList.js

@@ -0,0 +1,217 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { Link, routerRedux } from 'dva/router';
+import {
+  Popconfirm, Progress, notification, Badge, message, Card,
+  Icon, Button, Modal, Input, Select, Form, Row, Col
+} from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import styles from './SupportList.less';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const { Option } = Select;
+const FormItem = Form.Item;
+const ButtonGroup = Button.Group;
+
+const logger = Logger.getLogger('SupportList');
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  support: state.support,
+  cp: state.cp,
+}))
+@Form.create()
+export default class SupportList extends PureComponent {
+  state = {
+    selectedRows: [],   //记录选中的行
+    formValues: {},     //记录搜索表单值
+  };
+
+  _actionControlCenter(type, payload = {}) {
+    const { dispatch } = this.props;
+    //对空字段进行过滤
+    Object.keys(payload).map(key => {payload[key] ? null : delete payload[key]});
+    dispatch({ type, payload });
+  }
+
+  componentDidMount() {
+    this._actionControlCenter('support/getSupportList');
+    this._actionControlCenter('cp/getCPList');
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+    const { form } = this.props;
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = {
+        ...fieldsValue,
+      };
+      this.setState({
+        formValues: {...fieldsValue},
+      });
+      this._actionControlCenter('support/getSupportList', values);
+    });
+  }
+
+  handleReset = (e) => {
+    e.preventDefault();
+    const { form } = this.props;
+    form.resetFields();
+    this.setState({
+      formValues: {},
+    });
+    this._actionControlCenter('support/getSupportList');
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  handleItemAdd = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/support/add'));
+  }
+
+  handleItemEdit = (record) => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push({
+      pathname: '/product/support/edit',
+      state: { code: record.code },       //进入编辑页面传递code参数
+    }));
+  }
+
+  handleItemDel = (record) => {
+    this._actionControlCenter('support/delSupportItem', { code: record.code });
+    this._actionControlCenter('support/getSupportList');
+  }
+
+  //配置分页器
+  handleStandardTableChange = (pagination, filterArgs, sorter) => {
+    const { formValues } = this.state;
+    const filters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = {...obj};
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+    this._actionControlCenter('support/getSupportList', params);
+  }
+
+  render() {
+    const { selectedRows } = this.state;
+    const { dispatch, support, cp } = this.props;
+    const { getFieldDecorator } = this.props.form;
+
+    const columns = [{
+      title: '周边编号',
+      dataIndex: 'code',
+    },{
+      title: '周边名称',
+      dataIndex: 'name',
+    },{
+      title: '供应商名称',
+      dataIndex: 'cpName',
+      filters: cp.cpList.map(item => ({
+        text: item.cpName,
+        value: item.cpId,
+      })),
+    },{
+      title: '标签名称',
+      dataIndex: 'tags',
+      render: (text, record) => (
+        <p>{record.tags.map(item => item.tagName).join(',')}</p>
+      ),
+    },{
+      title: '使用状态',
+      dataIndex: 'status',
+      filters: config.SUPPORT_STATE.map(item => ({
+        text: item.name,
+        value: item.value,
+      })),
+      render: (text, record) => (
+        <p>
+          {(config.SUPPORT_STATE.filter(item => item.value === record.status)[0] || { name: '未知' }).name}
+        </p>
+      ),
+    },{
+      title: '修改时间',
+      dataIndex: 'gmtModified',
+    },{
+      title: '操作类型',
+      render: (record) => (
+        <p>
+          <Button size="small" shape="circle" icon="edit" onClick={() => this.handleItemEdit(record)}></Button>
+          <span className={styles.splitLine} />
+          <Popconfirm
+            placement="top"
+            okText="删除"
+            cancelText="取消"
+            title='确定删除?'
+            onConfirm={() => this.handleItemDel(record)}
+          >
+            <Button size="small" shape="circle" icon="delete"></Button>
+          </Popconfirm>
+        </p>
+      ),
+    }];
+
+    const standardTableProps = {
+      dataSource: support.data.list,
+      pagination: support.data.pagination,
+      loading: support.loading,
+      columns: columns,
+      rowKeyName: 'code',
+      selectedRows: selectedRows,
+    };
+
+    const pageHeaderSearch = (
+      <Form layout="inline" onSubmit={this.handleSearch}>
+        <FormItem label="周边编号">
+          {getFieldDecorator('code')(
+            <Input placeholder="请输入周边编号" />
+          )}
+        </FormItem>
+        <FormItem label="周边名称">
+          {getFieldDecorator('name')(
+            <Input placeholder="请输入周边名称" />
+          )}
+        </FormItem>
+        <FormItem>
+          <ButtonGroup>
+            <Button icon="search" type="primary" htmlType="submit">搜索</Button>
+            <Button icon="reload" type="danger" onClick={this.handleReset}>重置</Button>
+          </ButtonGroup>
+        </FormItem>
+      </Form>
+    );
+
+    return (
+      <PageHeaderLayout
+        content={pageHeaderSearch}
+      >
+        <div className={styles.content}>
+          <div className={styles.newButton}>
+            <Button type="primary" icon="plus" onClick={() => {this.handleItemAdd()}}>添加</Button>
+          </div>
+          <StandardTable
+            {...standardTableProps}
+            onChange={this.handleStandardTableChange}
+            onSelectRow={this.handleSelectRows}
+          />
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 19 - 0
src/routes/Product/SupportList.less

@@ -0,0 +1,19 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 1px 15px 15px 15px;
+
+  .newButton {
+    margin: 10px 0 10px 0;
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 378 - 0
src/routes/Product/SupportSave.js

@@ -0,0 +1,378 @@
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import React, { PureComponent } from 'react';
+import { Form, Input, Select, Card, Button, Table } from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import ModalSelectTable from '../../components/ModalSelectTable';
+import EditableTable from '../../components/EditableTable';
+import styles from './SupportSave.less';
+import { getParentPath } from '../../utils/utils';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const FormItem = Form.Item;
+const InputGroup = Input.Group;
+const { TextArea } = Input;
+const { Option } = Select;
+
+const { ROUTER_MAP, ACTION_MAP } = config;
+const logger = Logger.getLogger('SupportSave');
+
+@connect(state => ({
+  cp: state.cp,
+  tag: state.tag,
+  item: state.item,
+  support: state.support,
+  merchant: state.merchant,
+}))
+@Form.create()
+export default class SupportSave extends PureComponent {
+  state = {
+    endPoint: '',          //操作类型,add或edit
+    parentPath: '',        //路径前缀
+    searchSelect: 'name',  //模态框的搜索字段
+    searchInput: '',       //模态框的搜索字段内容
+  };
+
+  _actionControlCenter = (type, payload = {}) => {
+    const { dispatch } = this.props;
+    //对空字段进行过滤
+    Object.keys(payload).map(key => {payload[key] ? null : delete payload[key]});
+    //触发modal中action
+    dispatch({ type, payload });
+  }
+
+  _routerControlCenter = (router = '/', payload = null) => {
+    const { dispatch } = this.props;
+    if (payload) {
+      dispatch(routerRedux.push({ pathname: router, state: payload }));
+    }
+    else {
+      dispatch(routerRedux.push(router));
+    }
+  }
+
+  componentWillMount() {
+    const { location } = this.props;
+    const { endPoint, parentPath } = getParentPath(location);
+    if (endPoint === 'edit' && !location.state) {
+      this._routerControlCenter(parentPath);
+    }
+    else {
+      this.setState({ endPoint, parentPath });
+    }
+  }
+
+  componentDidMount() {
+    const { location } = this.props;
+    const { endPoint, parentPath } = this.state;
+
+    if (endPoint !== 'add' && endPoint !== 'edit') {
+      this._routerControlCenter(parentPath);
+    }
+
+    //如果是编辑选项,则请求编辑项的相关周边信息/相关商品信息
+    if (endPoint === 'edit') {
+      this._actionControlCenter(ACTION_MAP.supportItem, location.state);
+      this._actionControlCenter(ACTION_MAP.itemItem, {} ); //TODO
+    }
+    this._actionControlCenter(ACTION_MAP.supportList);
+    this._actionControlCenter(ACTION_MAP.merchantList);
+    this._actionControlCenter(ACTION_MAP.tagList);
+    this._actionControlCenter(ACTION_MAP.cpList);
+  }
+
+  componentWillUnmount(){
+    this._actionControlCenter(ACTION_MAP.supportClear);
+    this.setState({ endPoint: '', parentPath: '' });
+  }
+
+  //点击编辑周边按钮,出现模态框
+  handleEditSupportBtnClick = () => {
+    this.refs.getSupportModalSelectTable.showModal();
+  }
+
+  //模态框中的待选列表翻页动作
+  handleToBeSelectedTableChange = (pagination) => {
+    const { searchSelect, searchInput } = this.state;
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+    };
+    params[searchSelect] = searchInput;
+
+    this._actionControlCenter(ACTION_MAP.supportList, params);
+  }
+
+  //相关周边列表点击搜索按钮进行查询
+  handleSearchBtnClick = () => {
+    const { searchSelect, searchInput } = this.state;
+    let searchContent = {};
+    searchContent[searchSelect] = searchInput;
+    this._actionControlCenter(ACTION_MAP.supportList, {...searchContent});
+  }
+
+  //模态框中的重置按钮处理动作
+  handleSearchResetClick = () => {
+    this.setState({ searchSelect: 'name', searchInput: '' });
+    this._actionControlCenter(ACTION_MAP.supportList);
+  }
+
+  //模态框中的搜索字段改变触发
+  handleSearchSelectChange = (value) => {
+    this.setState({ searchSelect: value });
+  }
+
+  //模态框中的搜索内容输入框变化触发
+  handleSearchInputChange = (e) => {
+    this.setState({ searchInput: e.target.value });
+  }
+
+  //页面取消按钮触发
+  handlePageCancel = () => {
+    this._routerControlCenter(ROUTER_MAP.supportList);
+  }
+
+  //页面提交按钮触发
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+
+    const { endPoint } = this.state;
+    const { form, dispatch, support: { supportItem } } = this.props;
+
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = { ...supportItem, ...fieldsValue};
+      if (endPoint === 'edit')
+      {
+        this._actionControlCenter(ACTION_MAP.supportItemUpdate, values);
+      }
+      else if (endPoint === 'add')
+      {
+        this._actionControlCenter(ACTION_MAP.supportItemAdd, values);
+      }
+      else
+      {
+        message.error('未知操作类型!', 5);  //不可能走到这里
+      }
+
+      form.resetFields();
+      this._routerControlCenter(ROUTER_MAP.supportList);
+    });
+  }
+
+  //模态框提交按钮触发
+  handleModalSubmit = (selectedDataStorage) => {
+    const { support: { supportItem } } = this.props;
+    this._actionControlCenter(
+      ACTION_MAP.supportSaveItemData,
+      {...supportItem, supportArr: selectedDataStorage}
+    );
+  }
+
+  render() {
+    const { cp, tag, item, merchant, support: { supportItem, loading, data: { list, pagination } } } = this.props;
+    const { searchContent } = this.state;
+    const { getFieldDecorator } = this.props.form;
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 7 },
+        md: { span: 6 },
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 12 },
+        md: { span: 13 },
+      },
+    };
+
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    const baseColumnSet = [{
+      title: '周边编号',
+      dataIndex: 'code',
+      width: 150,
+    },{
+      title: '周边名称',
+      dataIndex: 'name',
+      width: 150,
+    }];
+
+    return(
+      <PageHeaderLayout>
+        <div className={styles.content}>
+          <Card title="基本信息" bordered={true} style={{ marginBottom: 24 }}>
+            <Form onSubmit={this.handlePageSubmit}>
+              <FormItem
+                label="周边编号"
+                {...formItemLayout}
+              >
+                {getFieldDecorator('code', {
+                    rules: [{ required: true, type: 'string', message: '周边编号必须填写!' }],
+                    initialValue: supportItem.code,
+                })(<Input placeholder="请输入周边编号" />)}
+              </FormItem>
+              <FormItem
+                label="周边名称"
+                {...formItemLayout}
+              >
+                {getFieldDecorator('name', {
+                  rules: [{ required: true, type: 'string', message: '周边名称必须填写!' }],
+                  initialValue: supportItem.name,
+                })(<Input placeholder="请输入周边名称" />)}
+              </FormItem>
+              <FormItem
+                label="周边描述"
+                {...formItemLayout}
+              >
+                {getFieldDecorator('digest', {
+                  initialValue: supportItem.digest,
+                })(<TextArea rows={4} placeholder="请添加周边描述" />)}
+              </FormItem>
+              <FormItem
+                label="状态"
+                {...formItemLayout}
+              >
+                {getFieldDecorator('status', {
+                  initialValue: supportItem.status,
+                })(
+                  <Select>
+                    {
+                      config.SUPPORT_STATE.map(item =>
+                        <Option key={item.value} value={item.value}>{item.name}</Option>
+                      )
+                    }
+                  </Select>
+                )}
+              </FormItem>
+              <FormItem
+                label="标签"
+                {...formItemLayout}
+              >
+                {getFieldDecorator('tags', {
+                    initialValue: supportItem.tags.map(item => item.tagId),
+                })(
+                  <Select
+                    mode="multiple"
+                    placeholder="请选择标签"
+                    notFoundContent="标签数据加载失败"
+                  >
+                    {tag.data.list.map(item =>
+                      <Option key={item.tagId} value={item.tagId}>{item.tagName}</Option>
+                    )}
+                  </Select>
+                )}
+              </FormItem>
+              <FormItem
+                label="内容提供商"
+                {...formItemLayout}
+              >
+                {getFieldDecorator('cpId', {
+                    initialValue: supportItem.cpId,
+                })(
+                  <Select
+                    placeholder="请选择内容提供商"
+                    notFoundContent="内容提供商信息加载失败"
+                  >
+                    {cp.cpList.map(item =>
+                      <Option key={item.cpId} value={item.cpId}>{item.cpName}</Option>
+                    )}
+                  </Select>
+                )}
+              </FormItem>
+              <FormItem
+                label="周边相关"
+                {...formItemLayout}
+              >
+                <Button
+                  icon="edit"
+                  type="primary"
+                  onClick={() => {this.handleEditSupportBtnClick()}}
+                >
+                  编辑周边
+                </Button>
+              </FormItem>
+              <FormItem
+                label="相关周边"
+                {...formItemLayout}
+              >
+                <Table
+                  size="middle"
+                  bordered={true}
+                  dataSource={supportItem.supportArr}
+                  columns={baseColumnSet}
+                  rowKey={record => record.code}
+                  pagination={{ pageSize: 20 }}
+                  scroll={{ y: 200 }}
+                />
+              </FormItem>
+              <ModalSelectTable
+                ref="getSupportModalSelectTable"
+                modalTitle="相关周边"
+                dataSource={list}
+                baseColumnSet={baseColumnSet}
+                selectedDataSource={supportItem.supportArr}
+                selectSort={true}
+                selectMode='multiple'
+                pagination={pagination}
+                rowKeyName="code"
+                loading={loading}
+                onChange={this.handleToBeSelectedTableChange}
+                onSubmit={this.handleModalSubmit}
+              >
+                <div>
+                  <InputGroup compact>
+                    <Select
+                      onChange={this.handleSearchSelectChange}
+                      defaultValue="name"
+                      style={{ width: '27%' }}
+                    >
+                      <Option value="name">周边配套名称</Option>
+                      <Option value="code">周边配套编号</Option>
+                    </Select>
+                    <Input
+                      onChange={this.handleSearchInputChange}
+                      onPressEnter={this.handleSearchBtnClick}
+                      placeholder="请输入搜索内容"
+                      style={{ width: '45%' }}
+                    />
+                    <Button
+                      type="primary"
+                      style={{ width: '14%' }}
+                      onClick={this.handleSearchBtnClick}
+                    >搜索
+                    </Button>
+                    <Button
+                      style={{ width: '14%' }}
+                      onClick={this.handleSearchResetClick}
+                    >重置
+                    </Button>
+                  </InputGroup>
+                </div>
+              </ModalSelectTable>
+            </Form>
+          </Card>
+          <Card title="价格信息" style={{ marginBottom: 24 }} bordered={true}>
+            <EditableTable
+              dataSource={item.itemItem.priceList}
+              merchants={merchant.merchantList}
+              rowKeyName={'id'}
+            />
+          </Card>
+          <Card bordered={false}>
+            <Button onClick={this.handlePageCancel}>取消</Button>
+            <Button style={{ marginLeft: 8 }} type="primary" onClick={(e) => this.handlePageSubmit(e)}>提交</Button>
+          </Card>
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 13 - 0
src/routes/Product/SupportSave.less

@@ -0,0 +1,13 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 30px 15px 15px 15px;
+}
+
+:global {
+  .ant-table-pagination {
+    margin-top: 10px;
+  }
+}

+ 217 - 0
src/routes/Product/TagList.js

@@ -0,0 +1,217 @@
+import React, { PureComponent } from 'react';
+import { routerRedux } from 'dva/router';
+import { connect } from 'dva';
+import { Popconfirm, Button, Input, Select, Form } from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import styles from './TagList.less';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const logger = Logger.getLogger('TagRoute');
+
+const { Option } = Select;
+const FormItem = Form.Item;
+const ButtonGroup = Button.Group;
+
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  tag: state.tag,
+  merchant: state.merchant,
+}))
+@Form.create()
+export default class TagList extends PureComponent {
+  state = {
+    modalVisible: false,
+    selectedRows: [],
+    formValues: {},
+  };
+
+  _actionControlCenter(type, payload = {}) {
+    const { dispatch } = this.props;
+    //对空字段进行过滤
+    Object.keys(payload).map(key => {payload[key] ? null : delete payload[key]});
+    dispatch({ type, payload });
+  }
+
+  componentDidMount() {
+    this._actionControlCenter('merchant/getAllMerchants');
+    this._actionControlCenter('tag/getTagList');
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = {
+        ...fieldsValue,
+      }
+      this._actionControlCenter('tag/getTagList', values);
+    });
+  }
+
+  handleReset = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.resetFields();
+    this._actionControlCenter('tag/getTagList');
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  handleItemAdd = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/tag/add'));
+  }
+
+  handleItemEdit = (record) => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/tag/edit', { tagId: record.tagId }));
+  }
+
+  handleItemDel = (record) => {
+    this._actionControlCenter('tag/delTagItem', { tagId: record.tagId });
+    this._actionControlCenter('tag/getTagList');
+  }
+
+  //配置分页器
+  handleStandardTableChange = (pagination, filterArgs, sorter) => {
+    const { dispatch } = this.props;
+    const { formValues } = this.state;
+
+    const filters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = {...obj};
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+
+    this._actionControlCenter('tag/getTagList', params);
+  }
+
+  render() {
+    const { selectedRows, modalVisible } = this.state;
+    const { dispatch, tag, merchant } = this.props;
+    const { getFieldDecorator } = this.props.form;
+
+    const columns = [{
+      title: '标签名称',
+      dataIndex: 'tagName',
+    },{
+      title: '标签类型',
+      dataIndex: 'tagType',
+      filters: config.TAG_TYPE.map((item) => ({
+        text: item.name,
+        value: item.value,
+      }))
+    },{
+      title: '渠道平台',
+      dataIndex: 'merchantName',
+      filters: merchant.merchantList.map((item) => ({
+        text: item.merchantName,
+        value: item.merchantId,
+      }))
+    },{
+      title: '修改时间',
+      dataIndex: 'gmtModified',
+    },{
+      title: '操作类型',
+      render: (record) => (
+        <p>
+          <Button size="small" shape="circle" icon="edit" onClick={() => this.handleItemEdit(record)}></Button>
+          <span className={styles.splitLine} />
+          <Popconfirm
+            placement="top"
+            okText="删除"
+            cancelText="取消"
+            title='确定删除?'
+            onConfirm={() => this.handleItemDel(record)}
+          >
+            <Button size="small" shape="circle" icon="delete"></Button>
+          </Popconfirm>
+        </p>
+      ),
+    }];
+
+    const standardTableProps = {
+      dataSource: tag.data.list,
+      pagination: tag.data.pagination,
+      loading: tag.loading,
+      columns: columns,
+      rowKeyName: 'tagId',
+      selectedRows: selectedRows,
+    };
+
+    const pageHeaderSearch = (
+      <Form
+        layout="inline"
+        onSubmit={this.handleSearch}
+        ref="searchForm"
+      >
+        <FormItem label="标签名称">
+          {getFieldDecorator('tagName')(
+            <Input placeholder="请输入标签名称" />
+          )}
+        </FormItem>
+        <FormItem label="标签类型">
+          {getFieldDecorator('tagType')(
+            <Select style={{ width: 140 }}>
+              {
+                config.TAG_TYPE.map(item =>
+                  <Option key={item.value} value={item.value}>{item.name}</Option>
+                )
+              }
+            </Select>
+          )}
+        </FormItem>
+        <FormItem label="渠道平台">
+          {getFieldDecorator('merchantId')(
+            <Select style={{ width: 140 }}>
+              {
+                merchant.merchantList.map(item =>
+                  <Option key={item.merchantId} value={item.merchantId}>{item.merchantName}</Option>
+                )
+              }
+            </Select>
+          )}
+        </FormItem>
+        <FormItem>
+          <ButtonGroup>
+            <Button icon="search" type="primary" htmlType="submit">搜索</Button>
+            <Button icon="reload" type="danger" onClick={this.handleReset}>重置</Button>
+          </ButtonGroup>
+        </FormItem>
+      </Form>
+    );
+
+    return (
+      <PageHeaderLayout
+        content={pageHeaderSearch}
+      >
+        <div className={styles.content}>
+          <div className={styles.newButton}>
+            <Button type="primary" icon="plus" onClick={() => {this.handleItemAdd()}}>添加</Button>
+          </div>
+          <StandardTable
+            {...standardTableProps}
+            onChange={this.handleStandardTableChange}
+            onSelectRow={this.handleSelectRows}
+          />
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 25 - 0
src/routes/Product/TagList.less

@@ -0,0 +1,25 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 1px 15px 15px 15px;
+
+  .newButton {
+    margin: 10px 0 10px 0;
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}
+
+.notification {
+  width: 300px;
+  margin-right: 0;
+  border-radius: 10px;
+}

+ 188 - 0
src/routes/Product/TagSave.js

@@ -0,0 +1,188 @@
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import React, { PureComponent } from 'react';
+import { Form, Input, Select, Button, Table } from 'antd';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import styles from './TagSave.less';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const FormItem = Form.Item;
+const InputGroup = Input.Group;
+const { TextArea } = Input;
+const { Option } = Select;
+
+const logger = Logger.getLogger('TagSave');
+
+@connect(state => ({
+  tag: state.tag,
+  merchant: state.merchant,
+}))
+@Form.create()
+export default class TagSave extends PureComponent {
+  state = {
+    endPoint: '',          //操作类型,add或edit
+    parentPath: '',        //路径前缀
+    searchInput: '',       //模态框的搜索字段内容
+  };
+
+  _actionControlCenter(type, payload = {}) {
+    const { dispatch } = this.props;
+    //对空字段进行过滤
+    Object.keys(payload).map(key => {payload[key] ? null : delete payload[key]});
+    dispatch({ type, payload });
+  }
+
+  componentWillMount() {
+    const { dispatch, location } = this.props;
+    let pathSnippets = location.pathname.split('/');
+    const endPoint = pathSnippets.pop();
+    const parentPath = pathSnippets.join('/') || '/';
+    if (endPoint === 'edit' && !location.state) {
+      dispatch(routerRedux.push(parentPath));
+    }
+    else {
+      this.setState({ endPoint, parentPath });
+    }
+  }
+
+  componentDidMount() {
+    const { form, dispatch, location } = this.props;
+    const { endPoint, parentPath } = this.state;
+
+    if (endPoint !== 'add' && endPoint !== 'edit') {
+      dispatch(routerRedux.push(parentPath));
+      return;
+    }
+
+    if (endPoint === 'edit') {
+      this._actionControlCenter('tag/getTagOne', location.state);
+    }
+
+    this._actionControlCenter('merchant/getAllMerchants');
+  }
+
+  componentWillUnmount(){
+    this._actionControlCenter('tag/clearAll');
+    this.setState({
+      endPoint: '',
+      parentPath: '',
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/tag'));
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+
+    const { endPoint } = this.state;
+    const { form, dispatch, tag: { tagItem } } = this.props;
+
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+
+      const values = { ...tagItem, ...fieldsValue};
+
+      if (endPoint === 'edit')
+      {
+        this._actionControlCenter('tag/updateTagItem', values);
+      }
+      else if (endPoint === 'add')
+      {
+        this._actionControlCenter('tag/addTagItem', values);
+      }
+      else
+      {
+        message.error('未知操作类型!', 5);  //不可能走到这里
+      }
+
+      form.resetFields();
+      dispatch(routerRedux.push('/product/tag'));
+    });
+  }
+
+  render() {
+    const { tag: { tagItem }, merchant } = this.props;
+    const { getFieldDecorator } = this.props.form;
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 7 },
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 12 },
+        md: { span: 10 },
+      },
+    };
+
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    return(
+      <PageHeaderLayout>
+        <div className={styles.content}>
+          <Form onSubmit={this.handlePageSubmit}>
+            <FormItem
+              label="标签名称"
+              {...formItemLayout}
+            >
+              {getFieldDecorator('tagName', {
+                rules: [{ required: true, type: 'string', message: '标签名称必须填写!' }],
+                initialValue: tagItem.tagName,
+              })(<Input placeholder="请输入标签名称" />)}
+            </FormItem>
+            <FormItem
+              label="标签类型"
+              {...formItemLayout}
+            >
+              {getFieldDecorator('type', {
+                  initialValue: tagItem.type,
+              })(
+                <Select
+                  placeholder="请选择标签类型"
+                >
+                  {config.TAG_TYPE.map(item =>
+                    <Option key={item.value} value={item.value}>{item.name}</Option>
+                  )}
+                </Select>
+              )}
+            </FormItem>
+            <FormItem
+              label="所属渠道方"
+              {...formItemLayout}
+            >
+              {getFieldDecorator('merchantId', {
+                  initialValue: tagItem.merchantId,
+              })(
+                <Select
+                  placeholder="请选择所属渠道方"
+                  notFoundContent="渠道方数据加载失败"
+                >
+                  {merchant.merchantList.map(item =>
+                    <Option key={item.merchantId} value={item.merchantId}>{item.merchantName}</Option>
+                  )}
+                </Select>
+              )}
+            </FormItem>
+            <FormItem
+              {...submitFormLayout}
+            >
+              <Button onClick={this.handlePageCancel}>取消</Button>
+              <Button style={{ marginLeft: 8 }} type="primary" htmlType="submit">提交</Button>
+            </FormItem>
+          </Form>
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 7 - 0
src/routes/Product/TagSave.less

@@ -0,0 +1,7 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 30px 15px 15px 15px;
+}

+ 262 - 0
src/routes/Product/WareList.js

@@ -0,0 +1,262 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { Link, routerRedux } from 'dva/router';
+import { Popconfirm, Progress, notification, Badge, message, Card, Icon, Button,
+  Modal, Input, Select, Form, Row, Col, Tooltip } from 'antd';
+import moment from 'moment';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import styles from './WareList.less';
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const logger = Logger.getLogger('WareList');
+
+const { Option } = Select;
+const FormItem = Form.Item;
+const ButtonGroup = Button.Group;
+
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  ware: state.ware,
+  cp: state.cp,
+}))
+@Form.create()
+export default class WareList extends PureComponent {
+  state = {
+    modalVisible: false,
+    selectedRows: [],
+    formValues: {},
+  }
+
+  componentDidMount() {
+    const { dispatch, ware: { data } } = this.props
+    dispatch({
+      type: 'cp/getCPList',
+      payload: {},
+    });
+    dispatch({
+      type: 'ware/getWareList',
+      payload: {},
+    })
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = {
+        ...fieldsValue,
+      }
+      logger.info('【Search Values】: %o', values);
+      dispatch({
+        type: 'ware/getWareList',
+        payload: values,
+      });
+    });
+  }
+
+  handleReset = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.resetFields();
+    dispatch({
+      type: 'ware/getWareList',
+      payload: {},
+    });
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  handleItemAdd = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/ware/save'));
+  }
+
+  handleItemEdit = (record) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'ware/saveItemData',
+      payload: record,
+    });
+    dispatch(routerRedux.push('/product/ware/save'));
+  }
+
+  handleItemDel = (record) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'ware/delWareItem',
+      payload: { code: record.code },
+    });
+    dispatch({
+      type: 'ware/getWareList',
+      payload: {},
+    });
+  }
+
+  handleModalVisible = (flag) => {
+    this.setState({
+      modalVisible: !!flag,
+    });
+  }
+
+  //配置分页器
+  handleStandardTableChange = (pagination, filterArgs, sorter) => {
+    const { dispatch } = this.props;
+    const { formValues } = this.state;
+
+    logger.info('【TableChangePagination】: %o', pagination);
+    const filters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = {...obj};
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+    if (sorter.field) {
+      params.sorter = `${sorter.field}_${sorter.order}`;
+    }
+
+    dispatch({
+      type: 'ware/getWareList',
+      payload: params,
+    })
+  }
+
+  render() {
+    const { selectedRows, modalVisible } = this.state;
+
+    const { dispatch, ware, cp: { cpList } } = this.props;
+
+    const { getFieldDecorator } = this.props.form;
+
+    const columns = [{
+      title: '课件编号',
+      dataIndex: 'code',
+      sorter: true,
+    },{
+      title: '课件名称',
+      dataIndex: 'name',
+      sorter: true,
+    },{
+      title: '课件类型',
+      dataIndex: 'type',
+      filters: config.WARE_TYPE.map((item) => ({
+        text: item.name,
+        value: item.value,
+      })),
+      render: (text, record) => (
+        <p>
+          {(config.WARE_TYPE.filter(item => item.value === record.type)[0] || { name: '未知' }).name}
+        </p>
+      ),
+    },{
+      title: '使用状态',
+      dataIndex: 'state',
+      filters: config.WARE_STATE.map((item) => ({
+        text: item.name,
+        value: item.value,
+      })),
+      render: (text, record) => (
+        <p>
+          {(config.WARE_STATE.filter(item => item.value === record.state)[0] || { name: '未知' }).name}
+        </p>
+      ),
+    },{
+      title: '内容提供商',
+      dataIndex: 'cpName',
+      filters: cpList.map((item) => ({
+        text: item.cpName,
+        value: item.cpId,
+      }))
+    },{
+      title: '修改时间',
+      dataIndex: 'gmtModified',
+      sorter: true,
+    },{
+      title: '操作类型',
+      render: (record) => (
+        <p>
+          <Button size="small" shape="circle" icon="edit" onClick={() => this.handleItemEdit(record)}></Button>
+          <span className={styles.splitLine} />
+          <Popconfirm
+            placement="top"
+            okText="删除"
+            cancelText="取消"
+            title={`确定删除${record.name}?`}
+            onConfirm={() => this.handleItemDel(record)}
+          >
+            <Button size="small" shape="circle" icon="delete"></Button>
+          </Popconfirm>
+        </p>
+      ),
+    }];
+
+    const standardTableProps = {
+      dataSource: ware.data.list,
+      pagination: ware.data.pagination,
+      loading: ware.loading,
+      columns: columns,
+      rowKeyName: 'code',
+      selectedRows: selectedRows,
+    };
+
+    const pageHeaderSearch = (
+      <Form layout="inline" onSubmit={this.handleSearch}>
+        <Row gutter={24}>
+          <Col md={9} sm={24}>
+            <FormItem label="课件编号">
+              {getFieldDecorator('code')(
+                <Input placeholder="请输入课件编号" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={10} sm={24}>
+            <FormItem label="课件名称">
+              {getFieldDecorator('name')(
+                <Input placeholder="请输入课件名称" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={5} sm={24} push={1}>
+            <FormItem>
+              <ButtonGroup>
+                <Button icon="search" type="primary" htmlType="submit">搜索</Button>
+                <Button icon="reload" type="danger" onClick={this.handleReset}>重置</Button>
+              </ButtonGroup>
+            </FormItem>
+          </Col>
+        </Row>
+      </Form>
+    );
+
+    return (
+      <PageHeaderLayout
+        content={pageHeaderSearch}
+      >
+        <div className={styles.content}>
+          <div className={styles.newButton}>
+            <Button type="primary" icon="plus" onClick={() => {this.handleItemAdd()}}>新建课件</Button>
+          </div>
+          <StandardTable
+            {...standardTableProps}
+            onChange={this.handleStandardTableChange}
+            onSelectRow={this.handleSelectRows}
+          />
+        </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 19 - 0
src/routes/Product/WareList.less

@@ -0,0 +1,19 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 1px 15px 15px 15px;
+
+  .newButton {
+    margin: 10px 0 10px 0;
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 636 - 0
src/routes/Product/WareSave.js

@@ -0,0 +1,636 @@
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { cloneDeep } from 'lodash';
+import React, { PureComponent } from 'react';
+import { message, Radio, Popover, Table, Row, Col, Card, Icon, Button, Form, Input, Select, Spin, Modal } from 'antd';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import styles from './WareSave.less';
+
+import config from '../../utils/config';
+import Logger from '../../utils/logger';
+
+const { Option } = Select;
+const { TextArea } = Input;
+const InputGroup = Input.Group;
+const ButtonGroup = Button.Group;
+const RadioGroup = Radio.Group;
+const RadioButton = Radio.Button;
+const FormItem = Form.Item;
+
+const logger = Logger.getLogger('WareEditPage');
+
+@connect(state => ({
+  cp: state.cp,
+  ware: state.ware,
+  resource: state.resource,
+}))
+@Form.create()
+export default class WareSave extends PureComponent {
+  state = {
+    wareType: config.WARE_TYPE_IMAGE, //课件的默认类型为图片类型
+    videoSelected: [],
+    modalVisible: false,
+    modalSearchContent: { key: 'name', value: '' },
+    imgSelectedArr: [],
+    tabSelected: 'WAIT',
+  }
+
+  componentDidMount() {
+    const { form, dispatch, ware: { wareItem } } = this.props;
+
+    dispatch({
+      type: 'cp/getCPList',
+    });
+
+    if (wareItem.actionType === 'EDIT')
+    {
+      form.setFieldsValue({
+        name: wareItem.itemData.name,
+        code: wareItem.itemData.code,
+        digest: wareItem.itemData.digest,
+        type: wareItem.itemData.type,
+        cpId: wareItem.itemData.cpId,
+      });
+      this.setState({
+        wareType: wareItem.itemData.type,
+      });
+    }
+  }
+
+  componentWillUnmount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'ware/clearAll',
+    });
+    this.setState({
+      wareType: config.WARE_TYPE_IMAGE,
+      videoSelected: [],
+      modalVisible: false,
+      modalSearchContent: { key: 'name', value: '' },
+      imgSelectedArr: [],
+      tabSelected: 'WAIT',
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch } = this.props;
+    dispatch(routerRedux.push('/product/ware'));
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+
+    const { form, dispatch, ware: { wareItem } } = this.props;
+
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+
+      const values = { ...fieldsValue };
+
+      values.imgIds = []; //TODO
+
+      values.playUrl = ''; //TODO
+
+      if ( wareItem.actionType === 'EDIT')
+      {
+        dispatch({
+          type: 'ware/updateWareItem',
+          payload: values,
+        });
+      }
+      else if ( wareItem.actionType === 'ADD')
+      {
+        dispatch({
+          type: 'ware/addWareItem',
+          payload: values,
+        });
+      }
+      else
+      {
+        message.error('未知操作类型!', 5000);  //不可能走到这里
+      }
+
+      form.resetFields();
+      dispatch(routerRedux.push('/product/ware'));
+    });
+  }
+
+  handleWareTypeChange = (value) => {
+    this.setState({
+      wareType: value,
+    });
+  }
+
+  handleVideoSearch = (value) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'resource/fetch',
+      payload: { type: config.RESOURCE_TYPE_IMAGE },
+    });
+  }
+
+  handleVideoSelectChange = (value) => {
+    this.setState({
+      videoSelected: value,
+    });
+  }
+
+  handleEditImageBtnClick = () => {
+    this.handleModalVisible(true);
+    this.props.dispatch({
+      type: 'resource/fetch',
+      payload: { type: config.RESOURCE_TYPE_IMAGE },
+    });
+  }
+
+  handleModalVisible = (flag) => {
+    this.setState({
+      modalVisible: !!flag
+    });
+  }
+
+  handleModalCancel = () => {
+    this.setState({
+      modalVisible: false,
+      modalSearchContent: { key: 'name', value: '' },
+      tabSelected: 'WAIT',
+    });
+  }
+
+  handleModalOk = () => {
+    this.setState({
+      modalVisible: false,
+      modalSearchContent: { key: 'name', value: '' },
+      tabSelected: 'WAIT',
+    });
+  }
+
+  handleModalSearchItemChange = (value) => {
+    this.setState({
+      modalSearchContent: { ...this.state.modalSearchContent, key: value },
+    });
+  }
+
+  handleModalInputValueChange = (e) => {
+    const value = e.target.value;
+    this.setState({
+      modalSearchContent: { ...this.state.modalSearchContent, value: value},
+    });
+  }
+
+  handleModalSearchBtnClick = () => {
+    const { dispatch } = this.props;
+    const { modalSearchContent: { key, value } } = this.state;
+    const payload = {};
+    payload[key] = value;
+    payload.type = config.RESOURCE_TYPE_IMAGE,
+
+    dispatch({
+      type: 'resource/fetch',
+      payload: payload,
+    });
+  }
+
+  handleModalResetBtnClick = () => {
+    const { dispatch } = this.props;
+
+    this.setState({
+      modalSearchContent: { ...this.state.modalSearchContent, value: '' },
+    });
+
+    dispatch({
+      type: 'resource/fetch',
+      payload: { type: config.RESOURCE_TYPE_IMAGE },
+    });
+  }
+
+  handleModalTableChange = (pagination) => {
+    const { dispatch } = this.props;
+
+    const { modalSearchContent: { key, value } } = this.state;
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      type: config.RESOURCE_TYPE_IMAGE,
+    };
+
+    params[key] = value;
+
+    dispatch({
+      type: 'resource/fetch',
+      payload: params,
+    })
+  }
+
+  handleModalTableAdd = (record) => {
+    const tmp = cloneDeep(this.state.imgSelectedArr);
+    tmp.push(cloneDeep(record));
+    this.setState({
+      imgSelectedArr: tmp,
+    });
+  }
+
+  handleModalTabChange = (e) => {
+    this.setState({
+      tabSelected: e.target.value,
+    });
+  }
+
+  handleModalSelectedItemDel = (record) => {
+    this.setState({
+      imgSelectedArr: this.state.imgSelectedArr.filter(item => item.key !== record.key),
+    });
+  }
+
+  handleModalSort = (record, sort_up) => {
+    const { imgSelectedArr } = this.state;
+
+    const arr = cloneDeep(imgSelectedArr);
+
+    const index = arr.findIndex(item => item.key === record.key);
+
+    if (sort_up)
+    {
+      //第一个元素或者未找到元素不做操作
+      if (!index || -1 === index) return;
+      //与前一个元素进行位置互换
+      arr.splice(index, 1, ...arr.splice(index - 1, 1, arr[index]));
+    }
+    else
+    {
+      //最后一个元素或者未找到元素不做操作
+      if (index + 1 === arr.length || -1 === index) return;
+      //与后一个元素进行位置互换
+      arr.splice(index, 1, ...arr.splice(index + 1, 1, arr[index]));
+    }
+
+    this.setState({
+      imgSelectedArr: arr,
+    });
+  }
+
+  render() {
+    const { wareType, videoSelected, modalVisible, modalSearchContent, imgSelectedArr, tabSelected } = this.state;
+
+    const { cp: { cpList }, resource: { data: { list, pagination }, loading } } = this.props;
+
+    const { getFieldDecorator } = this.props.form;
+
+    const formItemLayout = {
+      labelCol: {
+        xs: { span: 24 },
+        sm: { span: 7 },
+      },
+      wrapperCol: {
+        xs: { span: 24 },
+        sm: { span: 12 },
+        md: { span: 10 },
+      },
+    };
+
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    const tableColumns = [{
+      title: '图片编号',
+      dataIndex: 'code',
+      width: 150,
+    },{
+      title: '图片名称',
+      dataIndex: 'name',
+      width: 150,
+    },{
+      title: '缩略图',
+      dataIndex: 'url',
+      render: (text, record) => (
+        <Popover
+          content={
+            <img alt="" src={record.url} width={200} />
+          }
+          title={record.name}
+        >
+          <img alt="" src={record.url} width={70} />
+        </Popover>
+      ),
+    }];
+
+    const modalNotSelectTableColumns = [{
+      title: '图片编号',
+      dataIndex: 'code',
+      width: 190,
+    },{
+      title: '图片名称',
+      dataIndex: 'name',
+      width: 190,
+    },{
+      title: '缩略图',
+      dataIndex: 'url',
+      render: (text, record) => (
+        <Popover
+          content={
+            <img alt="" src={record.url} width={200} />
+          }
+          title={record.name}
+        >
+          <img alt="" src={record.url} width={50} />
+        </Popover>
+      ),
+      width: 100,
+    },{
+      title: '添加',
+      render: (text, record) => (
+        <Button
+          disabled={imgSelectedArr.filter(item => item.key === record.key).length}
+          size="small"
+          type="dashed"
+          icon="plus"
+          onClick={() => {this.handleModalTableAdd(record)}}
+        >
+        </Button>
+      ),
+    }];
+
+    const modalSelectedTableColumns = [{
+      title: '图片编号',
+      dataIndex: 'code',
+      width: 140,
+    },{
+      title: '图片名称',
+      dataIndex: 'name',
+      width: 140
+    },{
+      title: '缩略图',
+      dataIndex: 'url',
+      render: (text, record) => (
+        <Popover
+          content={
+            <img alt="" src={record.url} width={200} />
+          }
+          title={record.name}
+        >
+          <img alt="" src={record.url} width={50} />
+        </Popover>
+      ),
+      width: 100,
+    },{
+      title: '排序',
+      render: (text, record) => (
+        <ButtonGroup>
+          <Button size="small" icon="up" onClick={() => {this.handleModalSort(record, true)}}></Button>
+          <Button size="small" icon="down" onClick={() => {this.handleModalSort(record, false)}}></Button>
+        </ButtonGroup>
+      ),
+      width: 90,
+    },{
+      title: '删除',
+      render: (record) => (
+        <Button size="small" icon="delete" onClick={() => this.handleModalSelectedItemDel(record)}></Button>
+      ),
+    }];
+
+    return (
+      <PageHeaderLayout>
+        <div className={styles.content}>
+          <Form onSubmit={this.handlePageSubmit}>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课件编号"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('code', {
+                    rules: [{ required: true, type: 'string', message: '课件编号必须填写!' }]
+                  })(<Input placeholder="请输入课件编号" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课件名称"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('name', {
+                    rules: [{ required: true, type: 'string', message: '课件名称必须填写!' }]
+                  })(<Input placeholder="请输入课件名称" />)}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课件描述"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('digest')(
+                    <TextArea rows={4} placeholder="请添加课件描述" />
+                  )}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="课件类型"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('type', {
+                    rules: [{ required: true, type: 'number', message: '课件类型必须选择!' }]
+                  })(
+                    <Select placeholder="请选择课件类型" onChange={this.handleWareTypeChange}>
+                      {
+                        config.WARE_TYPE.map(item =>
+                          <Option key={item.value} value={item.value}>{item.name}</Option>
+                        )
+                      }
+                    </Select>
+                  )}
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  label="内容供应商"
+                  {...formItemLayout}
+                >
+                  {getFieldDecorator('cpId')(
+                    <Select placeholder="请选择内容提供商">
+                      {
+                        cpList.map(item =>
+                          <Option key={item.cpId} value={item.cpId}>{item.cpName}</Option>
+                        )
+                      }
+                    </Select>
+                  )}
+                </FormItem>
+              </Col>
+            </Row>
+            {wareType === config.WARE_TYPE_VIDEO ? (
+                <Row>
+                  <Col>
+                    <FormItem
+                      label="选择视频"
+                      {...formItemLayout}
+                    >
+                      <Select
+                        mode="multiple"
+                        value={videoSelected}
+                        placeholder="输入视频编号/名称进行搜索"
+                        notFoundContent={loading ? <Spin size="small" /> : null}
+                        filterOptions={false}
+                        onSearch={this.handleVideoSearch}
+                        onChange={this.handleVideoSelectChange}
+                        style={{ width: '100%' }}
+                      >
+                        {
+                          list.map(item =>
+                            <Option
+                              key={item.key}
+                              value={item.key}
+                            >{`${item.name}/${item.code}`}</Option>
+                          )
+                        }
+                      </Select>
+                    </FormItem>
+                  </Col>
+                </Row>
+              ) : null
+            }
+            <Row>
+              <Col>
+                <FormItem
+                  label="选取图片"
+                  {...formItemLayout}
+                >
+                  <Button
+                    icon="edit"
+                    type="primary"
+                    onClick={() => {this.handleEditImageBtnClick()}}
+                    disabled={wareType === config.WARE_TYPE_VIDEO ? true: false}
+                  >
+                    编辑图片
+                  </Button>
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  {...submitFormLayout}
+                >
+                  <Table
+                    columns={tableColumns}
+                    dataSource={[]}
+                    scroll={{ y: 200 }}
+                    pagination={{ pageSize: 50 }}
+                  >
+                  </Table>
+                </FormItem>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <FormItem
+                  {...submitFormLayout}
+                >
+                  <Button onClick={this.handlePageCancel}>取消</Button>
+                  <Button style={{ marginLeft: 8 }} type="primary" htmlType="submit">提交</Button>
+                </FormItem>
+              </Col>
+            </Row>
+          </Form>
+        </div>
+        {modalVisible ? (
+          <Modal
+            title="组合图片"
+            width={600}
+            maskClosable={false}
+            visible={modalVisible}
+            onOk={this.handleModalOk}
+            onCancel={this.handleModalCancel}
+          >
+            <Row gutter={24}>
+              <Col md={20}>
+                <InputGroup compact>
+                  <Select
+                    defaultValue="name"
+                    style={{ width: '30%' }}
+                    onChange={this.handleModalSearchItemChange}
+                  >
+                    <Option value="name">图片名称</Option>
+                    <Option value="code">图片编号</Option>
+                  </Select>
+                  <Input
+                    placeholder="请输入搜索内容"
+                    value={modalSearchContent.value}
+                    onChange={this.handleModalInputValueChange}
+                    onPressEnter={this.handleModalSearchBtnClick}
+                    style={{ width: '70%' }}
+                  />
+                </InputGroup>
+              </Col>
+              <Col md={2}>
+                <Button
+                  type="primary"
+                  icon="search"
+                  shape="circle"
+                  onClick={this.handleModalSearchBtnClick}
+                >
+                </Button>
+              </Col>
+              <Col md={2}>
+                <Button
+                  icon="reload"
+                  shape="circle"
+                  onClick={this.handleModalResetBtnClick}
+                >
+                </Button>
+              </Col>
+            </Row>
+            <Row>
+              <RadioGroup size="small" defaultValue="WAIT" onChange={this.handleModalTabChange} style={{ marginTop: 10, marginBottom: 10 }}>
+                <RadioButton value="WAIT">待选</RadioButton>
+                <RadioButton value="SELECTED">{`已选【${imgSelectedArr.length}】`}</RadioButton>
+              </RadioGroup>
+            </Row>
+            <Row>
+              <Col>
+                <Table
+                  columns={modalNotSelectTableColumns}
+                  rowKey={record => record.key}
+                  dataSource={list}
+                  loading={loading}
+                  onChange={this.handleModalTableChange}
+                  pagination={pagination}
+                  scroll={{ y: 200 }}
+                  style={tabSelected === 'WAIT' ? null : { display: 'none' }}
+                >
+                </Table>
+              </Col>
+            </Row>
+            <Row>
+              <Col>
+                <Table
+                  style={tabSelected === 'SELECTED' ? null : { display: 'none' }}
+                  columns={modalSelectedTableColumns}
+                  rowKey={record => record.key}
+                  dataSource={imgSelectedArr}
+                  pagination={{ pageSize: 50 }}
+                  scroll={{ y: 200 }}
+                >
+                </Table>
+              </Col>
+            </Row>
+          </Modal>
+          ) : null
+        }
+      </PageHeaderLayout>
+    )
+  }
+}

+ 7 - 0
src/routes/Product/WareSave.less

@@ -0,0 +1,7 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+  padding: 30px 15px 15px 15px;
+}

+ 398 - 0
src/routes/Resource/ImageList.js

@@ -0,0 +1,398 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import moment from 'moment';
+import { Progress, Notification, Popover, Badge, message, Upload, Card, Icon, Button, Modal, Input, Select, Form, Row, Col, DatePicker, Tooltip } from 'antd';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import config from '../../utils/config';
+import styles from './ImageList.less';
+
+const { Option, OptGroup } = Select;
+const FormItem = Form.Item;
+const ButtonGroup = Button.Group;
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  resource: state.resource
+}))
+@Form.create()
+export default class ImageList extends PureComponent {
+    state = {
+      selectedRows: [],
+      modalVisible: false,
+      formValues: {},
+      selectSearchItem: 'name',
+    };
+
+    componentDidMount() {
+      const { dispatch } = this.props;
+      dispatch({
+        type: 'resource/fetch',
+        payload: { type: config.RESOURCE_TYPE_IMAGE },
+      })
+    }
+
+    //响应不同搜索字段的变化
+    handleSelectChange = (value) => {
+      this.setState({
+        selectSearchItem: value,
+      });
+    }
+
+    //点击搜索按钮,提交表单,加载匹配内容
+    handleSearchBtnClick = (e) => {
+      e.preventDefault();
+
+      const { dispatch, form } = this.props;
+
+      form.validateFields((err, fieldsValue) => {
+        if (err) return;
+
+        if (fieldsValue['gmtCreated']) {
+          fieldsValue['gmtCreated'] = fieldsValue['gmtCreated'].format('YYYY-MM-DD');
+        }
+
+        if (fieldsValue['gmtModified']) {
+          fieldsValue['gmtModified'] = fieldsValue['gmtModified'].format('YYYY-MM-DD');
+        }
+
+        const values = {
+          ...fieldsValue,
+        };
+
+        // dispatch({
+        //   type: 'resource/fetch',
+        //   payload: { type: config.RESOURCE_IMAGE_TYPE },
+        // });
+      });
+    }
+
+    //点击重置按钮处理,重置搜索表单,加载第一页数据
+    handleReset = () => {
+      this.props.form.resetFields();
+      this.props.resource.data.list = []; //清空本地图片存储
+      this.props.dispatch({
+        type: 'resource/fetch',
+        payload: { type: config.RESOURCE_IMAGE_TYPE },
+      });
+    }
+
+    //响应行选择事件
+    handleSelectRows = (rows) => {
+      this.setState({
+        selectedRows: rows,
+      });
+    }
+
+    //配置分页器
+    handleStandardTableChange = (pagination, filterArgs, sorter) => {
+      const { dispatch } = this.props;
+      const { formValues } = this.state;
+
+      const filters = Object.keys(filterArgs).reduce((obj, key) => {
+        const newObj = {...obj};
+        newObj[key] = getValue(filterArgs[key]);
+        return newObj;
+      }, {});
+
+      const params = {
+        currentPage: pagination.pageNo,
+        pageSize: pagination.pageSize,
+        ...formValues,
+        ...filters,
+      };
+      if (sorter.field) {
+        params.sorter = `${sorter.field}_${sorter.order}`;
+      }
+
+      dispatch({
+        type: 'resource/fetch',
+        payload: params,
+      })
+    }
+
+    //图片上传前处理
+    handleBeforeUpload = (file) => {
+      const { dispatch, resource: { oss: { fileList } } } = this.props;
+      dispatch({
+        type: 'resource/setFileList',
+        payload: {
+          fileList: [...fileList, file],
+        }
+      });
+      return false;   //选则完文件后先不上传,等待点击确定按钮
+    }
+
+    //删除已选图片
+    handleRemove = (file) => {
+      const { dispatch, resource: { fileList } } = this.props;
+      const index = fileList.indexOf(file);
+      const newFileList = fileList.slice();
+      newFileList.splice(index, 1);
+      dispatch({
+        type: 'resource/setFileList',
+        payload: {
+          fileList: newFileList,
+        }
+      })
+    }
+
+    //点击确定,进行一些列上传动作
+    handleUploadSubmit = (e) => {
+      const { dispatch } = this.props;
+
+      // dispatch({
+      //   type: 'resource/upload',
+      // });
+
+      this.setState({ modalVisible: false });
+
+      Notification.success({
+        message: '图片上传成功',
+        description: '100%',
+      });
+      // e.preventDefault();
+      // const { dispatch, form } = this.props;
+      // form.validateFields(({fieldName: ['newName', 'newCode']}, err, fieldsValue) => {
+      //   if (err) return;
+      //   const values = {
+      //     ...fieldsValue,
+      //   };
+      //   dispatch({
+      //     type: 'resource/resourceUpload',
+      //   });
+      // });
+    }
+
+    //控制模态框显现状态
+    handleModalVisible = (flag) => {
+      this.setState({
+        modalVisible: !!flag,
+      });
+    }
+
+    //模态框表单提交,上传图片信息到应用服务器
+    handleModalSubmit = (e) => {
+      e.preventDefault();
+
+      const { dispatch, form } = this.props;
+
+      form.validateFields((err, fieldsValue) => {
+        if (err) return;
+      });
+    }
+
+    dynamicLoadWidget = () => {
+      const { getFieldDecorator } = this.props.form;
+
+      let config = {};
+
+      switch (this.state.selectSearchItem) {
+        case 'name':
+          config = {
+            rules: [{
+              type: 'string',
+              required: true,
+              message: '请填入图片名称!',
+            }],
+          }
+          return (
+            <FormItem>
+              {getFieldDecorator('name', config)(
+                <Input placeholder="请输入图片名称" style={{ minWidth: 200, maxWidth: 250, width: '100%' }}></Input>
+              )}
+            </FormItem>
+          );
+        case 'code':
+          config = {
+            rules: [{
+              type: 'string',
+              required: true,
+              message: '请填入图片编号!',
+            }],
+          }
+          return (
+            <FormItem>
+              {getFieldDecorator('code', config)(
+                <Input placeholder="请输入图片编号" style={{ minWidth: 200, maxWidth: 250, width: '100%' }}></Input>
+              )}
+            </FormItem>
+          );
+        case 'gmtCreated':
+          config = {
+            rules: [{
+              type: 'object',
+              required: true,
+              message: '请选择创建日期!',
+            }],
+          }
+          return (
+            <FormItem>
+              {getFieldDecorator('gmtCreated', config)(
+                <DatePicker style={{ minWidth: 200, maxWidth: 250, width: '100%' }} />
+              )}
+            </FormItem>
+          );
+        case 'gmtModified':
+          config = {
+            rules: [{
+              type: 'object',
+              required: true,
+              message: '请选择修改日期!',
+            }],
+          }
+          return (
+            <FormItem>
+              {getFieldDecorator('gmtModified', config)(
+                <DatePicker style={{ minWidth: 200, maxWidth: 250, width: '100%' }} />
+              )}
+            </FormItem>
+          );
+      }
+    }
+
+    render() {
+      const { modalVisible, selectedRows } = this.state;
+
+      const { getFieldDecorator } = this.props.form;
+
+      const { dispatch, resource } = this.props;
+
+      /* 列表表头定义 */
+      const columns = [
+        {
+          title: '图片编码',
+          dataIndex: 'code',
+        },
+        {
+          title: '图片名称',
+          dataIndex: 'name',
+        },
+        {
+          title: '图片大小',
+          dataIndex: 'size',
+        },
+        {
+          title: '修改时间',
+          dataIndex: 'gmtModified',
+          sorter: true,
+          render: val => <span>{moment(val).format('YYYY-MM-DD HH:mm:ss')}</span>,
+        },
+        {
+          title: '缩略图',
+          dataIndex: 'url',
+          render: (text, record) => (
+            <Popover
+              content={
+                <img alt="" src={record.url} width={350} />
+              }
+              title={record.name}
+            >
+              <img alt="" src={record.url} width={70} />
+            </Popover>
+          ),
+        },
+        {
+          title: '操作',
+          render: () => (
+            <p>
+              <Icon type="edit" />
+              <span className={styles.splitLine} />
+              <Icon type="delete" />
+            </p>
+          ),
+        },
+      ];
+
+      const standardTableProps = {
+        dataSource: resource.data.list,
+        pagination: resource.data.pagination,
+        loading: resource.loading,
+        columns: columns,
+        rowKeyName: 'code',
+        selectedRows: selectedRows,
+      };
+
+      /* 顶部的筛选组件 */
+      const headerSearchForm = (
+        <Form layout="inline" onSubmit={this.handleSearchBtnClick}>
+          <FormItem>
+            <Select style={{ width: 150 }} defaultValue="name" onChange={this.handleSelectChange}>
+              <Option value="name">图片名称</Option>
+              <Option value="code">图片编号</Option>
+              <Option value="gmtCreated">创建时间</Option>
+              <Option value="gmtModified">修改时间</Option>
+            </Select>
+          </FormItem>
+          {this.dynamicLoadWidget()}
+          <FormItem>
+            <ButtonGroup>
+              <Button type="primary" icon="search" htmlType="submit">搜索</Button>
+              <Button type="danger" icon="reload" onClick={this.handleReset}>重置</Button>
+            </ButtonGroup>
+          </FormItem>
+        </Form>
+      );
+
+      /* 模态弹框 */
+      const modalForm = (
+        <Modal
+          title="新建图片"
+          visible={modalVisible}
+          onOk={this.handleUploadSubmit}
+          onCancel={() => this.handleModalVisible(false)}
+        >
+          <FormItem
+            label="图片名称"
+            labelCol={{ span: 5 }}
+            wrapperCol={{ span: 15 }}
+          >
+            {getFieldDecorator('name', { rules: [{type:'string', required:true, message:'请填写图片名!'}] })(
+              <Input placeholder="请输入图片名称" />
+            )}
+          </FormItem>
+          <FormItem
+            label="图片编号"
+            labelCol={{ span: 5 }}
+            wrapperCol={{ span: 15 }}
+          >
+            {getFieldDecorator('code')(
+              <Input placeholder="请输入图片编号" />
+            )}
+          </FormItem>
+          <FormItem
+            label="选择图片"
+            labelCol={{ span: 5 }}
+            wrapperCol={{ span: 15 }}
+          >
+            <Upload
+              beforeUpload={this.handleBeforeUpload}
+              onRemove={this.handleRemove}
+            >
+              <Button icon="cloud-upload">选择图片</Button>
+            </Upload>
+          </FormItem>
+        </Modal>
+      );
+
+      return (
+        <PageHeaderLayout
+        >
+          <div className={styles.content}>
+            <div className={styles.search}>{headerSearchForm}</div>
+            <div className={styles.tableList}>
+              <div className={styles.tableListOperator}>
+                <Button type="primary" icon="plus" onClick={() => {this.handleModalVisible(true)}}>增加图片</Button>
+              </div>
+              <StandardTable
+                {...standardTableProps}
+                onSelectRow={this.handleSelectRows}
+                onChange={this.handleStandardTableChange}
+              />
+            </div>
+            <div>{modalForm}</div>
+          </div>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 53 - 0
src/routes/Resource/ImageList.less

@@ -0,0 +1,53 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.content {
+  background-color: #fff;
+
+  .search {
+    padding: 20px 0 0 15px;
+    border-bottom: 1px dashed #e3e3e3;
+    height: 75px;
+  }
+
+  .tableList {
+    padding: 15px;
+
+    .tableListOperator {
+      margin-bottom: 16px;
+      button {
+        margin-right: 8px;
+      }
+    }
+  }
+
+  .tableListForm {
+    :global {
+      .ant-form-item {
+        margin-bottom: 24px;
+        margin-right: 0;
+        display: flex;
+        > .ant-form-item-label {
+          width: auto;
+          line-height: 32px;
+          padding-right: 8px;
+        }
+      }
+      .ant-form-item-control-wrapper {
+        flex: 1;
+      }
+    }
+    .submitButtons {
+      white-space: nowrap;
+      margin-bottom: 24px;
+    }
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 140 - 0
src/routes/Resource/VideoList.js

@@ -0,0 +1,140 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'dva';
+import { Progress, notification, Badge, message, Card, Icon, Button,
+  Modal, Input, Select, Form, Row, Col, Tooltip } from 'antd';
+import moment from 'moment';
+
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import StandardTable from '../../components/StandardTable';
+import config from '../../utils/config';
+
+const { Option, OptGroup } = Select;
+
+const FormItem = Form.Item;
+
+const ButtonGroup = Button.Group;
+
+const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+
+@connect(state => ({
+  tag: state.tag,
+}))
+@Form.create()
+export default class VideoList extends PureComponent {
+  state = {
+    selectedRows: [],
+    formValues: {},
+  }
+
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'tag/getTagList',
+      payload: {},
+    })
+  }
+
+  handleSearch = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.validateFields((err, fieldsValue) => {
+      if (err) return;
+      const values = {
+        ...fieldsValue,
+      }
+      logger.info('【Search Values】: %o', values);
+      dispatch({
+        type: 'tag/getTagList',
+        payload: values,
+      });
+    });
+  }
+
+  handleReset = (e) => {
+    e.preventDefault();
+    const { dispatch, form } = this.props;
+    form.resetFields();
+    dispatch({
+      type: 'tag/getTagList',
+      payload: {},
+    });
+  }
+
+  handleSelectRows = (rows) => {
+    this.setState({
+      selectedRows: rows,
+    });
+  }
+
+  //配置分页器
+  handleStandardTableChange = (pagination, filterArgs, sorter) => {
+    const { dispatch } = this.props;
+    const { formValues } = this.state;
+
+    logger.info('【TableChangePagination】: %o', pagination);
+    const filters = Object.keys(filterArgs).reduce((obj, key) => {
+      const newObj = {...obj};
+      newObj[key] = getValue(filterArgs[key]);
+      return newObj;
+    }, {});
+
+    const params = {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      ...formValues,
+      ...filters,
+    };
+    if (sorter.field) {
+      params.sorter = `${sorter.field}_${sorter.order}`;
+    }
+
+    dispatch({
+      type: 'tag/getTagList',
+      payload: params,
+    })
+  }
+
+  render() {
+    const { selectedRows } = this.state;
+
+    const { dispatch } = this.props;
+
+    const { getFieldDecorator } = this.props.form;
+
+    const pageHeaderSearch = (
+      <Form layout="inline" onSubmit={this.handleSearch}>
+        <Row gutter={24}>
+          <Col md={9} sm={24}>
+            <FormItem label="视频编号">
+              {getFieldDecorator('videoName')(
+                <Input placeholder="请输入视频编号" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={10} sm={24}>
+            <FormItem label="视频名称">
+              {getFieldDecorator('videoName')(
+                <Input placeholder="请输入视频名称" />
+              )}
+            </FormItem>
+          </Col>
+          <Col md={5} sm={24}>
+            <FormItem>
+              <ButtonGroup>
+                <Button icon="search" type="primary" htmlType="submit">搜索</Button>
+                <Button icon="reload" type="danger" onClick={this.handleReset}>重置</Button>
+              </ButtonGroup>
+            </FormItem>
+          </Col>
+        </Row>
+      </Form>
+    );
+
+    return (
+      <PageHeaderLayout
+        content={pageHeaderSearch}
+      >
+      </PageHeaderLayout>
+    );
+  }
+}

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

@@ -0,0 +1,104 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.cardList {
+  margin-bottom: -24px;
+  background: #fff;
+
+  .card {
+    :global {
+      .ant-card-meta-title {
+        margin-bottom: 12px;
+        & > a {
+          color: @heading-color;
+        }
+      }
+      .ant-card-actions {
+        background: #f7f9fa;
+      }
+      .ant-card-body:hover {
+        .ant-card-meta-title > a {
+          color: @primary-color;
+        }
+      }
+    }
+  }
+}
+
+.extraImg {
+  margin-top: -60px;
+  text-align: center;
+  width: 195px;
+  img {
+    width: 100%;
+  }
+}
+
+.newButton {
+  margin-left: 5px;
+  background-color: #fff;
+  border-color: @border-color-base;
+  border-radius: @border-radius-sm;
+  color: @text-color-secondary;
+  width: 100%;
+  height: 293px;
+}
+
+.cardAvatar {
+  width: 48px;
+  height: 48px;
+  border-radius: 48px;
+}
+
+.cardDescription {
+  .textOverflowMulti();
+}
+
+.pageHeaderSearch {
+  position: relative;
+}
+
+.contentLink {
+  margin-top: 16px;
+  a {
+    margin-right: 32px;
+    img {
+      width: 24px;
+    }
+  }
+  img {
+    vertical-align: middle;
+    margin-right: 8px;
+  }
+}
+
+@media screen and (max-width: @screen-lg) {
+  .contentLink {
+    a {
+      margin-right: 16px;
+    }
+  }
+}
+@media screen and (max-width: @screen-md) {
+  .extraImg {
+    display: none;
+  }
+}
+
+@media screen and (max-width: @screen-sm) {
+  .pageHeaderContent {
+    padding-bottom: 30px;
+  }
+  .contentLink {
+    position: absolute;
+    left: 0;
+    bottom: -4px;
+    width: 1000px;
+    a {
+      margin-right: 16px;
+    }
+    img {
+      margin-right: 4px;
+    }
+  }
+}

+ 11 - 0
src/services/CPApi.js

@@ -0,0 +1,11 @@
+import { stringify } from 'qs';
+import { message } from 'antd';
+import request from '../utils/request';
+import config from '../utils/config';
+
+const { api } = config;
+const { cpList } = api;
+
+export async function getCPList() {
+  return request(`${cpList}`);
+}

+ 43 - 0
src/services/ItemAPI.js

@@ -0,0 +1,43 @@
+import { stringify } from 'qs';
+import { cloneDeep } from 'lodash';
+import { message } from 'antd';
+import request from '../utils/request';
+import config from '../utils/config';
+
+const { api } = config;
+const { itemList, itemItem, itemItemAdd, itemItemUpdate, itemItemDel } = api;
+
+export async function getItemList(params) {
+  return request(`${itemList}?${stringify(params)}`);
+}
+
+export async function getItemItem(params) {
+  let apiPath = itemItem.split('/:');
+  const queryBy = apiPath.pop();
+  const id = params[queryBy];
+  return request(`${apiPath.join('/')}/${id}`);
+}
+
+export async function addItemItem(params) {
+  const options = {
+    method: 'POST',
+    body: { ...params },
+  };
+  return request(`${itemItemAdd}`, options);
+}
+
+export async function updateItemItem(params) {
+  const options = {
+    method: 'PUT',
+    body: { ...params },
+  };
+  return request(`${itemItemUpdate}`, options);
+}
+
+export async function delItemItem(params) {
+  const options = {
+    method: 'DELETE',
+    body: { ...params },
+  };
+  return request(`${itemItemDel}`, options);
+}

+ 11 - 0
src/services/MerchantApi.js

@@ -0,0 +1,11 @@
+import { stringify } from 'qs';
+import { message } from 'antd';
+import request from '../utils/request';
+import config from '../utils/config';
+
+const { api } = config;
+const { merchantList } = api;
+
+export async function getMerchantList() {
+  return request(`${merchantList}`);
+}

+ 0 - 0
src/services/ProductApi.js


Some files were not shown because too many files changed in this diff