Browse Source

Merge branch 'master' of www.gitinn.com:zaixianjiaoyu/sourcecode

KaysonCui 5 years ago
parent
commit
47f723bb2c
100 changed files with 3545 additions and 580 deletions
  1. 47 4
      front/project/Constant.js
  2. 6 3
      front/project/admin/components/QuestionNoList/index.js
  3. 4 0
      front/project/admin/routes/course/data/page.js
  4. 8 89
      front/project/admin/routes/course/dataDetail/page.js
  5. 18 8
      front/project/admin/routes/course/experienceDetail/page.js
  6. 2 1
      front/project/admin/routes/course/package/page.js
  7. 4 4
      front/project/admin/routes/course/preview/page.js
  8. 6 3
      front/project/admin/routes/interaction/askQuestionDetail/page.js
  9. 16 6
      front/project/admin/routes/interaction/comment/page.js
  10. 19 6
      front/project/admin/routes/interaction/faq/page.js
  11. 1 0
      front/project/admin/routes/interaction/feedback/page.js
  12. 15 0
      front/project/admin/routes/ready/article/index.js
  13. 3 0
      front/project/admin/routes/ready/article/index.less
  14. 61 17
      front/project/admin/routes/show/article/page.js
  15. 16 0
      front/project/admin/routes/ready/articleDetail/index.js
  16. 0 0
      front/project/admin/routes/ready/articleDetail/index.less
  17. 174 0
      front/project/admin/routes/ready/articleDetail/page.js
  18. 15 0
      front/project/admin/routes/ready/data/index.js
  19. 3 0
      front/project/admin/routes/ready/data/index.less
  20. 152 0
      front/project/admin/routes/ready/data/page.js
  21. 7 2
      front/project/admin/routes/ready/index.js
  22. 2 2
      front/project/admin/routes/show/article/index.js
  23. 3 0
      front/project/admin/routes/ready/read/index.less
  24. 190 0
      front/project/admin/routes/ready/read/page.js
  25. 4 4
      front/project/admin/routes/show/articleDetail/index.js
  26. 3 0
      front/project/admin/routes/ready/readDetail/index.less
  27. 29 18
      front/project/admin/routes/show/articleDetail/page.js
  28. 15 0
      front/project/admin/routes/ready/room/index.js
  29. 1 1
      front/project/admin/routes/show/article/index.less
  30. 199 0
      front/project/admin/routes/ready/room/page.js
  31. 15 0
      front/project/admin/routes/setting/contract/index.js
  32. 3 0
      front/project/admin/routes/setting/contract/index.less
  33. 54 0
      front/project/admin/routes/setting/contract/page.js
  34. 16 0
      front/project/admin/routes/setting/contractDetail/index.js
  35. 3 0
      front/project/admin/routes/setting/contractDetail/index.less
  36. 100 0
      front/project/admin/routes/setting/contractDetail/page.js
  37. 3 1
      front/project/admin/routes/setting/index.js
  38. 69 3
      front/project/admin/routes/setting/promote/page.js
  39. 137 25
      front/project/admin/routes/show/ad/page.js
  40. 107 10
      front/project/admin/routes/show/comment/page.js
  41. 109 10
      front/project/admin/routes/show/faq/page.js
  42. 2 3
      front/project/admin/routes/show/index.js
  43. 157 119
      front/project/admin/routes/show/message/page.js
  44. 16 0
      front/project/admin/routes/show/messageDetail/index.js
  45. 3 0
      front/project/admin/routes/show/messageDetail/index.less
  46. 112 0
      front/project/admin/routes/show/messageDetail/page.js
  47. 6 3
      front/project/admin/routes/student/askCourseDetail/page.js
  48. 16 0
      front/project/admin/routes/user/abnormal/page.js
  49. 13 1
      front/project/admin/routes/user/detail/page.js
  50. 2 1
      front/project/admin/routes/user/list/page.js
  51. 2 0
      front/project/admin/routes/user/orderDetail/page.js
  52. 1 0
      front/project/admin/routes/user/recordAll/page.js
  53. 1 0
      front/project/admin/routes/user/recordBuy/page.js
  54. 2 1
      front/project/admin/stores/index.js
  55. 103 0
      front/project/admin/stores/ready.js
  56. 24 28
      front/project/admin/stores/system.js
  57. 4 0
      front/project/admin/stores/user.js
  58. 50 18
      front/project/h5/components/Block/index.js
  59. 2 11
      front/project/h5/routes/page/bind/page.js
  60. 97 6
      front/project/h5/routes/product/bought/page.js
  61. 20 0
      front/project/h5/stores/main.js
  62. 6 4
      front/project/h5/stores/user.js
  63. 6 23
      front/project/www/components/Login/index.js
  64. 3 2
      front/project/www/components/QAList/index.js
  65. 11 0
      front/project/www/routes/examination/main/page.js
  66. 23 1
      front/project/www/routes/exercise/main/page.js
  67. 4 0
      front/project/www/routes/paper/report/index.less
  68. 157 60
      front/project/www/routes/paper/report/page.js
  69. 9 7
      front/project/www/routes/sentence/read/page.js
  70. 20 4
      front/project/www/stores/main.js
  71. 42 10
      front/project/www/stores/my.js
  72. 6 4
      front/project/www/stores/user.js
  73. 1 1
      front/src/components/DragList/index.js
  74. 2 2
      front/src/components/FileUpload/index.js
  75. 3 0
      front/src/layouts/FormLayout/index.js
  76. 30 2
      front/src/services/Tools.js
  77. 37 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/MessageCategory.java
  78. 25 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/MessageMethod.java
  79. 40 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/MessageType.java
  80. 2 2
      server/data/src/main/java/com/qxgmat/data/constants/enums/SettingKey.java
  81. 1 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/trade/RecordSource.java
  82. 0 21
      server/data/src/main/java/com/qxgmat/data/constants/enums/user/MessageType.java
  83. 0 7
      server/data/src/main/java/com/qxgmat/data/dao/ArticleMapper.java
  84. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/ContractMapper.java
  85. 0 7
      server/data/src/main/java/com/qxgmat/data/dao/MessageMapper.java
  86. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/MessageTemplateMapper.java
  87. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/ReadyArticleCategoryMapper.java
  88. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/ReadyArticleMapper.java
  89. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/ReadyDataMapper.java
  90. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/ReadyReadMapper.java
  91. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/ReadyRoomAreaMapper.java
  92. 7 0
      server/data/src/main/java/com/qxgmat/data/dao/ReadyRoomMapper.java
  93. 70 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/Ad.java
  94. 39 4
      server/data/src/main/java/com/qxgmat/data/dao/entity/Comment.java
  95. 212 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/Contract.java
  96. 61 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/CourseData.java
  97. 39 4
      server/data/src/main/java/com/qxgmat/data/dao/entity/Faq.java
  98. 77 7
      server/data/src/main/java/com/qxgmat/data/dao/entity/Message.java
  99. 291 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/ReadyArticle.java
  100. 0 0
      server/data/src/main/java/com/qxgmat/data/dao/entity/ReadyArticleCategory.java

File diff suppressed because it is too large
+ 47 - 4
front/project/Constant.js


+ 6 - 3
front/project/admin/components/QuestionNoList/index.js

@@ -33,9 +33,12 @@ export default class QuestionNoList extends Component {
 
   orderQuestion(oldIndex, newIndex) {
     const { questionNos, loading } = this.state;
-    const tmp = questionNos[oldIndex];
-    questionNos[oldIndex] = questionNos[newIndex];
-    questionNos[newIndex] = tmp;
+    const tmp = questionNos.splice(oldIndex, 1);
+    if (newIndex === questionNos.length) {
+      questionNos.push(tmp[0]);
+    } else {
+      questionNos.splice(newIndex, 0, tmp[0]);
+    }
     this.setState({ questionNos, loading: loading + 1 });
     this.props.onChange(questionNos);
   }

+ 4 - 0
front/project/admin/routes/course/data/page.js

@@ -77,9 +77,13 @@ export default class extends Page {
       dataIndex: 'title',
     }, {
       title: '查看人数',
+      sorter: true,
+      sortDirections: ['ascend'],
       dataIndex: 'viewNumber',
     }, {
       title: '购买人数',
+      sorter: true,
+      sortDirections: ['descend'],
       dataIndex: 'saleNumber',
     }, {
       title: '更新时间',

+ 8 - 89
front/project/admin/routes/course/dataDetail/page.js

@@ -6,10 +6,10 @@ import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import Radio from '@src/components/Radio';
 import TreeSelect from '@src/components/TreeSelect';
-import EditTableCell from '@src/components/EditTableCell';
+// import EditTableCell from '@src/components/EditTableCell';
 // import ActionLayout from '@src/layouts/ActionLayout';
 // import TableLayout from '@src/layouts/TableLayout';
-import { formatFormError, formatTreeData, getMap, formatDate } from '@src/services/Tools';
+import { formatFormError, formatTreeData, getMap } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
 import { SwitchSelect, DataType } from '../../../../Constant';
 // import { User } from '../../../stores/user';
@@ -18,85 +18,8 @@ import { Course } from '../../../stores/course';
 import { System } from '../../../stores/system';
 
 export default class extends Page {
-  initState() {
-    return { history: false };
-  }
-
   init() {
     this.exerciseMap = {};
-    this.actionList = [{
-      key: 'addHistory',
-      type: 'primary',
-      name: '新增版本',
-    }];
-    this.itemList = [{
-      key: 'id',
-      type: 'hidden',
-    }, {
-      key: 'time',
-      type: 'date',
-      name: '更新时间',
-    }, {
-      key: 'position',
-      type: 'input',
-      name: '更新位置',
-    }, {
-      key: 'originContent',
-      type: 'input',
-      name: '原内容',
-    }, {
-      key: 'content',
-      type: 'input',
-      name: '更改为',
-    }, {
-      key: 'version',
-      type: 'input',
-      name: '更新至',
-    }];
-    this.columns = [{
-      title: '更新时间',
-      dataIndex: 'time',
-      render: (text) => {
-        return formatDate(text);
-      },
-    }, {
-      title: '版本名称',
-      dataIndex: 'version',
-      render: (text, record) => {
-        return <EditTableCell value={text} onChange={(v) => {
-          this.changeHistory('version', record.id, v);
-        }} />;
-      },
-    }, {
-      title: '位置',
-      dataIndex: 'position',
-    }, {
-      title: '原内容',
-      dataIndex: 'originContent',
-    }, {
-      title: '更正为',
-      dataIndex: 'content',
-    }, {
-      title: '更新至',
-      dataIndex: 'version',
-    }, {
-      title: '操作',
-      dataIndex: 'handler',
-      render: (text, record) => {
-        return <div className="table-button">
-          {(
-            <a onClick={() => {
-              this.changeHistory(record);
-            }}>编辑</a>
-          )}
-          {(
-            <a onClick={() => {
-              this.deleteHistory(record);
-            }}>删除</a>
-          )}
-        </div>;
-      },
-    }];
     Exercise.dataStruct().then((result) => {
       const list = result.map(row => { row.title = `${row.titleZh}`; row.value = row.id; return row; });
       const tree = formatTreeData(list, 'id', 'title', 'parentId');
@@ -129,10 +52,12 @@ export default class extends Page {
 
   submit() {
     const { form } = this.props;
-    form.validateFields((err) => {
+    form.validateFields((err, values) => {
+      console.log(values);
       if (!err) {
         const data = form.getFieldsValue();
         data.parentStructId = this.exerciseMap[data.structId] ? this.exerciseMap[data.structId].parentId : 0;
+        data.isSentence = this.exerciseMap[data.structId] ? (this.exerciseMap[data.structId].extend === 'sentence' ? 1 : 0) : 0;
         Course.editData(data).then(() => {
           asyncSMessage('保存成功');
         }).catch((e) => {
@@ -165,13 +90,7 @@ export default class extends Page {
               required: true, message: '请选择类型',
             }],
           })(
-            <Radio select={DataType} onChange={(e) => {
-              if (e.target.value === 'electron') {
-                this.refreshHistory();
-              } else {
-                this.setState({ history: false });
-              }
-            }} />,
+            <Radio select={DataType} />,
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='适合新手'>
@@ -268,7 +187,7 @@ export default class extends Page {
             <Input placeholder='请输入' />,
           )}
         </Form.Item>
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='试用页码'>
+        {/* <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='试用页码'>
           <Row>
             <Col span={3}>
               <Form.Item>
@@ -286,7 +205,7 @@ export default class extends Page {
               </Form.Item>
             </Col>
           </Row>
-        </Form.Item>
+        </Form.Item> */}
       </Form>
     </Block>;
   }

+ 18 - 8
front/project/admin/routes/course/experienceDetail/page.js

@@ -21,6 +21,10 @@ while (tmp <= maxScore) {
 }
 
 export default class extends Page {
+  initState() {
+    return { showNickname: true };
+  }
+
   initData() {
     const { id } = this.params;
     const { form } = this.props;
@@ -33,7 +37,12 @@ export default class extends Page {
 
     handler
       .then(result => {
-        generateSearch('userId', {}, this, (search) => {
+        if (result.userId) {
+          this.setState({ showNickname: false });
+        }
+        generateSearch('userId', {
+          allowClear: true,
+        }, this, (search) => {
           return User.list(search);
         }, (row) => {
           return {
@@ -68,6 +77,7 @@ export default class extends Page {
 
   renderBase() {
     const { getFieldDecorator } = this.props.form;
+    const { showNickname } = this.state;
     return <Block>
       <Form>
         {getFieldDecorator('id')(<input hidden />)}
@@ -76,26 +86,26 @@ export default class extends Page {
             rules: [
               { required: true, message: '请输入名称' },
             ],
-          })(
-            <Input placeholder='请输入名称' />,
-          )}
+          })(<Input placeholder='请输入名称' />)}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='作者'>
           {getFieldDecorator('userId', {
             rules: [
-              { required: true, message: '请选择作者' },
+              { required: !showNickname, message: '请选择作者' },
             ],
           })(
-            <Select {...this.state.userId} placeholder='请选择作者' />,
+            <Select {...this.state.userId} onChange={(value) => {
+              this.setState({ showNickname: !value });
+            }} placeholder='请选择作者' />,
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='昵称'>
           {getFieldDecorator('nickname', {
             rules: [
-              { required: true, message: '请输入昵称' },
+              { required: showNickname, message: '请输入昵称' },
             ],
           })(
-            <Input placeholder='输入昵称' />,
+            <Input disabled={!showNickname} placeholder='输入昵称' />,
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='作者信息'>

+ 2 - 1
front/project/admin/routes/course/package/page.js

@@ -5,7 +5,7 @@ import Block from '@src/components/Block';
 // import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { getMap, formatTreeData } from '@src/services/Tools';
+import { getMap, formatTreeData, flattenObject } from '@src/services/Tools';
 import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
 import { ServiceKey, ServiceParamMap, SwitchSelect } from '../../../../Constant';
 import { Course } from '../../../stores/course';
@@ -147,6 +147,7 @@ export default class extends Page {
   }
 
   editAction(row) {
+    row = flattenObject(row);
     asyncForm('编辑', this.itemList, row, data => {
       return Course.editPackage(data).then(() => {
         asyncSMessage('编辑成功!');

+ 4 - 4
front/project/admin/routes/course/preview/page.js

@@ -25,21 +25,21 @@ export default class extends Page {
       type: 'primary',
       name: '创建视频作业',
       render: (item) => {
-        return <Link to='/subject/preview/detail?module=video'><Button type='primary'>{item.name}</Button></Link>;
+        return <Link to='/course/preview/detail?module=video'><Button type='primary'>{item.name}</Button></Link>;
       },
     }, {
       key: 'addOnline',
       type: 'primary',
       name: '创建小班作业',
       render: (item) => {
-        return <Link to='/subject/preview/detail?module=online'><Button type='primary'>{item.name}</Button></Link>;
+        return <Link to='/course/preview/detail?module=online'><Button type='primary'>{item.name}</Button></Link>;
       },
     }, {
       key: 'addVs',
       type: 'primary',
       name: '创建1vs1作业',
       render: (item) => {
-        return <Link to='/subject/preview/detail?module=vs'><Button type='primary'>{item.name}</Button></Link>;
+        return <Link to='/course/preview/detail?module=vs'><Button type='primary'>{item.name}</Button></Link>;
       },
     }];
 
@@ -89,7 +89,7 @@ export default class extends Page {
       render: (text, record) => {
         return <div className="table-button">
           {(
-            <Link to={`/subject/preview/detail/${record.id}`}>编辑</Link>
+            <Link to={`/course/preview/detail/${record.id}`}>编辑</Link>
           )}
         </div>;
       },

+ 6 - 3
front/project/admin/routes/interaction/askQuestionDetail/page.js

@@ -46,9 +46,12 @@ export default class extends Page {
   orderQuestion(oldIndex, newIndex) {
     const { data } = this.state;
     const { others = [] } = data;
-    const tmp = others[oldIndex];
-    others[oldIndex] = others[newIndex];
-    others[newIndex] = tmp;
+    const tmp = others.splice(oldIndex, 1);
+    if (newIndex === others.length) {
+      others.push(tmp[0]);
+    } else {
+      others.splice(newIndex, 0, tmp[0]);
+    }
     this.setState({ others });
   }
 

+ 16 - 6
front/project/admin/routes/interaction/comment/page.js

@@ -5,7 +5,7 @@ import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 // import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { getMap, bindSearch, formatDate, formatTreeData } from '@src/services/Tools';
+import { getMap, bindSearch, formatDate, formatTreeData, flattenTree } from '@src/services/Tools';
 import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
 import { CommentChannel, SwitchSelect, MoneyRange } from '../../../../Constant';
 import { System } from '../../../stores/system';
@@ -13,7 +13,14 @@ import { User } from '../../../stores/user';
 import { Course } from '../../../stores/course';
 
 const SwitchSelectMap = getMap(SwitchSelect, 'value', 'label');
-const CommentChannelMap = getMap(CommentChannel, 'value', 'label');
+const CommentChannelTree = formatTreeData(CommentChannel, 'value', 'label', 'parent');
+const CommentChannelFlatten = flattenTree(CommentChannelTree, (row, item) => {
+  row = Object.assign({}, row);
+  row.value = `${item.value}-${row.value}`;
+  row.label = `${item.label}-${row.label}`;
+  return row;
+}, 'children');
+const CommentChannelMap = getMap(CommentChannelFlatten, 'value', 'label');
 export default class extends Page {
   init() {
     this.itemList = [{
@@ -30,7 +37,7 @@ export default class extends Page {
       type: 'cascader',
       allowClear: true,
       name: '频道',
-      select: formatTreeData(CommentChannel.filter(row => row.type !== 'manual'), 'value', 'label', 'parent'),
+      select: CommentChannelTree,
       placeholder: '请选择',
       onChange: (value) => {
         this.changeSearch(this.filterForm, this, value.join('-'), null);
@@ -127,16 +134,17 @@ export default class extends Page {
       };
     }, this.state.search.userId ? Number(this.state.search.userId) : null, null);
     this.changeSearch(this.filterForm, this, this.state.search.channel, this.state.search.position);
-    this.state.search.channel = this.state.search.channel ? this.state.search.channel.split('-') : '';
   }
 
   changeSearch(list, component, key, value) {
-    if (key === 'course-video' || key === 'course-vs' || key === 'course_data') {
+    if (key === 'course-video' || key === 'course-vs' || key === 'course-package' || key === 'course_data') {
       bindSearch(list, 'position', component, (search) => {
         if (key === 'course-video') {
           return Course.list(Object.assign({ courseModule: 'video' }, search));
         } if (key === 'course-vs') {
           return Course.list(Object.assign({ courseModule: 'vs' }, search));
+        } if (key === 'course-package') {
+          return Course.listPackage(search);
         }
         return Course.listData(search);
       }, (row) => {
@@ -149,6 +157,7 @@ export default class extends Page {
     } else {
       list[1].disabled = true;
     }
+    component.setState({ load: false });
   }
 
   initData() {
@@ -174,12 +183,13 @@ export default class extends Page {
   }
 
   renderView() {
+    const { search } = this.state;
     return <Block flex>
       <FilterLayout
         show
         ref={(ref) => { this.filterF = ref; }}
         itemList={this.filterForm}
-        data={this.state.search}
+        data={Object.assign({}, search, { channel: search.channel ? search.channel.split('-') : '' })}
         onChange={data => {
           data.channel = data.channel.join('-');
           this.search(data);

+ 19 - 6
front/project/admin/routes/interaction/faq/page.js

@@ -5,7 +5,7 @@ import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 // import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { getMap, formatDate, formatTreeData, bindSearch } from '@src/services/Tools';
+import { getMap, formatDate, formatTreeData, bindSearch, flattenTree } from '@src/services/Tools';
 import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
 import { FaqChannel, SwitchSelect, AskStatus, MoneyRange } from '../../../../Constant';
 import { System } from '../../../stores/system';
@@ -13,8 +13,16 @@ import { User } from '../../../stores/user';
 import { Course } from '../../../stores/course';
 
 const SwitchSelectMap = getMap(SwitchSelect, 'value', 'label');
-const FaqChannelMap = getMap(FaqChannel, 'value', 'label');
 const AskStatusMap = getMap(AskStatus, 'value', 'label');
+
+const FaqChannelTree = formatTreeData(FaqChannel, 'value', 'label', 'parent');
+const FaqChannelFlatten = flattenTree(FaqChannelTree, (row, item) => {
+  row = Object.assign({}, row);
+  row.value = `${item.value}-${row.value}`;
+  row.label = `${item.label}-${row.label}`;
+  return row;
+}, 'children');
+const FaqChannelMap = getMap(FaqChannelFlatten, 'value', 'label');
 export default class extends Page {
   init() {
     this.formF = null;
@@ -49,7 +57,7 @@ export default class extends Page {
       type: 'cascader',
       allowClear: true,
       name: '频道',
-      select: formatTreeData(FaqChannel, 'value', 'label', 'parent'),
+      select: FaqChannelTree,
       placeholder: '请选择',
       onChange: (value) => {
         this.changeSearch(this.filterForm, this, value.join('-'), null);
@@ -170,14 +178,17 @@ export default class extends Page {
       };
     }, this.state.search.userId ? Number(this.state.search.userId) : null, null);
     this.changeSearch(this.filterForm, this, this.state.search.channel, this.state.search.position);
-    this.state.search.channel = this.state.search.channel ? this.state.search.channel.split('-') : '';
   }
 
   changeSearch(list, component, key, value) {
-    if (key === 'course-video' || key === 'course_data') {
+    if (key === 'course-video' || key === 'course-vs' || key === 'course-package' || key === 'course_data') {
       bindSearch(list, 'position', component, (search) => {
         if (key === 'course-video') {
           return Course.list(Object.assign({ courseModule: 'video' }, search));
+        } if (key === 'course-vs') {
+          return Course.list(Object.assign({ courseModule: 'vs' }, search));
+        } if (key === 'course-package') {
+          return Course.listPackage(search);
         }
         return Course.listData(search);
       }, (row) => {
@@ -190,6 +201,7 @@ export default class extends Page {
     } else {
       list[1].disabled = true;
     }
+    component.setState({ load: false });
   }
 
   initData() {
@@ -229,12 +241,13 @@ export default class extends Page {
   }
 
   renderView() {
+    const { search } = this.state;
     return <Block flex>
       <FilterLayout
         show
         ref={(ref) => { this.filterF = ref; }}
         itemList={this.filterForm}
-        data={this.state.search}
+        data={Object.assign({}, search, { channel: search.channel ? search.channel.split('-') : '' })}
         onChange={data => {
           data.channel = data.channel.join('-');
           this.search(data);

+ 1 - 0
front/project/admin/routes/interaction/feedback/page.js

@@ -198,6 +198,7 @@ export default class extends Page {
       list[3].disabled = true;
       list[4].disabled = true;
     }
+    component.setState({ load: false });
   }
 
   initData() {

+ 15 - 0
front/project/admin/routes/ready/article/index.js

@@ -0,0 +1,15 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/ready/article',
+  key: 'ready-article',
+  title: '文章',
+  needLogin: true,
+  module,
+  group,
+  index: true,
+  component() {
+    return import('./page');
+  },
+};

+ 3 - 0
front/project/admin/routes/ready/article/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#ready-article {}

+ 61 - 17
front/project/admin/routes/show/article/page.js

@@ -8,26 +8,34 @@ import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
 import { formatDate, getMap } from '@src/services/Tools';
-import { ArticleChannel } from '../../../../Constant';
-import { System } from '../../../stores/system';
+import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
+// import { ArticleChannel } from '../../../../Constant';
+import { Ready } from '../../../stores/ready';
 
-const ArticleChannelMap = getMap(ArticleChannel, 'value', 'label');
+// const ArticleChannelMap = getMap(ArticleChannel, 'value', 'label');
 
 export default class extends Page {
   init() {
+    this.categoryMap = {};
+    this.categoryList = [];
     this.filterForm = [{
-      key: 'channel',
+      key: 'parentCategoryId',
       type: 'select',
       allowClear: true,
-      name: '频道',
-      select: ArticleChannel,
+      name: '一级标题',
       placeholder: '请选择',
+      select: [],
+      number: true,
+      onChange: (value) => {
+        this.changeSearch(this.filterForm, this, value);
+      },
     }, {
-      key: 'position',
+      key: 'categoryId',
       type: 'select',
       allowClear: true,
-      name: '位置',
+      name: '二级标题',
       select: [],
+      number: true,
       placeholder: '请选择',
     }];
     this.actionList = [{
@@ -35,18 +43,21 @@ export default class extends Page {
       type: 'primary',
       name: '创建',
       render: (item) => {
-        return <Link to='/show/article/detail'><Button>{item.name}</Button></Link>;
+        return <Link to='/ready/article/detail'><Button>{item.name}</Button></Link>;
       },
     }];
     this.columns = [{
-      title: '频道',
-      dataIndex: 'channel',
+      title: '一级标题',
+      dataIndex: 'parentCategoryId',
       render: (text) => {
-        return ArticleChannelMap[text] || '';
+        return this.categoryMap[text];
       },
     }, {
-      title: '位置',
-      dataIndex: 'position',
+      title: '二级标题',
+      dataIndex: 'categoryId',
+      render: (text) => {
+        return this.categoryMap[text];
+      },
     }, {
       title: '文章标题',
       dataIndex: 'title',
@@ -61,18 +72,52 @@ export default class extends Page {
       dataIndex: 'handler',
       render: (text, record) => {
         return <div className="table-button">
-          {<Link to={`/show/article/detail/${record.id}`}>编辑</Link>}
+          {<Link to={`/ready/article/detail/${record.id}`}>编辑</Link>}
+          {<a onClick={() => {
+            this.deleteAction(record);
+          }}>删除</a>}
         </div>;
       },
     }];
+    Ready.allCategory().then(result => {
+      this.categoryList = result.map(row => {
+        row.value = row.id;
+        return row;
+      });
+      this.categoryMap = getMap(result, 'id', 'title');
+      this.filterForm[0].select = this.categoryList.filter(row => row.parentId === 0);
+      this.onChangeSearch(this.filterForm, this, this.state.search.parentCategoryId);
+      this.initData();
+    });
+  }
+
+  changeSearch(list, component, value) {
+    if (value) {
+      list[1].disabled = false;
+      list[1].select = this.categoryList.filter(row => row.parentId === value);
+    } else {
+      list[1].disabled = true;
+      list[1].select = [];
+    }
+    component.setState({ load: false });
   }
 
   initData() {
-    System.listArticle(this.state.search).then(result => {
+    Ready.listArticle(this.state.search).then(result => {
       this.setTableData(result.list, result.total);
     });
   }
 
+  deleteAction(row) {
+    asyncDelConfirm('删除确认', '是否删除选中?', () => {
+      const handler = Ready.delArticle({ id: row.id });
+      return handler.then(() => {
+        asyncSMessage('删除成功!');
+        this.refresh();
+      });
+    });
+  }
+
   renderView() {
     return <Block flex>
       <FilterLayout
@@ -88,7 +133,6 @@ export default class extends Page {
         onAction={key => this.onAction(key)}
       />
       <TableLayout
-        select
         columns={this.tableSort(this.columns)}
         list={this.state.list}
         pagination={this.state.page}

+ 16 - 0
front/project/admin/routes/ready/articleDetail/index.js

@@ -0,0 +1,16 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/ready/article/detail',
+  matchPath: '/ready/article/detail/:id?',
+  key: 'ready-article-detail',
+  title: '文章编辑',
+  needLogin: true,
+  module,
+  group,
+  showKey: 'ready-article',
+  component() {
+    return import('./page');
+  },
+};

front/project/admin/routes/show/articleDetail/index.less → front/project/admin/routes/ready/articleDetail/index.less


+ 174 - 0
front/project/admin/routes/ready/articleDetail/page.js

@@ -0,0 +1,174 @@
+import React from 'react';
+import { Form, Input, Button, Row, Col } from 'antd';
+import './index.less';
+import Editor from '@src/components/Editor';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+import Select from '@src/components/Select';
+// import FileUpload from '@src/components/FileUpload';
+import { formatFormError, getMap } from '@src/services/Tools';
+import { asyncSMessage } from '@src/services/AsyncTools';
+import { Ready } from '../../../stores/ready';
+
+export default class extends Page {
+  init() {
+    Ready.allCategory().then(result => {
+      this.categoryList = result.map(row => {
+        row.value = row.id;
+        return row;
+      });
+      this.categoryMap = getMap(result, 'id', 'title');
+      const select = this.categoryList.filter(row => row.parentId === 0);
+      select.push({ label: '其他', value: 0 });
+      this.setState({
+        parentCategoryId: {
+          select,
+        },
+      });
+      this.initData();
+    });
+  }
+
+  onChangeSearch(parentValue, value) {
+    const { setFieldsValue } = this.props.form;
+    const info = {};
+    let showParentCategory = false;
+    let showCategory = false;
+    if (parentValue) {
+      info.disabled = false;
+      info.select = this.categoryList.filter(row => row.parentId === parentValue);
+      info.select.push({ label: '其他', value: 0 });
+      if (value === 0) {
+        showCategory = true;
+      } else if (!value) {
+        setFieldsValue({ categoryId: null });
+      }
+    } else if (parentValue === 0) {
+      showParentCategory = true;
+      showCategory = true;
+      info.disabled = true;
+      info.select = [];
+      setFieldsValue({ categoryId: null });
+    } else {
+      info.disabled = true;
+      info.select = [];
+    }
+    this.setState({
+      showParentCategory,
+      showCategory,
+      categoryId: info,
+    });
+  }
+
+  initData() {
+    if (!this.categoryList) return;
+    const { id } = this.params;
+    const { form } = this.props;
+    let handler;
+    if (id) {
+      handler = Ready.getArticle({ id });
+    } else {
+      handler = Promise.resolve({});
+    }
+
+    handler
+      .then(result => {
+        form.setFieldsValue(result);
+        this.onChangeSearch(result.parentCategoryId, result.categoryId);
+      });
+  }
+
+  submit() {
+    const { form } = this.props;
+    form.validateFields((err) => {
+      if (!err) {
+        const data = form.getFieldsValue();
+        let handler;
+        if (data.id) {
+          handler = Ready.editArticle(data);
+        } else {
+          handler = Ready.addArticle(data);
+        }
+        handler.then(() => {
+          asyncSMessage('保存成功');
+          goBack();
+        }).catch((e) => {
+          if (e.result) form.setFields(formatFormError(data, e.result));
+        });
+      }
+    });
+  }
+
+  renderBase() {
+    const { getFieldDecorator, getFieldValue } = this.props.form;
+    return <Block>
+      <Form>
+        {getFieldDecorator('id')(<input hidden />)}
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='一级标题'>
+          {getFieldDecorator('parentCategoryId', {
+            rules: [
+              { required: !this.state.showParentCategory, message: '请选择标题' },
+            ],
+          })(
+            <Select {...this.state.parentCategoryId} placeholder='请选择标题' onChange={(value) => {
+              this.onChangeSearch(value, null);
+            }} />,
+          )}
+          {this.state.showParentCategory && getFieldDecorator('parentCategory', {
+            rules: [
+              { required: true, message: '请输入标题' },
+            ],
+          })(
+            <Input placeholder='请输入标题' />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='二级标题'>
+          {getFieldDecorator('categoryId', {
+            rules: [
+              { required: !this.state.showCategory, message: '请选择标题' },
+            ],
+          })(
+            <Select {...this.state.categoryId} placeholder='请选择标题' onChange={(value) => {
+              this.onChangeSearch(getFieldValue('parentCategoryId'), value);
+            }} />,
+          )}
+          {this.state.showCategory && getFieldDecorator('category', {
+            rules: [
+              { required: true, message: '请输入标题' },
+            ],
+          })(
+            <Input placeholder='请输入标题' />,
+          )}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='文章标题'>
+          {getFieldDecorator('title', {
+            rules: [
+              { required: true, message: '请输入标题' },
+            ],
+          })(
+            <Input placeholder='请输入标题' />,
+          )}
+        </Form.Item>
+        <Form.Item label='正文'>
+          {getFieldDecorator('content', {
+          })(
+            <Editor placeholder='输入内容' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderView() {
+    return <div flex>
+      {this.renderBase()}
+      <Row type="flex" justify="center">
+        <Col>
+          <Button type="primary" onClick={() => {
+            this.submit();
+          }}>保存</Button>
+        </Col>
+      </Row>
+    </div>;
+  }
+}

+ 15 - 0
front/project/admin/routes/ready/data/index.js

@@ -0,0 +1,15 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/ready/data',
+  key: 'ready-data',
+  title: '备考资料',
+  needLogin: true,
+  module,
+  group,
+  index: true,
+  component() {
+    return import('./page');
+  },
+};

+ 3 - 0
front/project/admin/routes/ready/data/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#ready-data {}

+ 152 - 0
front/project/admin/routes/ready/data/page.js

@@ -0,0 +1,152 @@
+import React from 'react';
+import { Tabs } from 'antd';
+import './index.less';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+import ShowImage from '@src/components/ShowImage';
+import ActionLayout from '@src/layouts/ActionLayout';
+import TableLayout from '@src/layouts/TableLayout';
+// import { formatDate } from '@src/services/Tools';
+import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
+import { Ready } from '../../../stores/ready';
+import { System } from '../../../stores/system';
+
+export default class extends Page {
+  init() {
+    this.itemList = [{
+      key: 'id',
+      type: 'hidden',
+    }, {
+      key: 'title',
+      type: 'input',
+      name: '标题',
+    }, {
+      key: 'content',
+      type: 'textarea',
+      name: '内容',
+    }, {
+      key: 'cover',
+      type: 'image',
+      name: '封面图片',
+      onUpload: ({ file }) => {
+        return System.uploadImage(file).then(result => { return result; });
+      },
+    }];
+
+    this.columns = [{
+      title: '资料封面',
+      dataIndex: 'cover',
+      render: (text) => {
+        return <ShowImage src={text} />;
+      },
+    }, {
+      title: '资料标题',
+      dataIndex: 'title',
+    }, {
+      title: '内容',
+      dataIndex: 'content',
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          {(
+            <a onClick={() => {
+              this.editAction(record);
+            }}>编辑</a>
+          )}
+        </div>;
+      },
+    }];
+
+    this.actionList = [{
+      key: 'add',
+      name: '增加',
+    }];
+  }
+
+  initData() {
+    this.refreshTab(this.state.search.tab || 'official');
+  }
+
+  refreshTab(tab) {
+    const { search } = this.state;
+    search.tab = tab;
+    this.setState({ search });
+    if (tab === 'official') {
+      return this.refreshOfficial();
+    }
+    if (tab === 'unofficial') {
+      return this.refreshUnofficial();
+    }
+    return Promise.reject();
+  }
+
+  refreshOfficial() {
+    return Ready.listData({ isOfficial: true }).then((result) => {
+      this.setState({ list: result.list, total: result.total });
+    });
+  }
+
+  refreshUnofficial() {
+    return Ready.listData({ isOfficial: false }).then((result) => {
+      this.setState({ list: result.list, total: result.total });
+    });
+  }
+
+  addAction() {
+    asyncForm('创建资料', this.itemList, {}, data => {
+      data.isOfficial = this.state.search.tab === 'official' ? 1 : 0;
+      return Ready.addData(data).then(() => {
+        asyncSMessage('添加成功!');
+        this.refresh();
+      });
+    });
+  }
+
+  editAction(row) {
+    asyncForm('编辑资料', this.itemList, row, data => {
+      return Ready.editData(data).then(() => {
+        asyncSMessage('编辑成功!');
+        this.refresh();
+      });
+    });
+  }
+
+  renderOfficial() {
+    return <Block flex>
+      <ActionLayout
+        itemList={this.actionList}
+        selectedKeys={this.state.selectedKeys}
+        onAction={key => this.onAction(key)}
+      />
+      <TableLayout
+        columns={this.tableSort(this.columns)}
+        list={this.state.list}
+        pagination={false}
+        loading={this.props.core.loading}
+      />
+    </Block>;
+  }
+
+  renderUnofficial() {
+    return this.renderOfficial();
+  }
+
+  renderView() {
+    const { search } = this.state;
+    const { tab } = search;
+    return <div>
+      <Tabs activeKey={tab || 'official'} onChange={(value) => {
+        this.search({ tab: value });
+      }}>
+        <Tabs.TabPane tab="官方资料" key="official">
+          {this.renderOfficial()}
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="非官方资料" key="unofficial">
+          {this.renderUnofficial()}
+        </Tabs.TabPane>
+      </Tabs>
+    </div>;
+  }
+}

+ 7 - 2
front/project/admin/routes/ready/index.js

@@ -1,3 +1,8 @@
+import article from './article';
+import articleDetail from './articleDetail';
+import room from './room';
+import data from './data';
+import read from './read';
+import readDetail from './readDetail';
 
-
-export default [];
+export default [article, articleDetail, room, data, read, readDetail];

+ 2 - 2
front/project/admin/routes/show/article/index.js

@@ -2,8 +2,8 @@ import module from '../../module';
 import group from '../group';
 
 export default {
-  path: '/show/article',
-  key: 'show-article',
+  path: '/ready/read',
+  key: 'ready-read',
   title: '推荐阅读',
   needLogin: true,
   module,

+ 3 - 0
front/project/admin/routes/ready/read/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#ready-read {}

+ 190 - 0
front/project/admin/routes/ready/read/page.js

@@ -0,0 +1,190 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Button, Switch, Modal } from 'antd';
+import './index.less';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+import EditTableCell from '@src/components/EditTableCell';
+// import FilterLayout from '@src/layouts/FilterLayout';
+import ActionLayout from '@src/layouts/ActionLayout';
+import TableLayout from '@src/layouts/TableLayout';
+import { formatDate, getMap } from '@src/services/Tools';
+import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
+// import { SwitchSelect } from '../../../../Constant';
+import { Ready } from '../../../stores/ready';
+
+export default class extends Page {
+  init() {
+    this.structMap = {};
+    this.actionList = [{
+      key: 'add',
+      type: 'primary',
+      name: '创建',
+      render: (item) => {
+        return <Link to='/ready/read/detail'><Button>{item.name}</Button></Link>;
+      },
+    }, {
+      key: 'struct',
+      name: '管理板块',
+    }];
+    this.columns = [{
+      title: '板块',
+      dataIndex: 'plate',
+      render: (text) => {
+        return this.structMap[text] || '';
+      },
+    }, {
+      title: '文章标题',
+      dataIndex: 'title',
+    }, {
+      title: '更新时间',
+      dataIndex: 'updateTime',
+      render: (text) => {
+        return formatDate(text);
+      },
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          {<Link to={`/ready/read/detail/${record.id}`}>编辑</Link>}
+          {<a onClick={() => {
+            this.deleteAction(record);
+          }}>删除</a>}
+        </div>;
+      },
+    }];
+
+    this.structColumns = [{
+      title: '板块名称',
+      dataIndex: 'plate',
+      render: (text, record, index) => {
+        return <EditTableCell value={text} onChange={(v) => {
+          this.changeStruct(index, 'plate', v);
+        }} />;
+      },
+    }, {
+      title: '增加跳转',
+      dataIndex: 'jump',
+      render: (text, record, index) => {
+        return <Switch onChange={(value) => {
+          this.changeStruct(index, 'jump', value ? 1 : 0);
+        }} checked={!!text} />;
+      },
+    }, {
+      title: '跳转地址',
+      dataIndex: 'link',
+      render: (text, record, index) => {
+        return <EditTableCell value={text} onChange={(v) => {
+          this.changeStruct(index, 'link', v);
+        }} />;
+      },
+    }, {
+      title: '按钮名称',
+      dataIndex: 'title',
+      render: (text, record, index) => {
+        return <EditTableCell value={text} onChange={(v) => {
+          this.changeStruct(index, 'title', v);
+        }} />;
+      },
+    }];
+    Ready.getReadyRead().then(result => {
+      return this.refreshStruct(result);
+    }).then(() => {
+      this.initData();
+    });
+  }
+
+  initData() {
+    Ready.listRead(this.state.search).then(result => {
+      this.setTableData(result.list, result.total);
+    });
+  }
+
+  refreshStruct(result) {
+    result = result || {};
+    result.plates = (result.plates || []).map((row, index) => { row.value = index + 1; return row; });
+    this.structMap = getMap(result.plates, 'value', 'plate');
+    this.setState({ struct: result });
+  }
+
+  structAction() {
+    const { struct = {} } = this.state;
+    this.open(struct);
+  }
+
+  changeStruct(index, field, value, other) {
+    const { detail } = this.state;
+    if (other !== undefined) {
+      detail.plates.forEach((row) => {
+        row[field] = other;
+      });
+    }
+    detail.plates[index][field] = value;
+    this.setState({ detail });
+  }
+
+  submitStruct() {
+    const { detail } = this.state;
+    Ready.setReadyRead(detail).then(() => {
+      asyncSMessage('保存成功');
+      this.close(false, 'detail');
+      return this.refreshStruct(detail);
+    }).then(() => {
+      return this.initData();
+    });
+  }
+
+  deleteAction(row) {
+    asyncDelConfirm('删除确认', '是否删除选中?', () => {
+      const handler = Ready.delRead({ id: row.id });
+      return handler.then(() => {
+        asyncSMessage('删除成功!');
+        this.refresh();
+      });
+    });
+  }
+
+  renderView() {
+    return <Block flex>
+      {/* <FilterLayout
+        show
+        itemList={this.filterForm}
+        data={this.state.search}
+        onChange={data => {
+          this.search(data);
+        }} /> */}
+      <ActionLayout
+        itemList={this.actionList}
+        selectedKeys={this.state.selectedKeys}
+        onAction={key => this.onAction(key)}
+      />
+      <TableLayout
+        columns={this.tableSort(this.columns)}
+        list={this.state.list}
+        pagination={this.state.page}
+        loading={this.props.core.loading}
+        onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
+        onSelect={(keys, rows) => this.tableSelect(keys, rows)}
+        selectedKeys={this.state.selectedKeys}
+      />
+
+      {this.state.detail && <Modal visible closable title='编辑章节' onCancel={() => {
+        this.close(false, 'detail');
+      }} onOk={() => {
+        this.submitStruct();
+      }}>
+        <TableLayout
+          rowKey={'plate'}
+          columns={this.structColumns}
+          list={this.state.detail.plates || []}
+          pagination={false}
+        />
+        <Button onClick={() => {
+          const { detail } = this.state;
+          detail.plates.push({});
+          this.setState({ detail });
+        }}>增加板块</Button></Modal>}
+    </Block>;
+  }
+}

+ 4 - 4
front/project/admin/routes/show/articleDetail/index.js

@@ -2,14 +2,14 @@ import module from '../../module';
 import group from '../group';
 
 export default {
-  path: '/show/article/detail',
-  matchPath: '/show/article/detail/:id?',
-  key: 'show-article-detail',
+  path: '/ready/read/detail',
+  matchPath: '/ready/read/detail/:id?',
+  key: 'ready-read-detail',
   title: '推荐阅读',
   needLogin: true,
   module,
   group,
-  showKey: 'show-article',
+  showKey: 'ready-read',
   component() {
     return import('./page');
   },

+ 3 - 0
front/project/admin/routes/ready/readDetail/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#ready-read-detail {}

+ 29 - 18
front/project/admin/routes/show/articleDetail/page.js

@@ -6,18 +6,23 @@ import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import Select from '@src/components/Select';
 // import FileUpload from '@src/components/FileUpload';
-import { formatFormError } from '@src/services/Tools';
+import { formatFormError, getMap } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
-import { ArticleChannel } from '../../../../Constant';
-import { System } from '../../../stores/system';
+import { Ready } from '../../../stores/ready';
 
 export default class extends Page {
+  init() {
+    Ready.getReadyRead().then(result => {
+      return this.refreshStruct(result);
+    });
+  }
+
   initData() {
     const { id } = this.params;
     const { form } = this.props;
     let handler;
     if (id) {
-      handler = System.getArticle({ id });
+      handler = Ready.getRead({ id });
     } else {
       handler = Promise.resolve({});
     }
@@ -28,6 +33,21 @@ export default class extends Page {
       });
   }
 
+  refreshStruct(result) {
+    result = result || {};
+    result.plates = (result.plates || []).map((row, index) => { row.value = index + 1; return row; });
+    this.structMap = getMap(result.plates, 'value', 'plate');
+    this.setState({
+      struct: result,
+      plates: result.plates.map((row) => {
+        row.value = `${row.value}`;
+        row.label = row.plate;
+        row.title = '';
+        return row;
+      }),
+    });
+  }
+
   submit() {
     const { form } = this.props;
     form.validateFields((err) => {
@@ -35,9 +55,9 @@ export default class extends Page {
         const data = form.getFieldsValue();
         let handler;
         if (data.id) {
-          handler = System.editArticle(data);
+          handler = Ready.editRead(data);
         } else {
-          handler = System.addArticle(data);
+          handler = Ready.addRead(data);
         }
         handler.then(() => {
           asyncSMessage('保存成功');
@@ -54,22 +74,13 @@ export default class extends Page {
     return <Block>
       <Form>
         {getFieldDecorator('id')(<input hidden />)}
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='频道'>
-          {getFieldDecorator('channel', {
-            rules: [
-              { required: true, message: '请选择' },
-            ],
-          })(
-            <Select select={ArticleChannel} placeholder='请选择' />,
-          )}
-        </Form.Item>
-        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='位置'>
-          {getFieldDecorator('userId', {
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='板块'>
+          {getFieldDecorator('plate', {
             rules: [
               { required: true, message: '请选择' },
             ],
           })(
-            <Select {...this.state.userId} placeholder='请选择' />,
+            <Select select={this.state.plates} placeholder='请选择' />,
           )}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='文章标题'>

+ 15 - 0
front/project/admin/routes/ready/room/index.js

@@ -0,0 +1,15 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/ready/room',
+  key: 'ready-room',
+  title: '考试地址',
+  needLogin: true,
+  module,
+  group,
+  index: true,
+  component() {
+    return import('./page');
+  },
+};

+ 1 - 1
front/project/admin/routes/show/article/index.less

@@ -1,3 +1,3 @@
 @charset "utf-8";
 
-#show-article {}
+#ready-room {}

+ 199 - 0
front/project/admin/routes/ready/room/page.js

@@ -0,0 +1,199 @@
+import React from 'react';
+// import { Link } from 'react-router-dom';
+// import { Button } from 'antd';
+import './index.less';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+import FilterLayout from '@src/layouts/FilterLayout';
+import ActionLayout from '@src/layouts/ActionLayout';
+import TableLayout from '@src/layouts/TableLayout';
+import { asyncSMessage, asyncDelConfirm, asyncForm } from '@src/services/AsyncTools';
+import { getMap } from '@src/services/Tools';
+import { Ready } from '../../../stores/ready';
+import { RoomPosition } from '../../../../Constant';
+
+const RoomPositionMap = getMap(RoomPosition, 'value', 'label');
+
+export default class extends Page {
+  init() {
+    this.filterForm = [{
+      key: 'position',
+      type: 'select',
+      allowClear: true,
+      name: '区域',
+      placeholder: '请选择',
+      select: RoomPosition,
+      onChange: (text) => {
+        this.changeSearch(this.filterForm, this, text);
+      },
+    }, {
+      key: 'areaId',
+      type: 'select',
+      allowClear: true,
+      name: '省份',
+      select: [],
+      number: true,
+      placeholder: '请选择',
+    }];
+    this.formF = null;
+    this.itemList = [{
+      key: 'id',
+      type: 'hidden',
+    }, {
+      key: 'position',
+      type: 'select',
+      name: '区域',
+      select: RoomPosition,
+      onChange: (text) => {
+        this.changeSearch(this.itemList, this.formF, text, 2);
+      },
+      required: true,
+    }, {
+      key: 'areaId',
+      type: 'select',
+      name: '省份',
+      select: [],
+      required: true,
+    }, {
+      key: 'title',
+      type: 'input',
+      name: '考场',
+      required: true,
+    }, {
+      key: 'address',
+      type: 'input',
+      name: '地址',
+      required: true,
+    }, {
+      key: 'description',
+      type: 'textarea',
+      name: '详细说明',
+    }];
+    this.actionList = [{
+      key: 'add',
+      type: 'primary',
+      name: '创建',
+    }];
+    this.columns = [{
+      title: '区域',
+      dataIndex: 'position',
+      render: (text) => {
+        return RoomPositionMap[text] || '';
+      },
+    }, {
+      title: '省份',
+      dataIndex: 'areaId',
+      render: (text) => {
+        return this.areaMap[text] || '';
+      },
+    }, {
+      title: '考场',
+      dataIndex: 'title',
+    }, {
+      title: '地址',
+      dataIndex: 'address',
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          {<a onClick={() => {
+            this.editAction(record);
+          }}>编辑</a>}
+          {<a onClick={() => {
+            this.deleteAction(record);
+          }}>删除</a>}
+        </div>;
+      },
+    }];
+    Ready.allArea().then(result => {
+      this.areaList = result.map(row => {
+        row.value = row.id;
+        return row;
+      });
+      this.areaMap = getMap(result, 'id', 'title');
+      this.changeSearch(this.filterForm, this, this.state.search.position);
+    });
+  }
+
+  changeSearch(list, component, value, index = 1) {
+    if (value) {
+      list[index].select = this.areaList.filter(row => row.position === value);
+      list[index].disabled = list[index].select.length === 0;
+      list[index].required = list[index].select.length > 0;
+    } else {
+      list[index].disabled = true;
+      list[index].required = false;
+      list[index].select = [];
+    }
+    component.setState({ load: false });
+  }
+
+  initData() {
+    Ready.listRoom(this.state.search).then(result => {
+      this.setTableData(result.list, result.total);
+    });
+  }
+
+  addAction() {
+    asyncForm('创建', this.itemList, {}, data => {
+      data.isOverseas = data.position === 'overseas' ? 1 : 0;
+      return Ready.addRoom(data).then(() => {
+        asyncSMessage('添加成功!');
+        this.refresh();
+      });
+    }).then(component => {
+      this.formF = component;
+      this.changeSearch(this.itemList, this.formF, null, 2);
+    });
+  }
+
+  editAction(row) {
+    asyncForm('编辑', this.itemList, row, data => {
+      data.isOverseas = data.position === 'overseas' ? 1 : 0;
+      return Ready.editRoom(data).then(() => {
+        asyncSMessage('编辑成功!');
+        this.refresh();
+      });
+    }).then(component => {
+      this.formF = component;
+      this.changeSearch(this.itemList, this.formF, row.position, 2);
+    });
+  }
+
+  deleteAction(row) {
+    asyncDelConfirm('删除确认', '是否删除选中?', () => {
+      const handler = Ready.delRoom({ id: row.id });
+      return handler.then(() => {
+        asyncSMessage('删除成功!');
+        this.refresh();
+      });
+    });
+  }
+
+  renderView() {
+    return <Block flex>
+      <FilterLayout
+        show
+        itemList={this.filterForm}
+        data={this.state.search}
+        onChange={data => {
+          this.search(data);
+        }} />
+      <ActionLayout
+        itemList={this.actionList}
+        selectedKeys={this.state.selectedKeys}
+        onAction={key => this.onAction(key)}
+      />
+      <TableLayout
+        columns={this.tableSort(this.columns)}
+        list={this.state.list}
+        pagination={this.state.page}
+        loading={this.props.core.loading}
+        onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
+        onSelect={(keys, rows) => this.tableSelect(keys, rows)}
+        selectedKeys={this.state.selectedKeys}
+      />
+    </Block>;
+  }
+}

+ 15 - 0
front/project/admin/routes/setting/contract/index.js

@@ -0,0 +1,15 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/setting/contract',
+  key: 'setting-contract',
+  title: '合同配置',
+  needLogin: true,
+  module,
+  group,
+  index: true,
+  component() {
+    return import('./page');
+  },
+};

+ 3 - 0
front/project/admin/routes/setting/contract/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#setting-explain {}

+ 54 - 0
front/project/admin/routes/setting/contract/page.js

@@ -0,0 +1,54 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import './index.less';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+// import ActionLayout from '@src/layouts/ActionLayout';
+import TableLayout from '@src/layouts/TableLayout';
+import { getMap } from '@src/services/Tools';
+import { System } from '../../../stores/system';
+import { ContractKey } from '../../../../Constant';
+
+const ContractKeyMap = getMap(ContractKey, 'value', 'label');
+
+export default class extends Page {
+  constructor(props) {
+    super(props);
+
+    this.columns = [{
+      title: '协议名称',
+      dataIndex: 'title',
+    }, {
+      title: '应用场景',
+      dataIndex: 'key',
+      render: (text) => {
+        return ContractKeyMap[text] || '';
+      },
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          {<Link to={`/setting/contract/detail/${record.key}`}>修改</Link>}
+        </div>;
+      },
+    }];
+  }
+
+  initData() {
+    System.allContract().then(result => {
+      this.setState({ list: result });
+    });
+  }
+
+  renderView() {
+    return <Block flex>
+      <TableLayout
+        columns={this.tableSort(this.columns)}
+        list={this.state.list}
+        pagination={false}
+        loading={this.props.core.loading}
+      />
+    </Block>;
+  }
+}

+ 16 - 0
front/project/admin/routes/setting/contractDetail/index.js

@@ -0,0 +1,16 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/setting/contract/detail',
+  matchPath: '/setting/contract/detail/:key?',
+  key: 'setting-contract-detail',
+  title: '合同详情',
+  needLogin: true,
+  module,
+  group,
+  showKey: 'setting-contract',
+  component() {
+    return import('./page');
+  },
+};

+ 3 - 0
front/project/admin/routes/setting/contractDetail/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#setting-contract-detail {}

+ 100 - 0
front/project/admin/routes/setting/contractDetail/page.js

@@ -0,0 +1,100 @@
+import React from 'react';
+import { Form, Input, Button, Row, Col } from 'antd';
+import './index.less';
+import Editor from '@src/components/Editor';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+// import FileUpload from '@src/components/FileUpload';
+import { formatFormError, getMap } from '@src/services/Tools';
+import { asyncSMessage } from '@src/services/AsyncTools';
+// import {  } from '../../../../Constant';
+// import { User } from '../../../stores/user';
+import { System } from '../../../stores/system';
+import { ContractKey } from '../../../../Constant';
+
+const ContractKeyMap = getMap(ContractKey, 'value', 'label');
+
+export default class extends Page {
+  initData() {
+    const { key } = this.params;
+    const { form } = this.props;
+    let handler;
+    if (key) {
+      handler = System.getContract({ key });
+    } else {
+      handler = Promise.resolve({});
+    }
+
+    handler
+      .then(result => {
+        form.setFieldsValue(result);
+        this.setState({ data: result });
+      });
+  }
+
+  submit() {
+    const { form } = this.props;
+    form.validateFields((err) => {
+      if (!err) {
+        const data = form.getFieldsValue();
+        System.setContract(data).then(() => {
+          asyncSMessage('保存成功');
+          goBack();
+        }).catch((e) => {
+          if (e.result) form.setFields(formatFormError(data, e.result));
+        });
+      }
+    });
+  }
+
+  renderBase() {
+    const { getFieldDecorator } = this.props.form;
+    const { data } = this.state;
+    return <Block>
+      <h1>协议基本信息</h1>
+      <Form>
+        {getFieldDecorator('key')(<input hidden />)}
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='应用场景'>
+          {ContractKeyMap[data.key] || ''}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='协议标题'>
+          {getFieldDecorator('title', {
+            rules: [
+              { required: true, message: '请输入名称' },
+            ],
+          })(
+            <Input placeholder='请输入名称' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderContent() {
+    const { getFieldDecorator } = this.props.form;
+    return <Block>
+      <Form>
+        <Form.Item label='正文'>
+          {getFieldDecorator('content', {
+          })(
+            <Editor placeholder='输入内容' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderView() {
+    return <div flex>
+      {this.renderBase()}
+      {this.renderContent()}
+      <Row type="flex" justify="center">
+        <Col>
+          <Button type="primary" onClick={() => {
+            this.submit();
+          }}>保存</Button>
+        </Col>
+      </Row>
+    </div>;
+  }
+}

+ 3 - 1
front/project/admin/routes/setting/index.js

@@ -6,5 +6,7 @@ import struct from './struct';
 import time from './time';
 import place from './place';
 import rank from './rank';
+import contract from './contract';
+import contractDetail from './contractDetail';
 
-export default [service, promote, index, struct, time, place, rank];
+export default [service, promote, index, struct, time, place, rank, contract, contractDetail];

+ 69 - 3
front/project/admin/routes/setting/promote/page.js

@@ -9,7 +9,7 @@ import { System } from '../../../stores/system';
 
 export default class extends Page {
   initData() {
-    System.getCoursePromote().then(result => {
+    System.getPromote().then(result => {
       const { form } = this.props;
       (result.video_list || []).forEach((row, index) => {
         form.getFieldDecorator(`video_list[${index}].number`);
@@ -23,6 +23,11 @@ export default class extends Page {
         form.getFieldDecorator(`ask_time[${index}].money`);
         form.getFieldDecorator(`ask_time[${index}].hour`);
       });
+      (result.vs_record || []).forEach((row, index) => {
+        form.getFieldDecorator(`vs_record[${index}].money`);
+        form.getFieldDecorator(`vs_record[${index}].number`);
+      });
+      form.getFieldDecorator('coach.number');
       form.setFieldsValue(flattenObject(result));
       this.setState({ load: true, data: result });
     });
@@ -73,7 +78,7 @@ export default class extends Page {
         data.ask_time = (data.ask_time || []).map(row => {
           return { money: Number(row.money), hour: Number(row.hour) };
         });
-        System.setCoursePromote(data)
+        System.setPromote(data)
           .then(() => {
             this.setState(data);
             asyncSMessage('保存成功');
@@ -154,7 +159,7 @@ export default class extends Page {
   renderVs() {
     const { getFieldDecorator } = this.props.form;
     const { data } = this.state;
-    const vss = data.vs || [];
+    const vss = data.vs_list || [];
     return <Block>
       <h1>1v1课折扣</h1>
       <Form>
@@ -222,6 +227,7 @@ export default class extends Page {
     const { getFieldDecorator } = this.props.form;
     const { data } = this.state;
     const ask_time = data.ask_time || [];
+    const vs_record = data.vs_record || [];
     return <Block>
       <h1>课程赠品</h1>
       <Form>
@@ -264,6 +270,66 @@ export default class extends Page {
         <Button onClick={() => {
           this.addLength('ask_time', { money: 0, hour: 0 });
         }}><Icon type={'plus'} /></Button>
+
+        <Form.Item label='满额送1v1辅导' />
+        {vs_record.map((row, index) => {
+          return <Row>
+            <Col span={6}>
+              <Form.Item labelCol={{ span: 10 }} wrapperCol={{ span: 14 }} label='实付金额'>
+                {getFieldDecorator(`vs_record[${index}].money`, {
+                  rules: [
+                    { required: true, message: '输入金额' },
+                  ],
+                })(
+                  <Input placeholder={'输入金额'} onChange={(value) => {
+                    this.changeMapValue('vs_record', index, 'money', value);
+                  }} />,
+                )}
+              </Form.Item>
+            </Col>
+            <Col span={6}>
+              <Form.Item labelCol={{ span: 10 }} wrapperCol={{ span: 14 }} label='赠送'>
+                {getFieldDecorator(`vs_record[${index}].number`, {
+                  rules: [
+                    { required: true, message: '输入课时' },
+                  ],
+                })(
+                  <Input placeholder={'输入课时'} onChange={(value) => {
+                    this.changeMapValue('vs_record', index, 'number', value);
+                  }} />,
+                )}
+              </Form.Item>
+            </Col>
+            <Col span={1} onClick={() => {
+              this.deleteLength('vs_record', index, 1);
+            }}>
+              <Button><Icon type="minus" /></Button>
+            </Col>
+          </Row>;
+        })}
+        <Button onClick={() => {
+          this.addLength('vs_record', { money: 0, number: 0 });
+        }}><Icon type={'plus'} /></Button>
+
+        <Form.Item label='满课时送复习辅导规划' />
+        <Row>
+          <Col span={6}>
+            <Form.Item labelCol={{ span: 10 }} wrapperCol={{ span: 14 }} label='购买课时'>
+              {getFieldDecorator('coach.number', {
+                rules: [
+                  { required: true, message: '输入课时' },
+                ],
+              })(
+                <Input placeholder={'输入课时'} onChange={(value) => {
+                  this.changeValue('coach', 'number', value);
+                }} />,
+              )}
+            </Form.Item>
+          </Col>
+          <Col span={6}>
+            <Form.Item labelCol={{ span: 1 }}>赠送</Form.Item>
+          </Col>
+        </Row>
       </Form>
     </Block>;
   }

+ 137 - 25
front/project/admin/routes/show/ad/page.js

@@ -1,94 +1,206 @@
 import React from 'react';
-import { Link } from 'react-router-dom';
-import { Button } from 'antd';
+// import { Link } from 'react-router-dom';
+// import { Button } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
-import FilterLayout from '@src/layouts/FilterLayout';
+// import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { formatDate, getMap } from '@src/services/Tools';
-import { ArticleChannel } from '../../../../Constant';
+import { asyncSMessage, asyncForm, asyncDelConfirm } from '@src/services/AsyncTools';
+import { formatDate, getMap, formatTreeData, flattenTree } from '@src/services/Tools';
+import { AdChannel, AdPlace } from '../../../../Constant';
 import { System } from '../../../stores/system';
 
-const ArticleChannelMap = getMap(ArticleChannel, 'value', 'label');
 
+const AdChannelTree = formatTreeData(AdChannel, 'value', 'label', 'parent');
+const AdChannelFlatten = flattenTree(AdChannelTree, (row, item) => {
+  row = Object.assign({}, row);
+  row.value = `${item.value}-${row.value}`;
+  row.label = `${item.label}-${row.label}`;
+  return row;
+}, 'children');
+const AdChannelMap = getMap(AdChannelFlatten, 'value', 'label');
+
+const AdPlaceTree = formatTreeData([].concat(AdChannelFlatten.map(row => Object.assign({}, row)), AdPlace), 'value', 'label', 'parent');
+const AdPlaceChannelMap = getMap(AdPlaceTree, 'value', 'children');
+const AdPlaceMap = getMap(AdPlaceTree, 'value', 'label', 'children');
+console.log(AdPlaceTree, AdPlaceMap);
 export default class extends Page {
   init() {
-    this.filterForm = [{
+    this.formF = null;
+    this.itemList = [{
+      key: 'id',
+      type: 'hidden',
+    }, {
       key: 'channel',
-      type: 'select',
+      type: 'cascader',
       allowClear: true,
       name: '频道',
-      select: ArticleChannel,
+      select: AdChannelTree,
       placeholder: '请选择',
+      onChange: (value) => {
+        this.changeSearch(this.itemList, this.formF, value.join('-'), null, 2);
+      },
     }, {
-      key: 'position',
+      key: 'place',
       type: 'select',
       allowClear: true,
       name: '位置',
       select: [],
       placeholder: '请选择',
+    }, {
+      key: 'title',
+      type: 'input',
+      name: '广告名称',
+    }, {
+      key: 'time',
+      type: 'daterange',
+      name: '广告时间',
+    }, {
+      key: 'image',
+      type: 'image',
+      name: '广告图片',
+      onUpload: ({ file }) => {
+        return System.uploadImage(file).then(result => { return result; });
+      },
+    }, {
+      key: 'link',
+      type: 'input',
+      name: '链接地址',
+    }];
+    this.filterForm = [{
+      key: 'channel',
+      type: 'cascader',
+      allowClear: true,
+      name: '频道',
+      select: AdChannelTree,
+      placeholder: '请选择',
     }];
     this.actionList = [{
       key: 'add',
       type: 'primary',
       name: '创建',
-      render: (item) => {
-        return <Link to='/show/article/detail'><Button>{item.name}</Button></Link>;
-      },
     }];
     this.columns = [{
       title: '频道',
       dataIndex: 'channel',
       render: (text) => {
-        return ArticleChannelMap[text] || '';
+        return AdChannelMap[text] || '';
       },
     }, {
       title: '位置',
-      dataIndex: 'position',
+      dataIndex: 'place',
+      render: (text, record) => {
+        return (AdPlaceMap[record.channel] || {})[text];
+      },
     }, {
-      title: '文章标题',
+      title: '广告名称',
       dataIndex: 'title',
     }, {
-      title: '更新时间',
-      dataIndex: 'updateTime',
-      render: (text) => {
-        return formatDate(text);
+      title: '展示时间',
+      dataIndex: 'time',
+      render: (text, record) => {
+        return record.startTime && record.endTime ? `${formatDate(record.startTime, 'YYYY-MM-DD')}-${formatDate(record.endTime, 'YYYY-MM-DD')}` : '长期';
       },
     }, {
       title: '操作',
       dataIndex: 'handler',
       render: (text, record) => {
         return <div className="table-button">
-          {<Link to={`/show/article/detail/${record.id}`}>编辑</Link>}
+          {(
+            <a onClick={() => {
+              this.editAction(record);
+            }}>编辑</a>
+          )}
+          {(
+            <a onClick={() => {
+              this.deleteAction(record);
+            }}>删除</a>
+          )}
         </div>;
       },
     }];
   }
 
+  changeSearch(list, component, key, value, index = 1) {
+    if (key) {
+      list[index].select = AdPlaceChannelMap[key];
+      list[index].disabled = false;
+    } else {
+      list[index].disabled = true;
+    }
+    component.setFieldsValue({ [list[index].key]: value });
+    component.setState({ load: false });
+  }
+
   initData() {
-    System.listAd(this.state.search).then(result => {
+    System.listAd(Object.assign({}, this.state.search)).then(result => {
       this.setTableData(result.list, result.total);
     });
   }
 
+  addAction() {
+    asyncForm('创建广告', this.itemList, {}, data => {
+      if (data.time && data.time.length > 0) {
+        data.startTime = data.time[0].format('YYYY-MM-DD HH:mm:ss');
+        data.endTime = data.time[1].format('YYYY-MM-DD HH:mm:ss');
+      }
+      data.channel = data.channel.join('-');
+      return System.addAd(data).then(() => {
+        asyncSMessage('添加成功!');
+        this.refresh();
+      });
+    }).then(component => {
+      this.formF = component;
+      this.changeSearch(this.itemList, this.formF, null, null, 2);
+    });
+  }
+
+  editAction(row) {
+    const info = Object.assign({}, row);
+    info.channel = info.channel.split('-');
+    asyncForm('编辑广告', this.itemList, info, data => {
+      if (data.time && data.time.length > 0) {
+        data.startTime = data.time[0].format('YYYY-MM-DD HH:mm:ss');
+        data.endTime = data.time[1].format('YYYY-MM-DD HH:mm:ss');
+      }
+      data.channel = data.channel.join('-');
+      return System.editAd(data).then(() => {
+        asyncSMessage('编辑成功!');
+        this.refresh();
+      });
+    }).then(component => {
+      this.formF = component;
+      this.changeSearch(this.itemList, this.formF, row.channel, row.place, 2);
+    });
+  }
+
+  deleteAction(row) {
+    asyncDelConfirm('删除确认', '是否删除选中?', () => {
+      const handler = System.delAd({ id: row.id });
+      return handler.then(() => {
+        asyncSMessage('删除成功!');
+        this.refresh();
+      });
+    });
+  }
+
   renderView() {
     return <Block flex>
-      <FilterLayout
+      {/* <FilterLayout
         show
         itemList={this.filterForm}
         data={this.state.search}
         onChange={data => {
           this.search(data);
-        }} />
+        }} /> */}
       <ActionLayout
         itemList={this.actionList}
         selectedKeys={this.state.selectedKeys}
         onAction={key => this.onAction(key)}
       />
       <TableLayout
-        select
         columns={this.tableSort(this.columns)}
         list={this.state.list}
         pagination={this.state.page}

+ 107 - 10
front/project/admin/routes/show/comment/page.js

@@ -1,17 +1,26 @@
 import React from 'react';
+import { Row, Button, Switch, Col, List, Icon } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { getMap, formatDate, bindSearch, formatTreeData } from '@src/services/Tools';
+import DragList from '@src/components/DragList';
+import { getMap, formatDate, bindSearch, formatTreeData, flattenTree } from '@src/services/Tools';
 import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
 import { CommentChannel, SystemSelect } from '../../../../Constant';
 import { System } from '../../../stores/system';
 import { Course } from '../../../stores/course';
 
-const CommentChannelMap = getMap(CommentChannel, 'value', 'label');
+const CommentChannelTree = formatTreeData(CommentChannel, 'value', 'label', 'parent');
+const CommentChannelFlatten = flattenTree(CommentChannelTree, (row, item) => {
+  row = Object.assign({}, row);
+  row.value = `${item.value}-${row.value}`;
+  row.label = `${item.label}-${row.label}`;
+  return row;
+}, 'children');
+const CommentChannelMap = getMap(CommentChannelFlatten, 'value', 'label');
 const SystemSelectMap = getMap(SystemSelect, 'value', 'label');
 export default class extends Page {
   init() {
@@ -19,6 +28,21 @@ export default class extends Page {
       key: 'add',
       type: 'primary',
       name: '创建',
+    }, {
+      key: 'switch',
+      name: '切换模式',
+      render: () => {
+        const { channel, position } = this.state.search;
+        let d = false;
+        if (channel === 'course-video' || channel === 'course-vs' || channel === 'course-package' || channel === 'course_data') {
+          d = !position;
+        } else {
+          d = !channel;
+        }
+        return <Switch disabled={d} checked={this.state.mode === 'order'} checkedChildren='排序模式' unCheckedChildren='列表模式' onChange={(value) => {
+          this.changeModel(value);
+        }} />;
+      },
     }];
     this.formF = null;
     this.itemList = [{
@@ -29,7 +53,7 @@ export default class extends Page {
       type: 'cascader',
       allowClear: true,
       name: '频道',
-      select: formatTreeData(CommentChannel, 'value', 'label', 'parent'),
+      select: CommentChannelTree,
       placeholder: '请选择',
       onChange: (value) => {
         this.changeSearch(this.itemList, this.formF, value.join('-'), null, 2);
@@ -127,16 +151,17 @@ export default class extends Page {
       },
     }];
     this.changeSearch(this.filterForm, this, this.state.search.channel, this.state.search.position);
-    this.state.search.channel = this.state.search.channel ? this.state.search.channel.split('-') : '';
   }
 
   changeSearch(list, component, key, value, index = 1) {
-    if (key === 'course-video' || key === 'course-vs' || key === 'course_data') {
+    if (key === 'course-video' || key === 'course-vs' || key === 'course-package' || key === 'course_data') {
       bindSearch(list, 'position', component, (search) => {
         if (key === 'course-video') {
           return Course.list(Object.assign({ courseModule: 'video' }, search));
         } if (key === 'course-vs') {
           return Course.list(Object.assign({ courseModule: 'vs' }, search));
+        } if (key === 'course-package') {
+          return Course.listPackage(search);
         }
         return Course.listData(search);
       }, (row) => {
@@ -149,25 +174,49 @@ export default class extends Page {
     } else {
       list[index].disabled = true;
     }
+    component.setState({ load: false });
   }
 
   initData() {
+    if (!this.state.search.order) {
+      this.state.search.order = 'order';
+      this.state.search.direction = 'desc';
+    }
     System.listComment(Object.assign({ isSpecial: true }, this.state.search)).then(result => {
       this.setTableData(result.list, result.total);
     });
   }
 
+  changeModel(value) {
+    const { search, page } = this.state;
+    if (value) {
+      search.size = page.total;
+      search.order = 'order';
+      search.direction = 'desc';
+      search.isSystem = null;
+      this.setState({ mode: 'order', search });
+    } else {
+      search.size = 20;
+      search.order = null;
+      search.direction = null;
+      this.setState({ mode: null, search });
+    }
+    this.initData();
+  }
+
   addAction() {
     asyncForm('创建评价', this.itemList, {}, data => {
       data.isShow = 1;
       data.isSystem = 1;
       data.isSpecial = 1;
+      data.channel = data.channel.join('-');
       return System.addComment(data).then(() => {
         asyncSMessage('添加成功!');
         this.refresh();
       });
     }).then(component => {
       this.formF = component;
+      this.changeSearch(this.itemList, this.formF, null, null, 2);
     });
   }
 
@@ -176,14 +225,17 @@ export default class extends Page {
     if (row.userId) {
       item = this.userItemList;
     }
-    asyncForm('编辑', item, row, data => {
+    const info = Object.assign({}, row);
+    info.channel = info.channel.split('-');
+    asyncForm('编辑', item, info, data => {
+      data.channel = data.channel.join('-');
       return System.editComment(data).then(() => {
         asyncSMessage('编辑成功!');
         this.refresh();
       });
     }).then(component => {
       this.formF = component;
-      this.changeSearch(this.filterForm, this, row.channel, row.position, 2);
+      this.changeSearch(this.itemList, this.formF, row.channel, row.position, 2);
     });
   }
 
@@ -194,13 +246,33 @@ export default class extends Page {
     });
   }
 
+  order(oldIndex, newIndex) {
+    const { list } = this.state;
+    const tmp = list.splice(oldIndex, 1);
+    if (newIndex === list.length) {
+      list.push(tmp[0]);
+    } else {
+      list.splice(newIndex, 0, tmp[0]);
+    }
+    this.setState({ list });
+  }
+
+  submit() {
+    const { list } = this.state;
+    System.orderComment({ ids: list.map(row => row.id) }).then(() => {
+      asyncSMessage('操作成功!');
+      this.refresh();
+    });
+  }
+
   renderView() {
+    const { search } = this.state;
     return <Block flex>
       <FilterLayout
         show
         ref={(ref) => { this.filterF = ref; }}
         itemList={this.filterForm}
-        data={this.state.search}
+        data={Object.assign({}, search, { channel: search.channel ? search.channel.split('-') : '' })}
         onChange={data => {
           data.channel = data.channel.join('-');
           this.search(data);
@@ -210,7 +282,7 @@ export default class extends Page {
         selectedKeys={this.state.selectedKeys}
         onAction={key => this.onAction(key)}
       />
-      <TableLayout
+      {!this.state.mode && <TableLayout
         columns={this.tableSort(this.columns)}
         list={this.state.list}
         pagination={this.state.page}
@@ -218,7 +290,32 @@ export default class extends Page {
         onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
         onSelect={(keys, rows) => this.tableSelect(keys, rows)}
         selectedKeys={this.state.selectedKeys}
-      />
+      />}
+      {this.state.mode === 'order' && <DragList
+        loading={this.props.core.loading}
+        dataSource={this.state.list || []}
+        handle={'.icon'}
+        rowKey={'id'}
+        onMove={(oldIndex, newIndex) => {
+          this.order(oldIndex, newIndex);
+        }}
+        renderItem={(item) => (
+          <List.Item actions={[<Icon type='bars' className='icon' />]}>
+            <Row style={{ width: '100%' }}>
+              <Col span={8}>昵称:{item.user ? item.user.nickname : item.nickname}</Col>
+              <Col span={15} offset={1}>评论:{item.content}</Col>
+            </Row>
+          </List.Item>
+        )}
+      />}
+      {this.state.mode === 'order' && <Row type="flex" justify="center">
+        <Col>
+          <Button type="primary" onClick={() => {
+            this.submit();
+          }}>保存</Button>
+        </Col>
+      </Row>}
+
     </Block>;
   }
 }

+ 109 - 10
front/project/admin/routes/show/faq/page.js

@@ -1,24 +1,49 @@
 import React from 'react';
+import { Row, Button, Switch, Col, List, Icon } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import FilterLayout from '@src/layouts/FilterLayout';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { getMap, formatDate, bindSearch, formatTreeData } from '@src/services/Tools';
+import DragList from '@src/components/DragList';
+import { getMap, formatDate, bindSearch, formatTreeData, flattenTree } from '@src/services/Tools';
 import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
 import { FaqChannel, SystemSelect } from '../../../../Constant';
 import { System } from '../../../stores/system';
 import { Course } from '../../../stores/course';
 
-const FaqChannelMap = getMap(FaqChannel, 'value', 'label');
 const SystemSelectMap = getMap(SystemSelect, 'value', 'label');
+
+const FaqChannelTree = formatTreeData(FaqChannel, 'value', 'label', 'parent');
+const FaqChannelFlatten = flattenTree(FaqChannelTree, (row, item) => {
+  row = Object.assign({}, row);
+  row.value = `${item.value}-${row.value}`;
+  row.label = `${item.label}-${row.label}`;
+  return row;
+}, 'children');
+const FaqChannelMap = getMap(FaqChannelFlatten, 'value', 'label');
 export default class extends Page {
   init() {
     this.actionList = [{
       key: 'add',
       type: 'primary',
       name: '创建',
+    }, {
+      key: 'switch',
+      name: '切换模式',
+      render: () => {
+        const { channel, position } = this.state.search;
+        let d = false;
+        if (channel === 'course-video' || channel === 'course-vs' || channel === 'course-package' || channel === 'course_data') {
+          d = !position;
+        } else {
+          d = !channel;
+        }
+        return <Switch disabled={d} checked={this.state.mode === 'order'} checkedChildren='排序模式' unCheckedChildren='列表模式' onChange={(value) => {
+          this.changeModel(value);
+        }} />;
+      },
     }];
     this.formF = null;
     this.itemList = [{
@@ -28,7 +53,7 @@ export default class extends Page {
       key: 'channel',
       type: 'cascader',
       name: '频道',
-      select: formatTreeData(FaqChannel, 'value', 'label', 'parent'),
+      select: FaqChannelTree,
       placeholder: '请选择',
       onChange: (value) => {
         this.changeSearch(this.itemList, this.formF, value.join('-'), null, 2);
@@ -112,14 +137,17 @@ export default class extends Page {
     }];
 
     this.changeSearch(this.filterForm, this, this.state.search.channel, this.state.search.position);
-    this.state.search.channel = this.state.search.channel ? this.state.search.channel.split('-') : '';
   }
 
   changeSearch(list, component, key, value, index = 1) {
-    if (key === 'course-video' || key === 'course_data') {
+    if (key === 'course-video' || key === 'course-vs' || key === 'course-package' || key === 'course_data') {
       bindSearch(list, 'position', component, (search) => {
         if (key === 'course-video') {
           return Course.list(Object.assign({ courseModule: 'video' }, search));
+        } if (key === 'course-vs') {
+          return Course.list(Object.assign({ courseModule: 'vs' }, search));
+        } if (key === 'course-package') {
+          return Course.listPackage(search);
         }
         return Course.listData(search);
       }, (row) => {
@@ -132,37 +160,64 @@ export default class extends Page {
     } else {
       list[index].disabled = true;
     }
+    component.setState({ load: false });
   }
 
   initData() {
+    if (!this.state.search.order) {
+      this.state.search.order = 'order';
+      this.state.search.direction = 'desc';
+    }
     System.listFAQ(Object.assign({ isSpecial: true }, this.state.search)).then(result => {
       this.setTableData(result.list, result.total);
     });
   }
 
+  changeModel(value) {
+    const { search, page } = this.state;
+    if (value) {
+      search.order = 'order';
+      search.direction = 'desc';
+      search.size = page.total;
+      search.isSystem = null;
+      this.setState({ mode: 'order', search });
+    } else {
+      search.size = 20;
+      search.order = null;
+      search.direction = null;
+      this.setState({ mode: null, search });
+    }
+    this.initData();
+  }
+
   addAction() {
     asyncForm('创建', this.itemList, {}, data => {
       data.isShow = 1;
       data.isSystem = 1;
       data.isSpecial = 1;
+      data.channel = data.channel.join('-');
       return System.addFAQ(data).then(() => {
         asyncSMessage('添加成功!');
         this.refresh();
       });
     }).then(component => {
       this.formF = component;
+      this.changeSearch(this.itemList, this.formF, null, null, 2);
     });
   }
 
   editAction(row) {
-    asyncForm('编辑', this.itemList, row, data => {
+    const info = Object.assign({}, row);
+    info.channel = info.channel.split('-');
+    asyncForm('编辑', this.itemList, info, data => {
+      data.channel = data.channel.join('-');
       return System.editFAQ(data).then(() => {
         asyncSMessage('编辑成功!');
         this.refresh();
       });
     }).then(component => {
       this.formF = component;
-      this.changeSearch(this.filterForm, this, row.channel, row.position, 2);
+      this.changeSearch(this.itemList, this.formF, row.channel, row.position, 2);
     });
   }
 
@@ -173,13 +228,33 @@ export default class extends Page {
     });
   }
 
+  order(oldIndex, newIndex) {
+    const { list } = this.state;
+    const tmp = list.splice(oldIndex, 1);
+    if (newIndex === list.length) {
+      list.push(tmp[0]);
+    } else {
+      list.splice(newIndex, 0, tmp[0]);
+    }
+    this.setState({ list });
+  }
+
+  submit() {
+    const { list } = this.state;
+    System.orderComment({ ids: list.map(row => row.id) }).then(() => {
+      asyncSMessage('操作成功!');
+      this.refresh();
+    });
+  }
+
   renderView() {
+    const { search } = this.state;
     return <Block flex>
       <FilterLayout
         show
         ref={(ref) => { this.filterF = ref; }}
         itemList={this.filterForm}
-        data={this.state.search}
+        data={Object.assign({}, search, { channel: search.channel ? search.channel.split('-') : '' })}
         onChange={data => {
           data.channel = data.channel.join('-');
           this.search(data);
@@ -189,7 +264,7 @@ export default class extends Page {
         selectedKeys={this.state.selectedKeys}
         onAction={key => this.onAction(key)}
       />
-      <TableLayout
+      {!this.state.mode && <TableLayout
         columns={this.tableSort(this.columns)}
         list={this.state.list}
         pagination={this.state.page}
@@ -197,7 +272,31 @@ export default class extends Page {
         onChange={(pagination, filters, sorter) => this.tableChange(pagination, filters, sorter)}
         onSelect={(keys, rows) => this.tableSelect(keys, rows)}
         selectedKeys={this.state.selectedKeys}
-      />
+      />}
+      {this.state.mode === 'order' && <DragList
+        loading={this.props.core.loading}
+        dataSource={this.state.list || []}
+        handle={'.icon'}
+        rowKey={'id'}
+        onMove={(oldIndex, newIndex) => {
+          this.order(oldIndex, newIndex);
+        }}
+        renderItem={(item) => (
+          <List.Item actions={[<Icon type='bars' className='icon' />]}>
+            <Row style={{ width: '100%' }}>
+              <Col span={8}>{item.user ? item.user.nickname : item.nickname}</Col>
+              <Col span={15} offset={1}>{item.content}</Col>
+            </Row>
+          </List.Item>
+        )}
+      />}
+      {this.state.mode === 'order' && <Row type="flex" justify="center">
+        <Col>
+          <Button type="primary" onClick={() => {
+            this.submit();
+          }}>保存</Button>
+        </Col>
+      </Row>}
     </Block>;
   }
 }

+ 2 - 3
front/project/admin/routes/show/index.js

@@ -1,11 +1,10 @@
 
 import tips from './tips';
 import faq from './faq';
-import article from './article';
-import articleDetail from './articleDetail';
 import comment from './comment';
 import ad from './ad';
 import message from './message';
+import messageDetail from './messageDetail';
 import deploy from './deploy';
 
-export default [tips, faq, article, articleDetail, comment, ad, message, deploy];
+export default [tips, faq, comment, ad, message, messageDetail, deploy];

+ 157 - 119
front/project/admin/routes/show/message/page.js

@@ -1,13 +1,19 @@
 import React from 'react';
-import { Button, Tabs, Row, Col, Form, Input } from 'antd';
+import { Tabs } from 'antd';
+import { Link } from 'react-router-dom';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
 import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
-import { formatFormError, formatDate } from '@src/services/Tools';
+import { formatDate, getMap } from '@src/services/Tools';
 import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
 import { System } from '../../../stores/system';
+import { MessageCategory, MessageEmailStatus, MessageCustomStatus } from '../../../../Constant';
+
+const MessageCategoryMap = getMap(MessageCategory, 'value', 'label');
+const MessageEmailStatusMap = getMap(MessageEmailStatus, 'value', 'label');
+const MessageCustomStatusMap = getMap(MessageCustomStatus, 'value', 'label');
 
 export default class extends Page {
   init() {
@@ -45,11 +51,17 @@ export default class extends Page {
         return formatDate(text);
       },
     }, {
+      title: '状态',
+      dataIndex: 'sendStatus',
+      render: (text) => {
+        return MessageCustomStatusMap[text] || '';
+      },
+    }, {
       title: '操作',
       dataIndex: 'handler',
       render: (text, record) => {
         return <div className="table-button">
-          {(
+          {record.sendStatus === 0 && (
             <a onClick={() => {
               this.editAction(record);
             }}>编辑</a>
@@ -58,74 +70,135 @@ export default class extends Page {
       },
     }];
 
+    this.insideColumns = [{
+      title: '站内信标题',
+      dataIndex: 'title',
+    }, {
+      title: '触发场景',
+      dataIndex: 'messageCategory',
+      render: (text) => {
+        return MessageCategoryMap[text] || '';
+      },
+    }, {
+      title: '发送数量',
+      dataIndex: 'sendNumber',
+    }, {
+      title: '状态',
+      dataIndex: 'sendStatus',
+      render: (text) => {
+        return MessageEmailStatusMap[text] || '';
+      },
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          {(
+            <Link to={`/show/message/detail/${record.id}`}>编辑</Link>
+          )}
+          {record.sendStatus === 0 && (
+            <a onClick={() => {
+              this.openAction(record);
+            }}>开启</a>
+          )}
+          {record.sendStatus === 1 && (
+            <a onClick={() => {
+              this.closeAction(record);
+            }}>关闭</a>
+          )}
+        </div>;
+      },
+    }];
+
+    this.emailColumns = [{
+      title: '邮件标题',
+      dataIndex: 'title',
+    }, {
+      title: '触发场景',
+      dataIndex: 'messageCategory',
+      render: (text) => {
+        return MessageCategoryMap[text] || '';
+      },
+    }, {
+      title: '发送数量',
+      dataIndex: 'sendNumber',
+    }, {
+      title: '状态',
+      dataIndex: 'sendStatus',
+      render: (text) => {
+        return MessageEmailStatusMap[text] || '';
+      },
+    }, {
+      title: '操作',
+      dataIndex: 'handler',
+      render: (text, record) => {
+        return <div className="table-button">
+          {(
+            <Link to={`/show/message/detail/${record.id}`}>编辑</Link>
+          )}
+          {record.sendStatus === 0 && (
+            <a onClick={() => {
+              this.openAction(record);
+            }}>开启</a>
+          )}
+          {record.sendStatus === 1 && (
+            <a onClick={() => {
+              this.closeAction(record);
+            }}>关闭</a>
+          )}
+        </div>;
+      },
+    }];
+
     this.actionList = [{
       key: 'add',
       name: '增加',
     }];
-
-    this.state.tab = 'template';
   }
 
   initData() {
-    this.refresh(this.state.tab);
+    this.refreshTab(this.state.search.tab || 'inside');
   }
 
-  refresh(tab) {
-    if (tab === 'template') {
-      return this.refreshTemplate();
+  refreshTab(tab) {
+    const { search } = this.state;
+    search.tab = tab;
+    this.setState({ search });
+    if (tab === 'inside') {
+      return this.refreshInside();
     }
     if (tab === 'custom') {
       return this.refreshCustom();
     }
+    if (tab === 'email') {
+      return this.refreshEmail();
+    }
     return Promise.reject();
   }
 
-  refreshTemplate() {
-    const { form } = this.props;
-    return System.getMessageTemplate().then(result => {
-      form.setFieldsValue(result);
-      this.setState({ template: result || {} });
+  refreshInside() {
+    return System.listMessage({ messageMethod: 'inside', needCustom: false }).then((result) => {
+      this.setState({ list: result.list, total: result.total });
     });
   }
 
   refreshCustom() {
-    return System.listMessage().then((result) => {
+    return System.listMessage({ messageMethod: 'inside', messageCategory: 'custom' }).then((result) => {
       this.setState({ list: result.list, total: result.total });
     });
   }
 
-  submit(tab) {
-    if (tab === 'template') {
-      this.submitTemplate();
-    }
-  }
-
-  submitTemplate() {
-    const { form } = this.props;
-    form.validateFields((err) => {
-      if (!err) {
-        const data = form.getFieldsValue();
-        System.setMessageTemplate(data)
-          .then(() => {
-            this.setState(data);
-            asyncSMessage('保存成功');
-          }).catch((e) => {
-            form.setFields(formatFormError(data, e.result));
-          });
-      }
+  refreshEmail() {
+    return System.listMessage({ messageMethod: 'email', needCustom: false }).then((result) => {
+      this.setState({ list: result.list, total: result.total });
     });
   }
 
-  submitCustom() {
-    const { exercise } = this.state;
-    return System.setExerciseTime(exercise);
-  }
-
   addAction() {
     asyncForm('创建消息', this.itemList, {}, data => {
       return System.addMessage(data).then(() => {
         asyncSMessage('添加成功!');
-        this.refresh('custom');
+        this.refreshTab('custom');
       });
     });
   }
@@ -134,75 +207,34 @@ export default class extends Page {
     asyncForm('编辑消息', this.itemList, row, data => {
       return System.editMessage(data).then(() => {
         asyncSMessage('编辑成功!');
-        this.refresh('custom');
+        this.refreshTab('custom');
       });
     });
   }
 
-  renderTemplate() {
-    const { getFieldDecorator } = this.props.form;
-    return <div>
-      <Block>
-        <h1>购买消息</h1>
-        <Form>
-          <Form.Item help='可插入自定义字段' />
-          <Form.Item>
-            {getFieldDecorator('pay.content', {
-              rules: [
-                { required: false, message: '请输入购买消息内容' },
-              ],
-            })(
-              <Input.TextArea />,
-            )}
-          </Form.Item>
-          <Form.Item label='链接地址'>
-            {getFieldDecorator('pay.link')(
-              <Input placeholder='请输入跳转地址' />,
-            )}
-          </Form.Item>
-        </Form>
-      </Block>
-      <Block>
-        <h1>换库消息</h1>
-        <Form>
-          <Form.Item help='可插入自定义字段' />
-          <Form.Item>
-            {getFieldDecorator('library.content', {
-              rules: [
-                { required: false, message: '请输入换库消息内容' },
-              ],
-            })(
-              <Input.TextArea />,
-            )}
-          </Form.Item>
-          <Form.Item label='链接地址'>
-            {getFieldDecorator('library.link')(
-              <Input placeholder='请输入跳转地址' />,
-            )}
-          </Form.Item>
-        </Form>
-      </Block>
-      <Block>
-        <h1>课程消息</h1>
-        <Form>
-          <Form.Item help='可插入自定义字段' />
-          <Form.Item>
-            {getFieldDecorator('course.content', {
-              rules: [
-                { required: false, message: '请输入课程消息内容' },
-              ],
-            })(
-              <Input.TextArea />,
-            )}
-          </Form.Item>
-          <Form.Item label='链接地址'>
-            {getFieldDecorator('course.link')(
-              <Input placeholder='请输入跳转地址' />,
-            )}
-          </Form.Item>
-        </Form>
-      </Block>
-    </div>;
+  openAction(row) {
+    System.editMessage({ id: row.id, sendStatus: 1 }).then(() => {
+      asyncSMessage('操作成功!');
+      this.refresh();
+    });
+  }
+
+  closeAction(row) {
+    System.editMessage({ id: row.id, sendStatus: 0 }).then(() => {
+      asyncSMessage('操作成功!');
+      this.refresh();
+    });
+  }
+
+  renderInside() {
+    return <Block flex>
+      <TableLayout
+        columns={this.tableSort(this.insideColumns)}
+        list={this.state.list}
+        pagination={false}
+        loading={this.props.core.loading}
+      />
+    </Block>;
   }
 
   renderCustom() {
@@ -221,28 +253,34 @@ export default class extends Page {
     </Block>;
   }
 
+  renderEmail() {
+    return <Block flex>
+      <TableLayout
+        columns={this.tableSort(this.emailColumns)}
+        list={this.state.list}
+        pagination={false}
+        loading={this.props.core.loading}
+      />
+    </Block>;
+  }
+
   renderView() {
-    const { tab } = this.state;
+    const { search } = this.state;
+    const { tab } = search;
     return <div>
-      <Tabs activeKey={tab} onChange={(value) => {
-        this.setState({ tab: value, selectedKeys: [], checkedKeys: [] });
-        this.refresh(value);
+      <Tabs activeKey={tab || 'inside'} onChange={(value) => {
+        this.search({ tab: value });
       }}>
-        <Tabs.TabPane tab="模版消息" key="template">
-          {this.renderTemplate()}
+        <Tabs.TabPane tab="模版消息" key="inside">
+          {this.renderInside()}
         </Tabs.TabPane>
         <Tabs.TabPane tab="自定义消息" key="custom">
           {this.renderCustom()}
         </Tabs.TabPane>
+        <Tabs.TabPane tab="邮件模版" key="email">
+          {this.renderEmail()}
+        </Tabs.TabPane>
       </Tabs>
-      {tab !== 'custom' && <Row type="flex" justify="center">
-        <Col>
-          <Button type="primary" onClick={() => {
-            this.submit(tab);
-          }}>保存</Button>
-        </Col>
-      </Row>}
-
     </div>;
   }
 }

+ 16 - 0
front/project/admin/routes/show/messageDetail/index.js

@@ -0,0 +1,16 @@
+import module from '../../module';
+import group from '../group';
+
+export default {
+  path: '/show/message/detail',
+  matchPath: '/show/message/detail/:id?',
+  key: 'show-message-detail',
+  title: '消息详情',
+  needLogin: true,
+  module,
+  group,
+  showKey: 'show-message',
+  component() {
+    return import('./page');
+  },
+};

+ 3 - 0
front/project/admin/routes/show/messageDetail/index.less

@@ -0,0 +1,3 @@
+@charset "utf-8";
+
+#show-message-detail {}

+ 112 - 0
front/project/admin/routes/show/messageDetail/page.js

@@ -0,0 +1,112 @@
+import React from 'react';
+import { Form, Input, Button, Row, Col } from 'antd';
+import './index.less';
+import Editor from '@src/components/Editor';
+import Page from '@src/containers/Page';
+import Block from '@src/components/Block';
+// import FileUpload from '@src/components/FileUpload';
+import { formatFormError, getMap } from '@src/services/Tools';
+import { asyncSMessage } from '@src/services/AsyncTools';
+// import {  } from '../../../../Constant';
+// import { User } from '../../../stores/user';
+import { System } from '../../../stores/system';
+import { MessageCategory } from '../../../../Constant';
+
+const MessageCategoryMap = getMap(MessageCategory, 'value', 'label');
+const MessageCategoryParamsMap = getMap(MessageCategory, 'value', 'params');
+
+export default class extends Page {
+  initData() {
+    const { id } = this.params;
+    const { form } = this.props;
+    let handler;
+    if (id) {
+      handler = System.getMessage({ id });
+    } else {
+      handler = Promise.resolve({});
+    }
+
+    handler
+      .then(result => {
+        form.setFieldsValue(result);
+        this.setState({ data: result });
+      });
+  }
+
+  submit() {
+    const { form } = this.props;
+    form.validateFields((err) => {
+      if (!err) {
+        const data = form.getFieldsValue();
+        System.setContract(data).then(() => {
+          asyncSMessage('保存成功');
+          goBack();
+        }).catch((e) => {
+          if (e.result) form.setFields(formatFormError(data, e.result));
+        });
+      }
+    });
+  }
+
+  renderBase() {
+    const { getFieldDecorator } = this.props.form;
+    const { data } = this.state;
+    return <Block>
+      <h1>{data.messageMethod === 'email' && '邮件模版'}{data.messageMethod === 'inside' && '站内信模版'}</h1>
+      <Form>
+        {getFieldDecorator('id')(<input hidden />)}
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='触发场景'>
+          {MessageCategoryMap[data.messageCategory] || ''}
+        </Form.Item>
+        <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='消息标题'>
+          {getFieldDecorator('title', {
+            rules: [
+              { required: true, message: '请输入名称' },
+            ],
+          })(
+            <Input placeholder='请输入名称' />,
+          )}
+        </Form.Item>
+        {data.messageMethod === 'inside' && <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='跳转链接'>
+          {getFieldDecorator('link', {
+            rules: [
+              { required: true, message: '请输入链接' },
+            ],
+          })(
+            <Input placeholder='请输入链接' />,
+          )}
+        </Form.Item>}
+      </Form>
+    </Block>;
+  }
+
+  renderContent() {
+    const { getFieldDecorator } = this.props.form;
+    const { data } = this.state;
+    return <Block>
+      <Form>
+        <Form.Item label='正文'>
+          可插入自定义字段:{(MessageCategoryParamsMap[data.messageCategory] || []).map(row => `{${row}}`).join(', ')}
+          {getFieldDecorator('content', {
+          })(
+            <Editor placeholder='输入内容' />,
+          )}
+        </Form.Item>
+      </Form>
+    </Block>;
+  }
+
+  renderView() {
+    return <div flex>
+      {this.renderBase()}
+      {this.renderContent()}
+      <Row type="flex" justify="center">
+        <Col>
+          <Button type="primary" onClick={() => {
+            this.submit();
+          }}>保存</Button>
+        </Col>
+      </Row>
+    </div>;
+  }
+}

+ 6 - 3
front/project/admin/routes/student/askCourseDetail/page.js

@@ -50,9 +50,12 @@ export default class extends Page {
   orderQuestion(oldIndex, newIndex) {
     const { data } = this.state;
     const { others = [] } = data;
-    const tmp = others[oldIndex];
-    others[oldIndex] = others[newIndex];
-    others[newIndex] = tmp;
+    const tmp = others.splice(oldIndex, 1);
+    if (newIndex === others.length) {
+      others.push(tmp[0]);
+    } else {
+      others.splice(newIndex, 0, tmp[0]);
+    }
     this.setState({ others });
   }
 

+ 16 - 0
front/project/admin/routes/user/abnormal/page.js

@@ -72,6 +72,13 @@ export default class extends Page {
       dataIndex: 'handler',
       render: (text, record) => {
         return <div className="table-button">
+          {record.isIgnore && '已忽略'}
+          {record.isAlert && '已警告'}
+          {record.isAlert && record.user.isFrozen && (
+            <a onClick={() => {
+              this.noFrozenAction(record);
+            }}>取消封禁</a>
+          )}
           {!record.isIgnore && !record.isAlert && (
             <a onClick={() => {
               this.alertAction(record);
@@ -133,6 +140,15 @@ export default class extends Page {
     });
   }
 
+  noFrozenAction(row) {
+    asyncDelConfirm('取消封禁确认', '是否取消封禁选中用户?', () => {
+      return User.noFrozen({ id: row.userId }).then(() => {
+        asyncSMessage('操作成功!');
+        this.refresh();
+      });
+    });
+  }
+
   renderView() {
     return <Block flex>
       <FilterLayout

+ 13 - 1
front/project/admin/routes/user/detail/page.js

@@ -95,6 +95,16 @@ export default class extends Page {
     });
   }
 
+  noFrozenAction() {
+    asyncDelConfirm('操作确认', '是否要取消封禁账户?取消后账户可以使用网站', () => {
+      return User.frozen({ id: this.state.data.id })
+        .then(() => {
+          asyncSMessage('操作成功!');
+          this.refresh();
+        });
+    });
+  }
+
   refreshService(p, size) {
     const { id } = this.params;
     const { page } = this.state;
@@ -113,7 +123,9 @@ export default class extends Page {
       <h1>用户基本信息{data.isFrozen === 0 && <Button type='danger' onClick={() => {
         this.frozenAction();
       }}>封禁账户</Button>}
-        {data.isFrozen === 1 && '已封禁'}</h1>
+        {data.isFrozen === 1 && <Button type='danger' onClick={() => {
+          this.noFrozenAction();
+        }}>取消封禁</Button>}</h1>
 
       <Form>
         <div className="group">

+ 2 - 1
front/project/admin/routes/user/list/page.js

@@ -34,13 +34,14 @@ export default class extends Page {
       type: 'select',
       name: '国际码',
       select: MobileArea,
+      required: true,
     }, {
       key: 'mobile',
       type: 'input',
       name: '手机号',
       placeholder: '请输入',
+      required: true,
       option: {
-        rules: [{ required: true, message: '请输入手机号' }],
         normalize: (value) => {
           if (this.mobile === value) return value;
           if (this.timeout) {

+ 2 - 0
front/project/admin/routes/user/orderDetail/page.js

@@ -41,6 +41,8 @@ export default class extends Page {
       <h1>包含商品</h1>
       <Form>
         {(checkouts || []).map(row => {
+          // 赠送过滤
+          if (row.source.indexOf('gift') >= 0) return null;
           let title = '';
           switch (row.productType) {
             case 'course':

+ 1 - 0
front/project/admin/routes/user/recordAll/page.js

@@ -258,6 +258,7 @@ export default class extends Page {
       list[2].disabled = true;
       list[3].disabled = true;
     }
+    component.setState({ load: false });
   }
 
   initData() {

+ 1 - 0
front/project/admin/routes/user/recordBuy/page.js

@@ -155,6 +155,7 @@ export default class extends Page {
       list[2].disabled = true;
       list[3].disabled = true;
     }
+    component.setState({ load: false });
   }
 
   initData() {

+ 2 - 1
front/project/admin/stores/index.js

@@ -7,6 +7,7 @@ import { User } from './user';
 import { Sentence } from './sentence';
 import { Question } from './question';
 import { Course } from './course';
+import { Ready } from './ready';
 import { Slient } from './slient';
 
-export default [System, Examination, Exercise, Preview, Textbook, User, Sentence, Question, Course, Slient];
+export default [System, Examination, Exercise, Preview, Textbook, User, Sentence, Question, Course, Ready, Slient];

+ 103 - 0
front/project/admin/stores/ready.js

@@ -0,0 +1,103 @@
+
+
+import BaseStore from '@src/stores/base';
+
+export default class ReadyStore extends BaseStore {
+  getReadyRead() {
+    return this.apiGet('/setting/ready_read');
+  }
+
+  setReadyRead(params) {
+    return this.apiPut('/setting/ready_read', params);
+  }
+
+  allCategory(params) {
+    return this.apiGet('/ready/category/all', params);
+  }
+
+  listArticle(params) {
+    return this.apiGet('/ready/article/list', params);
+  }
+
+  addArticle(params) {
+    return this.apiPost('/ready/article/add', params);
+  }
+
+  editArticle(params) {
+    return this.apiPut('/ready/article/edit', params);
+  }
+
+  delArticle(params) {
+    return this.apiDel('/ready/article/delete', params);
+  }
+
+  getArticle(params) {
+    return this.apiGet('/ready/article/detail', params);
+  }
+
+  allArea(params) {
+    return this.apiGet('/ready/area/all', params);
+  }
+
+  listRoom(params) {
+    return this.apiGet('/ready/room/list', params);
+  }
+
+  addRoom(params) {
+    return this.apiPost('/ready/room/add', params);
+  }
+
+  editRoom(params) {
+    return this.apiPut('/ready/room/edit', params);
+  }
+
+  delRoom(params) {
+    return this.apiDel('/ready/room/delete', params);
+  }
+
+  getRoom(params) {
+    return this.apiGet('/ready/room/detail', params);
+  }
+
+  listRead(params) {
+    return this.apiGet('/ready/read/list', params);
+  }
+
+  addRead(params) {
+    return this.apiPost('/ready/read/add', params);
+  }
+
+  editRead(params) {
+    return this.apiPut('/ready/read/edit', params);
+  }
+
+  delRead(params) {
+    return this.apiDel('/ready/read/delete', params);
+  }
+
+  getRead(params) {
+    return this.apiGet('/ready/read/detail', params);
+  }
+
+  listData(params) {
+    return this.apiGet('/ready/data/list', params);
+  }
+
+  addData(params) {
+    return this.apiPost('/ready/data/add', params);
+  }
+
+  editData(params) {
+    return this.apiPut('/ready/data/edit', params);
+  }
+
+  delData(params) {
+    return this.apiDel('/ready/data/delete', params);
+  }
+
+  getData(params) {
+    return this.apiGet('/ready/data/detail', params);
+  }
+}
+
+export const Ready = new ReadyStore({ key: 'ready' });

+ 24 - 28
front/project/admin/stores/system.js

@@ -29,14 +29,6 @@ export default class SystemStore extends BaseStore {
     return this.apiPut('/setting/index', params);
   }
 
-  getMessageTemplate() {
-    return this.apiGet('/setting/message_template');
-  }
-
-  setMessageTemplate(params) {
-    return this.apiPut('/setting/message_template', params);
-  }
-
   getPlace() {
     return this.apiGet('/setting/place');
   }
@@ -173,6 +165,18 @@ export default class SystemStore extends BaseStore {
     return this.apiPut('/setting/sentence_info', params);
   }
 
+  allContract(params) {
+    return this.apiGet('/setting/contract/all', params);
+  }
+
+  getContract(params) {
+    return this.apiGet('/setting/contract/detail', params);
+  }
+
+  setContract(params) {
+    return this.apiPut('/setting/contract/edit', params);
+  }
+
   listRank(params) {
     return this.apiGet('/setting/rank/list', params);
   }
@@ -205,6 +209,10 @@ export default class SystemStore extends BaseStore {
     return this.apiPut('/setting/comment/edit', params);
   }
 
+  orderComment(params) {
+    return this.apiPut('/setting/comment/order', params);
+  }
+
   delComment(params) {
     return this.apiDel('/setting/comment/delete', params);
   }
@@ -221,6 +229,10 @@ export default class SystemStore extends BaseStore {
     return this.apiPut('/setting/faq/edit', params);
   }
 
+  orderFAQ(params) {
+    return this.apiPut('/setting/faq/order', params);
+  }
+
   delFAQ(params) {
     return this.apiDel('/setting/faq/delete', params);
   }
@@ -229,6 +241,10 @@ export default class SystemStore extends BaseStore {
     return this.apiGet('/setting/message/list', params);
   }
 
+  getMessage(params) {
+    return this.apiGet('/setting/message/detail', params);
+  }
+
   addMessage(params) {
     return this.apiPost('/setting/message/add', params);
   }
@@ -241,26 +257,6 @@ export default class SystemStore extends BaseStore {
     return this.apiDel('/setting/message/delete', params);
   }
 
-  listArticle(params) {
-    return this.apiGet('/setting/article/list', params);
-  }
-
-  addArticle(params) {
-    return this.apiPost('/setting/article/add', params);
-  }
-
-  editArticle(params) {
-    return this.apiPut('/setting/article/edit', params);
-  }
-
-  delArticle(params) {
-    return this.apiDel('/setting/article/delete', params);
-  }
-
-  getArticle(params) {
-    return this.apiGet('/setting/article/detail', params);
-  }
-
   listAd(params) {
     return this.apiGet('/setting/ad/list', params);
   }

+ 4 - 0
front/project/admin/stores/user.js

@@ -37,6 +37,10 @@ export default class UserStore extends BaseStore {
     return this.apiPost('/user/frozen', params);
   }
 
+  noFrozen(params) {
+    return this.apiPost('/user/nofrozen', params);
+  }
+
   real(params) {
     return this.apiPost('/user/real', params);
   }

+ 50 - 18
front/project/h5/components/Block/index.js

@@ -2,13 +2,20 @@ import React, { Component } from 'react';
 import './index.less';
 import { Icon } from 'antd-mobile';
 import Assets from '@src/components/Assets';
-import { getMap } from '@src/services/Tools';
-import { CrowdList } from '../../../Constant';
+import { getMap, formatDate } from '@src/services/Tools';
+import { CrowdList, ServiceParamMap } from '../../../Constant';
 import Tag from '../Tag';
 import Money from '../Money';
 import Button from '../Button';
 
 const CrowdMap = getMap(CrowdList, 'value', 'label');
+const ServiceParamRelation = getMap(Object.keys(ServiceParamMap).map(key => {
+  const list = (ServiceParamMap[key] || []).map((row, index) => {
+    row.index = index;
+    return row;
+  });
+  return { map: getMap(list, 'value'), key };
+}), 'key', 'map');
 
 export class Block extends Component {
   render() {
@@ -53,6 +60,7 @@ export class LinkBlock extends Component {
 export class CourseBlock extends Component {
   render() {
     const { data } = this.props;
+    const comment = data.comments ? data.comments[0] : {};
     return (
       <Block className="course-block" onClick={() => {
         linkTo(`/product/course/detail/${data.id}`);
@@ -64,9 +72,7 @@ export class CourseBlock extends Component {
             <div className="teacher">
               授课老师<span>{data.teacher}</span>
             </div>
-            <div className="desc">
-              {data.comment}
-            </div>
+            <div className="desc">{comment.content}</div>
             <div className="division" />
             <div className="data">
               {CrowdMap[data.crowd] && <Tag size="small">适合{CrowdMap[data.crowd]}</Tag>}
@@ -84,21 +90,21 @@ export class CourseBlock extends Component {
 export class CoursePackageBlock extends Component {
   render() {
     const { theme, data } = this.props;
+    const comment = data.comments ? data.comments[0] : {};
     return (
       <TagBlock className="course-co-block" theme={theme} tag={data.tag} onClick={() => {
         linkTo(`/product/course/package/${data.id}`);
       }}>
         <div className="title">{data.title}</div>
         <div className="info">
-          <div className="teacher">
-            授课老师{(data.courses || []).map(row => {
+          <div className="teacher">授课老师{(data.courses || []).map(row => {
             return <span>{row.teacher}</span>;
           })}
           </div>
           {CrowdMap[data.crowd] && <Tag size="small">适合{CrowdMap[data.crowd]}</Tag>}
         </div>
         <div className="desc">
-          {data.comment}
+          {comment.content}
         </div>
       </TagBlock>
     );
@@ -108,6 +114,7 @@ export class CoursePackageBlock extends Component {
 export class DataBlock extends Component {
   render() {
     const { data } = this.props;
+    const comment = data.comments ? data.comments[0] : {};
     return (
       <Block className="data-block" onClick={() => {
         linkTo(`/product/data/detail/${data.id}`);
@@ -115,7 +122,7 @@ export class DataBlock extends Component {
         <Assets name="d_b" src={data.cover} />
         <div className="info">
           <div className="title">{data.title}</div>
-          <div className="desc">{data.comment}</div>
+          <div className="desc">{comment.content}</div>
           <div className="division" />
           <div className="data">
             <div className="people">{data.saleNumber}人已购</div>
@@ -129,26 +136,51 @@ export class DataBlock extends Component {
 
 export class BuyBlock extends Component {
   render() {
-    const { theme } = this.props;
+    const { theme, data, onBuy, onOpen, onRead } = this.props;
+    const now = new Date().getTime();
+    let { title, price } = data;
+    if (data.productType === 'course') {
+      ({ title, price } = (data.course || {}));
+    }
+    if (data.productType === 'data') {
+      ({ title, price } = (data.data || {}));
+    }
+    if (data.productType === 'service') {
+      const p = ServiceParamRelation[data.service][data.param];
+      const index = p ? p.index : 0;
+      ({ title, price } = (data.serviceInfo || {}).package[index]);
+    }
+    const expire = data.useEndTime && new Date(data.useEndTime).getTime() < now.getTime();
     return (
       <TopBlock className="buy-block" theme={theme}>
         <div className="block-left">
           <div className="title">
-            <Tag theme="border" radius size="small">
+            {expire && < Tag theme="border" radius size="small">
               已到期
-            </Tag>
-            VIP会员
+            </Tag>}
+            {!expire && data.productType !== 'data' && data.isUsed && < Tag theme="border" radius size="small">
+              已开通
+            </Tag>}
+            {!expire && data.productType !== 'data' && !data.isUsed && < Tag theme="border" radius size="small">
+              未开通
+            </Tag>}
+            {title}
           </div>
-          <div className="date">有效期:2019-11-20</div>
-          <div className="desc">请访问千行 GMAT 官网开通使用</div>
+          {((data.useEndTime || data.endTime) && data.productType === 'service') && <div className="date">有效期:{formatDate(data.useEndTime || data.endTime, 'YYYY-MM-DD')}</div>}
+          {data.productType === 'data' && <div className="date">最新更新:{formatDate(data.data.latestTime, 'YYYY-MM-DD HH:mm:ss')}</div>}
+          {data.productType === 'course' && !data.isUsed && <div className="date">有效期:{formatDate(data.endTime, 'YYYY-MM-DD')}</div>}
+          {data.productType === 'course' && data.isUsed && <div className="date">课程学习时间:{formatDate(data.useStartTime, 'YYYY-MM-DD')}-{formatDate(data.useEndTime, 'YYYY-MM-DD')}</div>}
+          {data.service !== 'textbook' && !data.isUsed && <div className="desc">请访问千行 GMAT 官网开通使用</div>}
         </div>
         <div className="block-right">
           <div className="btn">
-            <Button radius>开通</Button>
+            {!data.isUsed && <Button radius onClick={() => onOpen && onOpen(data)}>开通</Button>}
+            {expire && data.service === 'vip' && <Button radius onClick={() => onBuy && onBuy(data)}>立即购买</Button>}
+            {data.productType === 'data' && data.data.resource && <Button radius onClick={() => onRead && onRead(data)}>阅读</Button>}
           </div>
-          <div className="tip">¥888/ 3个月</div>
+          {expire && data.service === 'vip' && <div className="tip">¥{price}/{title}</div>}
         </div>
-      </TopBlock>
+      </TopBlock >
     );
   }
 }

+ 2 - 11
front/project/h5/routes/page/bind/page.js

@@ -6,7 +6,6 @@ import { MobileArea } from '../../../../Constant';
 import Input, { SelectInput, VerificationInput } from '../../../components/Input';
 import Button from '../../../components/Button';
 import { User } from '../../../stores/user';
-import { My } from '../../../stores/my';
 import { Common } from '../../../stores/common';
 
 export default class extends Page {
@@ -76,16 +75,8 @@ export default class extends Page {
     if (mobileError || validError) return;
     if (!area || !mobile || !mobileVerifyCode) return;
     if (needEmail && !email) return;
-    User.bind(area, mobile, mobileVerifyCode).then(() => {
-      let handler = null;
-      if (needEmail) {
-        handler = My.bindEmail(email);
-      } else {
-        handler = Promise.resolve();
-      }
-      handler.then(() => {
-        linkTo('/');
-      });
+    User.bind(area, mobile, mobileVerifyCode, email).then(() => {
+      linkTo('/');
     })
       .catch(err => {
         if (err.message.indexOf('验证码') >= 0) {

+ 97 - 6
front/project/h5/routes/product/bought/page.js

@@ -1,18 +1,109 @@
 import React from 'react';
 import './index.less';
+import { ListView } from 'antd-mobile';
 import Page from '@src/containers/Page';
+import ListData from '@src/services/ListData';
 import { BuyBlock } from '../../../components/Block';
+import { Order } from '../../../stores/order';
 
 export default class extends Page {
-  init() {}
+  initState() {
+    return {
+      listMap: {},
+    };
+  }
+
+  init() {
+  }
+
+  initData() {
+    return this.initListKeys(['data']).then(() => {
+      return this.getList('data', 1);
+    });
+  }
+
+  initListKeys(keys) {
+    const { listMap = {} } = this.state;
+    keys.forEach(key => {
+      listMap[key] = new ListData();
+    });
+    this.setState({ listMap });
+    return Promise.resolve();
+  }
+
+  getList(key, page) {
+    Order.listRecord(Object.assign({ page }, this.state.search)).then(result => {
+      const { listMap = {} } = this.state;
+      if (page === 1) {
+        // todo 是否重新读取第一页为刷新所有数据
+        listMap[key] = new ListData();
+      }
+      listMap[key].get(page, result, this.state.search.size);
+      this.setState({ listMap });
+    });
+  }
 
   renderView() {
     return (
-      <div>
-        <BuyBlock theme="not" />
-        <BuyBlock />
-        <BuyBlock theme="end" />
-      </div>
+      this.renderList()
     );
   }
+
+  renderRow(rowData) {
+    let theme = 'default';
+    if (!rowData.isUsed) {
+      theme = 'not';
+    } else if (rowData.useEndTime && new Date(rowData.useEndTime).getTime() < new Date().getTime()) {
+      theme = 'end';
+    }
+    return <BuyBlock data={rowData} theme={theme} onOpen={() => {
+      linkTo(`/open/${rowData.id}`);
+    }} onBuy={() => {
+      linkTo(`/product/service/${rowData.service}`);
+    }} onRead={() => {
+      openLink(rowData.data.resource);
+    }} />;
+  }
+
+  renderList() {
+    const { listMap } = this.state;
+    const { data = {} } = listMap;
+    const { dataSource = {}, bottom, loading, finish, maxSectionId = 1, total } = data;
+    if (total === 0) return this.renderEmpty();
+    return (
+      <ListView
+        className="list"
+        ref={el => {
+          this.lv = el;
+        }}
+        dataSource={dataSource}
+        renderFooter={() => (
+          <div style={{ padding: 30, textAlign: 'center' }}>{loading ? '加载中...' : bottom ? '没有更多了' : ''}</div>
+        )}
+        renderRow={(rowData, sectionID, rowID) => this.renderRow(rowData, sectionID, rowID)}
+        style={{
+          height: this.state.height,
+          overflow: 'auto',
+        }}
+        pageSize={this.state.search.size}
+        scrollRenderAheadDistance={500}
+        onEndReached={() => {
+          if (loading) return;
+          if (bottom) {
+            if (!finish) {
+              data.finish = true;
+              // this.setState({ time: new Date() })
+            }
+            return;
+          }
+          this.getList('data', maxSectionId + 1);
+        }}
+        onEndReachedThreshold={10}
+      />
+    );
+  }
+
+  renderEmpty() {
+    return <div />;
+  }
 }

+ 20 - 0
front/project/h5/stores/main.js

@@ -117,6 +117,26 @@ export default class MainStore extends BaseStore {
   getService(service) {
     return this.apiGet('/base/service', { service });
   }
+
+  getContract(key) {
+    return this.apiGet('/base/contract', { key });
+  }
+
+  listFaq(page, size, channel, position) {
+    return this.apiGet('/base/faq/list', { page, size, channel, position });
+  }
+
+  listComment(page, size, channel, position) {
+    return this.apiGet('/base/comment/list', { page, size, channel, position });
+  }
+
+  readyInfo() {
+    return this.apiGet('/base/ready_info');
+  }
+
+  listRead(page, size, plate) {
+    return this.apiGet('/base/read/list', { page, size, plate });
+  }
 }
 
 export const Main = new MainStore({ key: 'main' });

+ 6 - 4
front/project/h5/stores/user.js

@@ -40,12 +40,13 @@ export default class UserStore extends BaseStore {
    * @param {*} mobile 手机号
    * @param {*} mobileVerifyCode 手机验证码
    * @param {*} inviteCode 邀请人手机/邀请码
+   * @param {*} email 绑定邮箱
    */
-  login(area, mobile, mobileVerifyCode, inviteCode) {
+  login(area, mobile, mobileVerifyCode, inviteCode, email) {
     if (!inviteCode) {
       ({ inviteCode } = this.state);
     }
-    return this.apiPost('/auth/login', { area, mobile, mobileVerifyCode, inviteCode }).then(result => {
+    return this.apiPost('/auth/login', { area, mobile, mobileVerifyCode, inviteCode, email }).then(result => {
       this.infoHandle(result);
       return result;
     });
@@ -71,12 +72,13 @@ export default class UserStore extends BaseStore {
    * @param {*} mobile 手机号
    * @param {*} mobileVerifyCode 手机验证码
    * @param {*} inviteCode 邀请人手机/邀请码
+   * @param {*} email 绑定邮箱
    */
-  bind(area, mobile, mobileVerifyCode, inviteCode) {
+  bind(area, mobile, mobileVerifyCode, inviteCode, email) {
     if (!inviteCode) {
       ({ inviteCode } = this.state);
     }
-    return this.apiPost('/auth/bind', { area, mobile, mobileVerifyCode, inviteCode });
+    return this.apiPost('/auth/bind', { area, mobile, mobileVerifyCode, inviteCode, email });
   }
 
   /**

+ 6 - 23
front/project/www/components/Login/index.js

@@ -6,7 +6,6 @@ import { asyncSMessage } from '@src/services/AsyncTools';
 import { Icon as GIcon } from '../Icon';
 import { Button as GButton } from '../Button';
 import { User } from '../../stores/user';
-import { My } from '../../stores/my';
 import { Common } from '../../stores/common';
 import { MobileArea, WechatUserAppId } from '../../../Constant';
 
@@ -47,21 +46,13 @@ export default class Login extends Component {
     if (mobileError || validError) return;
     if (!area || !mobile || !mobileVerifyCode) return;
     if (needEmail && !email) return;
-    User.login(area, mobile, mobileVerifyCode)
+    User.login(area, mobile, mobileVerifyCode, email)
       .then((result) => {
-        let handler = null;
-        if (needEmail) {
-          handler = My.bindEmail(email);
+        if (result.bindWechat) {
+          this.close();
         } else {
-          handler = Promise.resolve();
+          this.setState({ type: BIND_WX });
         }
-        handler.then(() => {
-          if (result.bindWechat) {
-            this.close();
-          } else {
-            this.setState({ type: BIND_WX });
-          }
-        });
       })
       .catch(err => {
         if (err.message.indexOf('验证码') >= 0) {
@@ -78,17 +69,9 @@ export default class Login extends Component {
     if (mobileError || validError) return;
     if (!area || !mobile || !mobileVerifyCode) return;
     if (needEmail && !email) return;
-    User.bind(area, mobile, mobileVerifyCode)
+    User.bind(area, mobile, mobileVerifyCode, email)
       .then(() => {
-        let handler = null;
-        if (needEmail) {
-          handler = My.bindEmail(email);
-        } else {
-          handler = Promise.resolve();
-        }
-        handler.then(() => {
-          this.close();
-        });
+        this.close();
       })
       .catch(err => {
         if (err.message.indexOf('验证码') >= 0) {

+ 3 - 2
front/project/www/components/QAList/index.js

@@ -46,15 +46,16 @@ class QAItem extends Component {
 }
 
 function QAList(props) {
-  const { style, data = [] } = props;
+  const { style, data = [], tabs = [], active } = props;
   return (
     <Module style={style} className="qa-list">
       <Tabs
         type="division"
         theme="theme"
+        active={active}
         space={2.5}
         width={100}
-        tabs={[{ key: 'qx', name: '解析详情' }, { key: 'chinese', name: '中文语意' }]}
+        tabs={tabs}
       />
       {data.map(item => {
         return <QAItem data={item} />;

+ 11 - 0
front/project/www/routes/examination/main/page.js

@@ -7,6 +7,7 @@ import Panel, { WaitPanel, BuyPanel, SmallPanel, SmallWaitPanel, SmallBuyPanel }
 import Tabs from '../../../components/Tabs';
 import Module from '../../../components/Module';
 import Division from '../../../components/Division';
+import QAList from '../../../components/QAList';
 import { Main } from '../../../stores/main';
 // import { My } from '../../../stores/my';
 import { Question } from '../../../stores/question';
@@ -115,6 +116,10 @@ export default class extends Page {
       });
       this.setState({ textbookProgress: result });
     });
+
+    Main.listFaq({ page: 1, size: 100, channel: 'textbook' }).then(result => {
+      this.setState({ faqs: result.list });
+    });
   }
 
   refreshExamination(tab) {
@@ -148,6 +153,10 @@ export default class extends Page {
       });
       this.setState({ examinationProgress: result });
     });
+
+    Main.listFaq({ page: 1, size: 100, channel: `examination-${subject.extend}` }).then(result => {
+      this.setState({ faqs: result.list });
+    });
   }
 
   onChangeTab(level, tab) {
@@ -221,6 +230,8 @@ export default class extends Page {
           </Module>
           {tab1 !== TEXTBOOK && this.renderExamination()}
           {tab1 === TEXTBOOK && this.renderTextbook()}
+
+          {this.state.faqs && <QAList data={this.state.faqs} active={'faq'} tabs={[{ key: 'faq', name: 'FAQs' }]} />}
         </div>
       </div>
     );

+ 23 - 1
front/project/www/routes/exercise/main/page.js

@@ -19,6 +19,7 @@ import { Card1 } from '../../../components/Card';
 import ListTable from '../../../components/ListTable';
 import ProgressText from '../../../components/ProgressText';
 import IconButton from '../../../components/IconButton';
+import QAList from '../../../components/QAList';
 import { Main } from '../../../stores/main';
 import { My } from '../../../stores/my';
 import { Sentence } from '../../../stores/sentence';
@@ -34,6 +35,7 @@ const PREVIEW_COURSE = 'PREVIEW_COURSE';
 const PREVIEW_LIST = 'PREVIEW_LIST';
 
 const CourseModuleMap = getMap(CourseModule, 'value', 'label');
+const CourseSorted = getMap([{ value: 'sc', sort: 1 }, { value: 'rc', sort: 2 }, { value: 'cr', sort: 3 }], 'value', 'sort');
 
 const exerciseColumns = [{
   title: '练习册',
@@ -433,13 +435,25 @@ export default class extends Page {
     const [courseStruct] = courseStructs.filter(row => row.key === struct);
     Course.progress(tab.value, courseStruct ? courseStruct.id : null).then(result => {
       const courseMap = {};
+      // 排序:sc,rc,cr
+      result = result.map(row => {
+        row.sort = CourseSorted[row.course.extend] || 10;
+        return row;
+      });
+      result.sort((a, b) => {
+        return a.sort < b.sort ? -1 : (a.sort > b.sort ? 1 : 0);
+      });
       const now = new Date().getTime();
       courseMap.open = result.filter(row => !row.isUsed);
       courseMap.end = result.filter(row => row.isUsed && (!row.isStop || (row.isStop && row.restoreTime)) && new Date(row.useEndTime).getTime() < now);
-      // todo 排序:sc,rc,cr
+
       courseMap.process = result.filter(row => row.isUsed && (row.isStop || row.restoreTime) && new Date(row.useEndTime).getTime() >= now);
       this.setState({ courseMap });
     });
+
+    Main.listFaq({ page: 1, size: 100, channel: 'preview' }).then(result => {
+      this.setState({ faqs: result.list });
+    });
   }
 
   refreshListPreview() {
@@ -507,6 +521,9 @@ export default class extends Page {
       });
       this.setState({ exerciseProgress: result });
     });
+    Main.listFaq({ page: 1, size: 100, channel: 'exercise' }).then(result => {
+      this.setState({ faqs: result.list });
+    });
   }
 
   onChangeTab(level, tab) {
@@ -674,6 +691,8 @@ export default class extends Page {
           {tab1 !== SENTENCE && tab1 !== PREVIEW && this.renderExercise()}
           {tab1 === SENTENCE && this.renderSentence()}
           {tab1 === PREVIEW && this.renderPreview()}
+
+          {this.state.faqs && <QAList data={this.state.faqs} active={'faq'} tabs={[{ key: 'faq', name: 'FAQs' }]} />}
         </div>
         {sentenceModel && this.renderInputCodeModel()}
       </div>
@@ -728,6 +747,9 @@ export default class extends Page {
               onPreview={() => {
                 this.onPreviewList(row.id);
               }}
+              previewAction={(type, item) => {
+                this.previewAction(type, item);
+              }}
             />;
           })}
         </Division>

+ 4 - 0
front/project/www/routes/paper/report/index.less

@@ -243,6 +243,10 @@
         background: #ECEDEE;
         height: 60px;
         border: 1px solid #fff;
+
+        &.point {
+          cursor: pointer;
+        }
       }
 
       td {

+ 157 - 60
front/project/www/routes/paper/report/page.js

@@ -1,11 +1,12 @@
 import React from 'react';
+import { Link } from 'react-router-dom';
 import './index.less';
 import LineChart from '@src/components/LineChart';
 import BarChart from '@src/components/BarChart';
 import PieChart from '@src/components/PieChart';
 import Assets from '@src/components/Assets';
 import Page from '@src/containers/Page';
-import { formatDate, formatPercent, formatSeconds, formatMinute, formatSecond, getMap } from '@src/services/Tools';
+import { formatDate, formatPercent, formatSeconds, formatMinute, formatSecond, formatMinuteSecond, getMap } from '@src/services/Tools';
 import { Icon, Tooltip } from 'antd';
 import { Question } from '../../../stores/question';
 import { Button } from '../../../components/Button';
@@ -18,6 +19,7 @@ import {
 } from '../../../../Constant';
 
 const QuestionDifficultMap = getMap(QuestionDifficult, 'value', 'label');
+const QuestionDifficultSort = getMap(QuestionDifficult, 'value', 'sort');
 
 function BarOption3(titles, source, data1, data2, color1, color2) {
   return {
@@ -502,7 +504,7 @@ function pieOption2(title, value1, value2, value3) {
 
 export default class extends Page {
   initState() {
-    return { tab: 'main', report: { paperModule: 'sentence' } };
+    return { tab: 'main', report: { paperModule: '' } };
   }
 
   initData() {
@@ -530,6 +532,31 @@ export default class extends Page {
       case 'question':
         // 题目回顾列表
         Question.questionReport(id).then(result => {
+          switch (result.paperModule) {
+            case 'sentence':
+              result = result.map((row) => {
+                row.struct = row.detail.subject && row.detail.predicate && row.detail.object ? 0 : 1;
+                row.logic = row.detail.options ? 0 : 1;
+
+                row.note = row.note ? 1 : 0;
+                row.collect = row.collect ? 1 : 0;
+                return row;
+              });
+              break;
+            case 'textbook':
+            case 'exercise':
+              result = result.map((row) => {
+                row.correct = row.isCorrect ? 0 : 1;
+                row.diff = QuestionDifficultSort[row.question.difficult];
+
+                row.note = row.note ? 1 : 0;
+                row.collect = row.collect ? 1 : 0;
+                return row;
+              });
+              this.refreshExercise(result);
+              break;
+            default:
+          }
           this.setState({ list: result });
         });
         break;
@@ -569,15 +596,48 @@ export default class extends Page {
     }
   }
 
+  questionSort(field) {
+    let { order } = this.state;
+    const { list = [] } = this.state;
+    if (order === field) {
+      order = 'no';
+      // direction = 'asc';
+    } else {
+      order = field;
+      // direction = 'desc';
+    }
+    list.sort((a, b) => {
+      const aValue = a[order];
+      const bValue = a[order];
+      if (aValue === bValue) {
+        return a.no < b.no ? -1 : a.no > b.no ? 1 : 0;
+      }
+      return aValue > bValue ? -1 : 1;
+    });
+    // if (direction === 'desc') {
+    //   list.reverse();
+    // }
+    this.setState({ order, list });
+  }
+
   renderView() {
-    const { report = {} } = this.state;
-    console.log(report);
+    const { report = {}, search = {} } = this.state;
+    const { info } = search;
     switch (report.paperModule) {
       case 'sentence':
+        if (info === 'question') {
+          return this.renderSentenceQuestion();
+        }
         return this.renderSentence();
       case 'textbook':
+        if (info === 'question') {
+          return this.renderExerciseQuestion();
+        }
         return this.renderTextbook();
       case 'exercise':
+        if (info === 'question') {
+          return this.renderExerciseQuestion();
+        }
         return this.renderExercise();
       case 'examination':
         return this.renderExamination();
@@ -690,11 +750,14 @@ export default class extends Page {
   }
 
   renderSentenceQuestion() {
+    const { report, list, order } = this.state;
     return <div className='sentence question'>
       <div className='header'>
         <div className='content'>
           <div className='title'>题目回顾</div>
-          <Button className='back' radius>返回长难句报告</Button>
+          <Button className='back' radius onClick={() => {
+            linkTo(`/paper/report/${report.id}`);
+          }}>返回长难句报告</Button>
         </div>
       </div>
       <div className='body'>
@@ -704,63 +767,39 @@ export default class extends Page {
             <thead>
               <tr>
                 <th>序号</th>
-                <th width='360'>题目</th>
-                <th>句子结构<GIcon name='arrow-up' /></th>
-                <th>逻辑关系<GIcon name='arrow-down' /></th>
-                <th>用时<GIcon name='arrow-up' /></th>
-                <th>收藏<GIcon name='arrow-up' /></th>
-                <th>笔记<GIcon name='arrow-up' /></th>
+                <th width='420'>题目</th>
+                <th className="point" onClick={() => {
+                  this.questionSort('struct');
+                }}>句子结构<GIcon name={order === 'struct' ? 'arrow-down' : 'arrow-up'} active={order === 'struct'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('logic');
+                }}>逻辑关系<GIcon name={order === 'logic' ? 'arrow-down' : 'arrow-up'} active={order === 'logic'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('userTime');
+                }}>用时<GIcon name={order === 'userTime' ? 'arrow-down' : 'arrow-up'} active={order === 'userTime'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('collect');
+                }}>收藏<GIcon name={order === 'collect' ? 'arrow-down' : 'arrow-up'} active={order === 'collect'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('note');
+                }}>笔记<GIcon name={order === 'note' ? 'arrow-down' : 'arrow-up'} active={order === 'note'} /></th>
               </tr>
             </thead>
             <tbody>
-              <tr>
-                <td>1</td>
-                <td>
-                  <div className='n'> OG18 #678</div>
-                  <div className='desc'>「图形」None of the attempts to specify the causes of crime explains why most of the people exposed to</div>
-                </td>
-                <td><GIcon name='error' noHover /></td>
-                <td><GIcon name='right' noHover /></td>
-                <td>1.3</td>
-                <td><GIcon name='star' noHover /></td>
-                <td><GIcon name='note' noHover /></td>
-              </tr>
-              <tr>
-                <td>1</td>
-                <td>
-                  <div className='n'> OG18 #678</div>
-                  <div className='desc'>「图形」None of the attempts to specify the causes of crime explains why most of the people exposed to</div>
-                </td>
-                <td><GIcon name='error' noHover /></td>
-                <td><GIcon name='right' noHover /></td>
-                <td>1.3</td>
-                <td><GIcon name='star' active /></td>
-                <td><GIcon name='note' active /></td>
-              </tr>
-              <tr>
-                <td>1</td>
-                <td>
-                  <div className='n'> OG18 #678</div>
-                  <div className='desc'>「图形」None of the attempts to specify the causes of crime explains why most of the people exposed to</div>
-                </td>
-                <td><GIcon name='error' noHover /></td>
-                <td><GIcon name='right' noHover /></td>
-                <td>1.3</td>
-                <td><GIcon name='star' noHover /></td>
-                <td><GIcon name='note' noHover /></td>
-              </tr>
-              <tr>
-                <td>1</td>
-                <td>
-                  <div className='n'> OG18 #678</div>
-                  <div className='desc'>「图形」None of the attempts to specify the causes of crime explains why most of the people exposed to</div>
-                </td>
-                <td><GIcon name='error' noHover /></td>
-                <td><GIcon name='right' noHover /></td>
-                <td>1.3</td>
-                <td><GIcon name='star' active /></td>
-                <td><GIcon name='note' active /></td>
-              </tr>
+              {(list || []).map(row => {
+                return <tr>
+                  <td>{row.no}</td>
+                  <td>
+                    <div className='n'><Link to={`/paper/question/${row.id}`}>{row.questionNo.title}</Link></div>
+                    <div className='desc'>{row.question.description}</div>
+                  </td>
+                  <td><GIcon name={row.detail.subject && row.detail.predicate && row.detail.object ? 'right' : 'error'} noHover /></td>
+                  <td><GIcon name={row.detail.options ? 'right' : 'error'} noHover /></td>
+                  <td>{formatMinuteSecond(row.userTime)}</td>
+                  <td><GIcon name='star' active={row.collect} noHover /></td>
+                  <td><GIcon name='note' active={row.note} noHover /></td>
+                </tr>;
+              })}
             </tbody>
           </table>
         </div>
@@ -830,7 +869,65 @@ export default class extends Page {
   }
 
   renderExerciseQuestion() {
-    return <div />;
+    const { report, list, order } = this.state;
+    return <div className='sentence question'>
+      <div className='header'>
+        <div className='content'>
+          <div className='title'>题目回顾</div>
+          <Button className='back' radius onClick={() => {
+            linkTo(`/paper/report/${report.id}`);
+          }}>返回练习报告</Button>
+        </div>
+      </div>
+      <div className='body'>
+        <div className='content'>
+          <div className='tip'><Assets name='notice' />点击题目查看详情</div>
+          <table>
+            <thead>
+              <tr>
+                <th>序号</th>
+                <th width='340'>题目</th>
+                <th className="point" onClick={() => {
+                  this.questionSort('correct');
+                }}>正误<GIcon name={order === 'correct' ? 'arrow-down' : 'arrow-up'} active={order === 'correct'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('diff');
+                }}>难度<GIcon name={order === 'diff' ? 'arrow-down' : 'arrow-up'} active={order === 'diff'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('userTime');
+                }}>用时<GIcon name={order === 'userTime' ? 'arrow-down' : 'arrow-up'} active={order === 'userTime'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('place');
+                }}>主要考点<GIcon name={order === 'place' ? 'arrow-down' : 'arrow-up'} active={order === 'place'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('collect');
+                }}>收藏<GIcon name={order === 'collect' ? 'arrow-down' : 'arrow-up'} active={order === 'collect'} /></th>
+                <th className="point" onClick={() => {
+                  this.questionSort('note');
+                }}>笔记<GIcon name={order === 'note' ? 'arrow-down' : 'arrow-up'} active={order === 'note'} /></th>
+              </tr>
+            </thead>
+            <tbody>
+              {(list || []).map(row => {
+                return <tr>
+                  <td>{row.no}</td>
+                  <td>
+                    <div className='n'><Link to={`/paper/question/${row.id}`}>{row.questionNo.title}</Link></div>
+                    <div className='desc'>{row.question.description}</div>
+                  </td>
+                  <td><GIcon name={row.isCorrect ? 'right' : 'error'} noHover /></td>
+                  <td>{row.question.difficult}</td>
+                  <td>{formatMinuteSecond(row.userTime)}</td>
+                  <td>{row.question.place}</td>
+                  <td><GIcon name='star' active={row.collect} noHover /></td>
+                  <td><GIcon name='note' active={row.note} noHover /></td>
+                </tr>;
+              })}
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>;
   }
 
   renderExerciseDetail() {

+ 9 - 7
front/project/www/routes/sentence/read/page.js

@@ -7,7 +7,7 @@ import Progress from '../../../components/Progress';
 import Assets from '../../../../../src/components/Assets';
 import { Sentence } from '../../../stores/sentence';
 import { Main } from '../../../stores/main';
-import { formatMoney } from '../../../../../src/services/Tools';
+import { formatMoney, formatDate } from '../../../../../src/services/Tools';
 
 export default class extends Page {
   constructor(props) {
@@ -209,7 +209,7 @@ export default class extends Page {
   }
 
   renderBody() {
-    const { showMenu, article, index, chapterMap = {}, info = {} } = this.state;
+    const { showMenu, article, index, chapterMap = {}, info = {}, sentence = {} } = this.state;
     return article ? (
       <div className="layout-body">
         <div className="crumb">千行长难句解析 >> {(chapterMap[article.chapter] || {}).title}</div>
@@ -223,13 +223,15 @@ export default class extends Page {
       <div className="free-over">
         <div className="free-over-title">试读已结束,购买后可继续阅读。</div>
         <div className="free-over-btn" onClick={() => {
-          window.location.href = info.link;
+          openLink(info.link);
         }}>{formatMoney(info.price)} | 立即购买</div>
         <div className="free-over-desc">
-          <div className="free-over-desc-title">{info.title}</div>
-          <div className="free-over-desc-content">
-            {info.description}
-          </div>
+          {(sentence.comments || []).map(row => {
+            return [
+              <div className="free-over-desc-title">{row.nickname} {formatDate(row.createTime, 'YYYY-MM-DD')}</div>,
+              <div className="free-over-desc-content">{row.content}</div>,
+            ];
+          })}
         </div>
       </div>
     </div>

+ 20 - 4
front/project/www/stores/main.js

@@ -124,16 +124,32 @@ export default class MainStore extends BaseStore {
     }, 3600);
   }
 
-  listFaq(page, size, channel, position) {
+  /**
+   * 获取服务信息
+   * @param {*} service
+   */
+  getService(service) {
+    return this.apiGet('/base/service', { service });
+  }
+
+  getContract(key) {
+    return this.apiGet('/base/contract', { key });
+  }
+
+  listFaq({ page, size, channel, position }) {
     return this.apiGet('/base/faq/list', { page, size, channel, position });
   }
 
-  listComment(page, size, channel, position) {
+  listComment({ page, size, channel, position }) {
     return this.apiGet('/base/comment/list', { page, size, channel, position });
   }
 
-  listArticle(page, size, channel, position) {
-    return this.apiGet('/base/article/list', { page, size, channel, position });
+  readyInfo() {
+    return this.apiGet('/base/ready_info');
+  }
+
+  listRead(page, size, plate) {
+    return this.apiGet('/base/read/list', { page, size, plate });
   }
 }
 

+ 42 - 10
front/project/www/stores/my.js

@@ -166,8 +166,8 @@ export default class MyStore extends BaseStore {
    * @param {*} order
    * @param {*} direction
    */
-  listQuestionCollect({ module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
-    return this.apiGet('/my/collect/question/list', { module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
+  listQuestionCollect({ keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/collect/question/list', { keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -176,8 +176,8 @@ export default class MyStore extends BaseStore {
    * @param {*} page
    * @param {*} size
    */
-  listError({ module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
-    return this.apiGet('/my/error/list', { module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
+  listError({ keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/error/list', { keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -251,8 +251,8 @@ export default class MyStore extends BaseStore {
    * @param {*} order
    * @param {*} direction
    */
-  questionNoteList({ module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
-    return this.apiGet('/my/note/question/list', { module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
+  questionNoteList({ keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/note/question/list', { keyword, module, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -266,8 +266,8 @@ export default class MyStore extends BaseStore {
    * @param {*} order
    * @param {*} direction
    */
-  reportList({ module, origin, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
-    return this.apiGet('/my/report/list', { module, origin, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
+  reportList({ keyword, module, origin, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/report/list', { keyword, module, origin, questionTypes, structIds, latest, year, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -290,8 +290,8 @@ export default class MyStore extends BaseStore {
     return this.apiDel('/my/ask/question/delete', { id });
   }
 
-  listQuestionAsk({ module, questionTypes, structIds, latest, year, askStatus, page, size, startTime, endTime, order, direction }) {
-    return this.apiGet('/my/ask/question/list', { module, questionTypes, structIds, latest, year, askStatus, page, size, startTime, endTime, order, direction });
+  listQuestionAsk({ keyword, module, questionTypes, structIds, latest, year, askStatus, page, size, startTime, endTime, order, direction }) {
+    return this.apiGet('/my/ask/question/list', { keyword, module, questionTypes, structIds, latest, year, askStatus, page, size, startTime, endTime, order, direction });
   }
 
   /**
@@ -359,6 +359,38 @@ export default class MyStore extends BaseStore {
   addComment(channel, position, content) {
     return this.apiPost('/my/comment', { channel, position, content });
   }
+
+  /**
+   * 购买的课程列表
+   * @param {*} param0
+   */
+  listCourse({ page, size, courseModule, isUsed, isEnd, order, direction }) {
+    return this.apiGet('/my/course/list', { page, size, courseModule, isUsed, isEnd, order, direction });
+  }
+
+  /**
+   * 申请停课
+   * @param {*} recordId
+   */
+  suspendCourse(recordId) {
+    return this.apiPost('/my/course/suspend', { recordId });
+  }
+
+  /**
+   * 申请恢复课程
+   * @param {*} recordId
+   */
+  restoreCourse(recordId) {
+    return this.apiPost('/my/course/restore', { recordId });
+  }
+
+  /**
+   * 课程时间表
+   * @param {*} recordId
+   */
+  timeCourse(recordId) {
+    return this.apiGet('/my/course/time', { recordId });
+  }
 }
 
 export const My = new MyStore({ key: 'my' });

+ 6 - 4
front/project/www/stores/user.js

@@ -89,12 +89,13 @@ export default class UserStore extends BaseStore {
    * @param {*} mobile 手机号
    * @param {*} mobileVerifyCode 手机验证码
    * @param {*} inviteCode 邀请人手机/邀请码
+   * @param {*} email 绑定邮箱
    */
-  login(area, mobile, mobileVerifyCode, inviteCode) {
+  login(area, mobile, mobileVerifyCode, inviteCode, email) {
     if (!inviteCode) {
       ({ inviteCode } = this.state);
     }
-    return this.apiPost('/auth/login', { area, mobile, mobileVerifyCode, inviteCode }).then(result => {
+    return this.apiPost('/auth/login', { area, mobile, mobileVerifyCode, inviteCode, email }).then(result => {
       this.infoHandle(result);
       return result;
     });
@@ -131,9 +132,10 @@ export default class UserStore extends BaseStore {
    * @param {*} mobile 手机号
    * @param {*} mobileVerifyCode 手机验证码
    * @param {*} inviteCode 邀请人手机/邀请码
+   * @param {*} email 绑定邮箱
    */
-  bind(area, mobile, mobileVerifyCode, inviteCode) {
-    return this.apiPost('/auth/bind', { area, mobile, mobileVerifyCode, inviteCode });
+  bind(area, mobile, mobileVerifyCode, inviteCode, email) {
+    return this.apiPost('/auth/bind', { area, mobile, mobileVerifyCode, inviteCode, email });
   }
 
   /**

+ 1 - 1
front/src/components/DragList/index.js

@@ -55,7 +55,7 @@ export default class DragSortingTable extends React.Component {
             if (ref) this.sortableDecorator(ref);
           }}>
           {this.props.dataSource.map((item, index) => {
-            return <div className={`drag ${this.props.type}`}>{this.props.renderItem(item, index)}</div>;
+            return <div key={this.props.rowKey ? item[this.props.rowKey] : index} className={`drag ${this.props.type}`}>{this.props.renderItem(item, index)}</div>;
           })}
         </div></Spin>
     );

+ 2 - 2
front/src/components/FileUpload/index.js

@@ -88,8 +88,8 @@ class FileUpload extends Component {
           onChange={e => this.onChange(e)}
         />
         {this.state.type === 'image' ? (
-          this.props.src && !this.state.uploading ? (
-            <img src={this.props.src} style={{ width: '100%', height: '100%' }} />
+          this.props.value && !this.state.uploading ? (
+            <img src={this.props.value} style={{ width: '100%', height: '100%' }} />
           ) : (
             <Icon type={this.state.uploading ? 'loading' : 'upload'} />
           )

+ 3 - 0
front/src/layouts/FormLayout/index.js

@@ -124,6 +124,9 @@ class FormLayout extends Component {
           <FileUpload
             {...item}
             title={item.name}
+            onEnd={url => {
+              this.props.form.setFieldsValue({ [item.key]: url });
+            }}
             onError={err => {
               this.props.form.setFields({ [item.key]: { value: '', errors: [err] } });
             }}

+ 30 - 2
front/src/services/Tools.js

@@ -160,10 +160,26 @@ export function SortByProps(item1, item2, props) {
   return false;
 }
 
-export function getMap(list, key = 'value', value = null) {
+export function flattenTree(tree, fn, children = 'children') {
+  const l = tree.map(item => {
+    if (item[children] && item[children].length > 0) {
+      const list = flattenTree(item[children], fn, children);
+      return list.map((row) => fn(row, item));
+    }
+    return [item];
+  });
+  return [].concat(...l);
+}
+
+export function getMap(list, key = 'value', value = null, children = null) {
   const map = {};
   for (let i = 0; i < list.length; i += 1) {
-    map[list[i][key]] = value ? list[i][value] : list[i];
+    const item = list[i];
+    let v = value ? item[value] : item;
+    if (children && item[children] && item[children].length > 0) {
+      v = getMap(item[children], key, value, children);
+    }
+    map[item[key]] = v;
   }
   return map;
 }
@@ -511,3 +527,15 @@ export function resortListWithOrder(target, order) {
   });
   return list;
 }
+
+// 下划线转换驼峰
+export function toHump(name) {
+  return name.replace(/_(\w)/g, (all, letter) => {
+    return letter.toUpperCase();
+  });
+}
+
+// 驼峰转换下划线
+export function toLine(name) {
+  return name.replace(/([A-Z])/g, '_$1').toLowerCase();
+}

+ 37 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/MessageCategory.java

@@ -0,0 +1,37 @@
+package com.qxgmat.data.constants.enums;
+
+/**
+ * Created by gaojie on 2017/11/19.
+ */
+public enum MessageCategory {
+    REGISTER("register", "注册消息"),
+    LOGIN_ABNORMAL("login_abnormal", "登录异常"),
+    TEXTBOOK_UPDATE("textbook_update","机经更新"),
+    PREVIEW_NOTICE("preview_notice", "预习作业提醒"),
+    PAYED("payed", "支付成功提醒"),
+    DATA_UPDATE("data_update", "资料更新"),
+    ASK_QUESTION("ask_question", "题目提问回复"),
+    ASK_COURSE("ask_course", "课程提问回复"),
+    FAQ_CALLBACK("faq_callback", "咨询回复"),
+    FEEDBACK_CALLBACK("feedback_callback", "纠错回复"),
+
+    INVITED("invited", "邀请好友注册"),
+    EMAIL_CHANGE("email_change", "邮箱变更"),
+
+    CUSTOM("custom", "自定义消息")
+    ;
+
+    final static public String message = "消息种类";
+
+    public String key;
+    public String title;
+    private MessageCategory(String key, String title){
+        this.key = key;
+        this.title = title;
+    }
+
+    public static MessageCategory ValueOf(String name){
+        if (name == null) return null;
+        return MessageCategory.valueOf(name.toUpperCase());
+    }
+}

+ 25 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/MessageMethod.java

@@ -0,0 +1,25 @@
+package com.qxgmat.data.constants.enums;
+
+/**
+ * Created by gaojie on 2017/11/19.
+ */
+public enum MessageMethod {
+    INSIDE("inside", "站内信"),
+    EMAIL("email", "邮件"),
+    WECHAT("wechat","微信通知"),
+    ;
+
+    final static public String message = "消息方式";
+
+    public String key;
+    public String title;
+    private MessageMethod(String key, String title){
+        this.key = key;
+        this.title = title;
+    }
+
+    public static MessageMethod ValueOf(String name){
+        if (name == null) return null;
+        return MessageMethod.valueOf(name.toUpperCase());
+    }
+}

+ 40 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/MessageType.java

@@ -0,0 +1,40 @@
+package com.qxgmat.data.constants.enums;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by gaojie on 2017/11/19.
+ */
+public enum MessageType {
+    SYSTEM("system", "系统消息"),
+    FEED("feed", "动态消息"),
+    ;
+
+    final static public String message = "消息类型";
+
+    public String key;
+    public String title;
+    private MessageType(String key, String title){
+        this.key = key;
+        this.title = title;
+    }
+
+    public static MessageType ValueOf(String name){
+        if (name == null) return null;
+        return MessageType.valueOf(name.toUpperCase());
+    }
+
+    public static MessageType FromCategory(MessageCategory messageCategory){
+        switch (messageCategory){
+            case ASK_QUESTION:
+            case ASK_COURSE:
+            case FAQ_CALLBACK:
+            case FEEDBACK_CALLBACK:
+                return FEED;
+            default:
+                return SYSTEM;
+        }
+    }
+}

+ 2 - 2
server/data/src/main/java/com/qxgmat/data/constants/enums/SettingKey.java

@@ -9,7 +9,6 @@ import com.qxgmat.data.constants.enums.module.QuestionModule;
 public enum SettingKey {
     INDEX("index"), // 首页设置
     SENTENCE("sentence"), // 长难句目录
-    MESSAGE_TEMPLATE("message_template"), // 消息模版
     PLACE("place"), // 难点设置
     EXERCISE_TIME("exercise_time"), // 练习题目时间
     EXERCISE_PAPER_AUTO("exercise_paper_auto"), // 自动组卷设置
@@ -26,10 +25,11 @@ public enum SettingKey {
     SERVICE_TEXTBOOK("service_textbook"), // 数学机经服务信息
     SERVICE_VIP("service_vip"), // vip服务信息
     COURSE_INDEX("course_index"), // 课程首页设置
-    PROMOTE("course_promote"), // 促销
+    PROMOTE("promote"), // 促销
     EXPERIENCE_INFO("experience_info"), // 心经信息
     SENTENCE_INFO("sentence_info"), // 长难句信息
     WECHAT_INFO("wechat_info"), // 微信公众号信息
+    READY_READ("ready_read"), // 推荐阅读设置
 
     TIPS("tips"); // 页面提示信息
 

+ 1 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/trade/RecordSource.java

@@ -9,6 +9,7 @@ public enum RecordSource {
     INVITE("invite"),
     PREPARE("prepare"),
     BACKEND("backend"),
+    // 赠送用gift开头,方便过滤
     GIFT_COURSE("gift_course"),
     GIFT_ACTIVITY("gift_activity"),
 

+ 0 - 21
server/data/src/main/java/com/qxgmat/data/constants/enums/user/MessageType.java

@@ -1,21 +0,0 @@
-package com.qxgmat.data.constants.enums.user;
-
-/**
- * Created by gaojie on 2017/11/20.
- */
-public enum MessageType {
-    PAY("pay", "购买"),
-    LIBRARY("library", "换库"),
-    COURSE("course", "课程"),
-
-    ;
-    final static public String message = "消息类型";
-
-
-    public String type;
-    public String title;
-    private MessageType(String type, String title){
-        this.type = type;
-        this.title = title;
-    }
-}

+ 0 - 7
server/data/src/main/java/com/qxgmat/data/dao/ArticleMapper.java

@@ -1,7 +0,0 @@
-package com.qxgmat.data.dao;
-
-import com.nuliji.tools.mybatis.Mapper;
-import com.qxgmat.data.dao.entity.Article;
-
-public interface ArticleMapper extends Mapper<Article> {
-}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/ContractMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.Contract;
+
+public interface ContractMapper extends Mapper<Contract> {
+}

+ 0 - 7
server/data/src/main/java/com/qxgmat/data/dao/MessageMapper.java

@@ -1,7 +0,0 @@
-package com.qxgmat.data.dao;
-
-import com.nuliji.tools.mybatis.Mapper;
-import com.qxgmat.data.dao.entity.Message;
-
-public interface MessageMapper extends Mapper<Message> {
-}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/MessageTemplateMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.MessageTemplate;
+
+public interface MessageTemplateMapper extends Mapper<MessageTemplate> {
+}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/ReadyArticleCategoryMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.ReadyArticleCategory;
+
+public interface ReadyArticleCategoryMapper extends Mapper<ReadyArticleCategory> {
+}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/ReadyArticleMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.ReadyArticle;
+
+public interface ReadyArticleMapper extends Mapper<ReadyArticle> {
+}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/ReadyDataMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.ReadyData;
+
+public interface ReadyDataMapper extends Mapper<ReadyData> {
+}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/ReadyReadMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.ReadyRead;
+
+public interface ReadyReadMapper extends Mapper<ReadyRead> {
+}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/ReadyRoomAreaMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.ReadyRoomArea;
+
+public interface ReadyRoomAreaMapper extends Mapper<ReadyRoomArea> {
+}

+ 7 - 0
server/data/src/main/java/com/qxgmat/data/dao/ReadyRoomMapper.java

@@ -0,0 +1,7 @@
+package com.qxgmat.data.dao;
+
+import com.nuliji.tools.mybatis.Mapper;
+import com.qxgmat.data.dao.entity.ReadyRoom;
+
+public interface ReadyRoomMapper extends Mapper<ReadyRoom> {
+}

+ 70 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/Ad.java

@@ -18,6 +18,18 @@ public class Ad implements Serializable {
     private String title;
 
     /**
+     * 频道页面
+     */
+    @Column(name = "`channel`")
+    private String channel;
+
+    /**
+     * 位置
+     */
+    @Column(name = "`place`")
+    private String place;
+
+    /**
      * 广告图片
      */
     @Column(name = "`image`")
@@ -82,6 +94,42 @@ public class Ad implements Serializable {
     }
 
     /**
+     * 获取频道页面
+     *
+     * @return channel - 频道页面
+     */
+    public String getChannel() {
+        return channel;
+    }
+
+    /**
+     * 设置频道页面
+     *
+     * @param channel 频道页面
+     */
+    public void setChannel(String channel) {
+        this.channel = channel;
+    }
+
+    /**
+     * 获取位置
+     *
+     * @return place - 位置
+     */
+    public String getPlace() {
+        return place;
+    }
+
+    /**
+     * 设置位置
+     *
+     * @param place 位置
+     */
+    public void setPlace(String place) {
+        this.place = place;
+    }
+
+    /**
      * 获取广告图片
      *
      * @return image - 广告图片
@@ -179,6 +227,8 @@ public class Ad implements Serializable {
         sb.append("Hash = ").append(hashCode());
         sb.append(", id=").append(id);
         sb.append(", title=").append(title);
+        sb.append(", channel=").append(channel);
+        sb.append(", place=").append(place);
         sb.append(", image=").append(image);
         sb.append(", position=").append(position);
         sb.append(", link=").append(link);
@@ -218,6 +268,26 @@ public class Ad implements Serializable {
         }
 
         /**
+         * 设置频道页面
+         *
+         * @param channel 频道页面
+         */
+        public Builder channel(String channel) {
+            obj.setChannel(channel);
+            return this;
+        }
+
+        /**
+         * 设置位置
+         *
+         * @param place 位置
+         */
+        public Builder place(String place) {
+            obj.setPlace(place);
+            return this;
+        }
+
+        /**
          * 设置广告图片
          *
          * @param image 广告图片

+ 39 - 4
server/data/src/main/java/com/qxgmat/data/dao/entity/Comment.java

@@ -39,7 +39,7 @@ public class Comment implements Serializable {
      * 位置
      */
     @Column(name = "`position`")
-    private String position;
+    private Integer position;
 
     /**
      * 是否展示
@@ -59,6 +59,12 @@ public class Comment implements Serializable {
     @Column(name = "`is_system`")
     private Integer isSystem;
 
+    /**
+     * 排序
+     */
+    @Column(name = "`order`")
+    private Integer order;
+
     @Column(name = "`create_time`")
     private Date createTime;
 
@@ -158,7 +164,7 @@ public class Comment implements Serializable {
      *
      * @return position - 位置
      */
-    public String getPosition() {
+    public Integer getPosition() {
         return position;
     }
 
@@ -167,7 +173,7 @@ public class Comment implements Serializable {
      *
      * @param position 位置
      */
-    public void setPosition(String position) {
+    public void setPosition(Integer position) {
         this.position = position;
     }
 
@@ -226,6 +232,24 @@ public class Comment implements Serializable {
     }
 
     /**
+     * 获取排序
+     *
+     * @return order - 排序
+     */
+    public Integer getOrder() {
+        return order;
+    }
+
+    /**
+     * 设置排序
+     *
+     * @param order 排序
+     */
+    public void setOrder(Integer order) {
+        this.order = order;
+    }
+
+    /**
      * @return create_time
      */
     public Date getCreateTime() {
@@ -268,6 +292,7 @@ public class Comment implements Serializable {
         sb.append(", isShow=").append(isShow);
         sb.append(", isSpecial=").append(isSpecial);
         sb.append(", isSystem=").append(isSystem);
+        sb.append(", order=").append(order);
         sb.append(", createTime=").append(createTime);
         sb.append(", content=").append(content);
         sb.append("]");
@@ -338,7 +363,7 @@ public class Comment implements Serializable {
          *
          * @param position 位置
          */
-        public Builder position(String position) {
+        public Builder position(Integer position) {
             obj.setPosition(position);
             return this;
         }
@@ -374,6 +399,16 @@ public class Comment implements Serializable {
         }
 
         /**
+         * 设置排序
+         *
+         * @param order 排序
+         */
+        public Builder order(Integer order) {
+            obj.setOrder(order);
+            return this;
+        }
+
+        /**
          * @param createTime
          */
         public Builder createTime(Date createTime) {

+ 212 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/Contract.java

@@ -0,0 +1,212 @@
+package com.qxgmat.data.dao.entity;
+
+import java.io.Serializable;
+import java.util.Date;
+import javax.persistence.*;
+
+@Table(name = "contract")
+public class Contract implements Serializable {
+    @Id
+    @Column(name = "`id`")
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Integer id;
+
+    /**
+     * 合同场景
+     */
+    @Column(name = "`key`")
+    private String key;
+
+    /**
+     * 协议名称
+     */
+    @Column(name = "`title`")
+    private String title;
+
+    @Column(name = "`create_time`")
+    private Date createTime;
+
+    @Column(name = "`update_time`")
+    private Date updateTime;
+
+    @Column(name = "`content`")
+    private String content;
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * @return id
+     */
+    public Integer getId() {
+        return id;
+    }
+
+    /**
+     * @param id
+     */
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * 获取合同场景
+     *
+     * @return key - 合同场景
+     */
+    public String getKey() {
+        return key;
+    }
+
+    /**
+     * 设置合同场景
+     *
+     * @param key 合同场景
+     */
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    /**
+     * 获取协议名称
+     *
+     * @return title - 协议名称
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * 设置协议名称
+     *
+     * @param title 协议名称
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
+     * @return create_time
+     */
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    /**
+     * @param createTime
+     */
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    /**
+     * @return update_time
+     */
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    /**
+     * @param updateTime
+     */
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    /**
+     * @return content
+     */
+    public String getContent() {
+        return content;
+    }
+
+    /**
+     * @param content
+     */
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", id=").append(id);
+        sb.append(", key=").append(key);
+        sb.append(", title=").append(title);
+        sb.append(", createTime=").append(createTime);
+        sb.append(", updateTime=").append(updateTime);
+        sb.append(", content=").append(content);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    public static Contract.Builder builder() {
+        return new Contract.Builder();
+    }
+
+    public static class Builder {
+        private Contract obj;
+
+        public Builder() {
+            this.obj = new Contract();
+        }
+
+        /**
+         * @param id
+         */
+        public Builder id(Integer id) {
+            obj.setId(id);
+            return this;
+        }
+
+        /**
+         * 设置合同场景
+         *
+         * @param key 合同场景
+         */
+        public Builder key(String key) {
+            obj.setKey(key);
+            return this;
+        }
+
+        /**
+         * 设置协议名称
+         *
+         * @param title 协议名称
+         */
+        public Builder title(String title) {
+            obj.setTitle(title);
+            return this;
+        }
+
+        /**
+         * @param createTime
+         */
+        public Builder createTime(Date createTime) {
+            obj.setCreateTime(createTime);
+            return this;
+        }
+
+        /**
+         * @param updateTime
+         */
+        public Builder updateTime(Date updateTime) {
+            obj.setUpdateTime(updateTime);
+            return this;
+        }
+
+        /**
+         * @param content
+         */
+        public Builder content(String content) {
+            obj.setContent(content);
+            return this;
+        }
+
+        public Contract build() {
+            return this.obj;
+        }
+    }
+}

+ 61 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/CourseData.java

@@ -37,6 +37,12 @@ public class CourseData implements Serializable {
     private Integer parentStructId;
 
     /**
+     * 是否是长难句
+     */
+    @Column(name = "`is_sentence`")
+    private Integer isSentence;
+
+    /**
      * 资料形式
      */
     @Column(name = "`data_type`")
@@ -120,6 +126,9 @@ public class CourseData implements Serializable {
     @Column(name = "`update_time`")
     private Date updateTime;
 
+    @Column(name = "`latest_time`")
+    private Date latestTime;
+
     /**
      * 摘要介绍
      */
@@ -233,6 +242,24 @@ public class CourseData implements Serializable {
     }
 
     /**
+     * 获取是否是长难句
+     *
+     * @return is_sentence - 是否是长难句
+     */
+    public Integer getIsSentence() {
+        return isSentence;
+    }
+
+    /**
+     * 设置是否是长难句
+     *
+     * @param isSentence 是否是长难句
+     */
+    public void setIsSentence(Integer isSentence) {
+        this.isSentence = isSentence;
+    }
+
+    /**
      * 获取资料形式
      *
      * @return data_type - 资料形式
@@ -495,6 +522,20 @@ public class CourseData implements Serializable {
     }
 
     /**
+     * @return latest_time
+     */
+    public Date getLatestTime() {
+        return latestTime;
+    }
+
+    /**
+     * @param latestTime
+     */
+    public void setLatestTime(Date latestTime) {
+        this.latestTime = latestTime;
+    }
+
+    /**
      * 获取摘要介绍
      *
      * @return description - 摘要介绍
@@ -577,6 +618,7 @@ public class CourseData implements Serializable {
         sb.append(", comment=").append(comment);
         sb.append(", structId=").append(structId);
         sb.append(", parentStructId=").append(parentStructId);
+        sb.append(", isSentence=").append(isSentence);
         sb.append(", dataType=").append(dataType);
         sb.append(", isNovice=").append(isNovice);
         sb.append(", isOriginal=").append(isOriginal);
@@ -592,6 +634,7 @@ public class CourseData implements Serializable {
         sb.append(", saleNumber=").append(saleNumber);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
+        sb.append(", latestTime=").append(latestTime);
         sb.append(", description=").append(description);
         sb.append(", content=").append(content);
         sb.append(", authorContent=").append(authorContent);
@@ -660,6 +703,16 @@ public class CourseData implements Serializable {
         }
 
         /**
+         * 设置是否是长难句
+         *
+         * @param isSentence 是否是长难句
+         */
+        public Builder isSentence(Integer isSentence) {
+            obj.setIsSentence(isSentence);
+            return this;
+        }
+
+        /**
          * 设置资料形式
          *
          * @param dataType 资料形式
@@ -806,6 +859,14 @@ public class CourseData implements Serializable {
         }
 
         /**
+         * @param latestTime
+         */
+        public Builder latestTime(Date latestTime) {
+            obj.setLatestTime(latestTime);
+            return this;
+        }
+
+        /**
          * 设置摘要介绍
          *
          * @param description 摘要介绍

+ 39 - 4
server/data/src/main/java/com/qxgmat/data/dao/entity/Faq.java

@@ -39,7 +39,7 @@ public class Faq implements Serializable {
      * 位置
      */
     @Column(name = "`position`")
-    private String position;
+    private Integer position;
 
     /**
      * 处理人
@@ -77,6 +77,12 @@ public class Faq implements Serializable {
     @Column(name = "`is_system`")
     private Integer isSystem;
 
+    /**
+     * 排序
+     */
+    @Column(name = "`order`")
+    private Integer order;
+
     @Column(name = "`create_time`")
     private Date createTime;
 
@@ -185,7 +191,7 @@ public class Faq implements Serializable {
      *
      * @return position - 位置
      */
-    public String getPosition() {
+    public Integer getPosition() {
         return position;
     }
 
@@ -194,7 +200,7 @@ public class Faq implements Serializable {
      *
      * @param position 位置
      */
-    public void setPosition(String position) {
+    public void setPosition(Integer position) {
         this.position = position;
     }
 
@@ -307,6 +313,24 @@ public class Faq implements Serializable {
     }
 
     /**
+     * 获取排序
+     *
+     * @return order - 排序
+     */
+    public Integer getOrder() {
+        return order;
+    }
+
+    /**
+     * 设置排序
+     *
+     * @param order 排序
+     */
+    public void setOrder(Integer order) {
+        this.order = order;
+    }
+
+    /**
      * @return create_time
      */
     public Date getCreateTime() {
@@ -374,6 +398,7 @@ public class Faq implements Serializable {
         sb.append(", answerStatus=").append(answerStatus);
         sb.append(", answerTime=").append(answerTime);
         sb.append(", isSystem=").append(isSystem);
+        sb.append(", order=").append(order);
         sb.append(", createTime=").append(createTime);
         sb.append(", content=").append(content);
         sb.append(", answer=").append(answer);
@@ -445,7 +470,7 @@ public class Faq implements Serializable {
          *
          * @param position 位置
          */
-        public Builder position(String position) {
+        public Builder position(Integer position) {
             obj.setPosition(position);
             return this;
         }
@@ -521,6 +546,16 @@ public class Faq implements Serializable {
         }
 
         /**
+         * 设置排序
+         *
+         * @param order 排序
+         */
+        public Builder order(Integer order) {
+            obj.setOrder(order);
+            return this;
+        }
+
+        /**
          * @param createTime
          */
         public Builder createTime(Date createTime) {

+ 77 - 7
server/data/src/main/java/com/qxgmat/data/dao/entity/Message.java

@@ -4,8 +4,8 @@ import java.io.Serializable;
 import java.util.Date;
 import javax.persistence.*;
 
-@Table(name = "message")
-public class Message implements Serializable {
+@Table(name = "message_template")
+public class MessageTemplate implements Serializable {
     @Id
     @Column(name = "`id`")
     @GeneratedValue(strategy = GenerationType.IDENTITY)
@@ -18,6 +18,18 @@ public class Message implements Serializable {
     private String title;
 
     /**
+     * 消息形式
+     */
+    @Column(name = "`message_method`")
+    private String messageMethod;
+
+    /**
+     * 消息类型
+     */
+    @Column(name = "`message_category`")
+    private String messageCategory;
+
+    /**
      * 消息链接
      */
     @Column(name = "`link`")
@@ -85,6 +97,42 @@ public class Message implements Serializable {
     }
 
     /**
+     * 获取消息形式
+     *
+     * @return message_method - 消息形式
+     */
+    public String getMessageMethod() {
+        return messageMethod;
+    }
+
+    /**
+     * 设置消息形式
+     *
+     * @param messageMethod 消息形式
+     */
+    public void setMessageMethod(String messageMethod) {
+        this.messageMethod = messageMethod;
+    }
+
+    /**
+     * 获取消息类型
+     *
+     * @return message_category - 消息类型
+     */
+    public String getMessageCategory() {
+        return messageCategory;
+    }
+
+    /**
+     * 设置消息类型
+     *
+     * @param messageCategory 消息类型
+     */
+    public void setMessageCategory(String messageCategory) {
+        this.messageCategory = messageCategory;
+    }
+
+    /**
      * 获取消息链接
      *
      * @return link - 消息链接
@@ -196,6 +244,8 @@ public class Message implements Serializable {
         sb.append("Hash = ").append(hashCode());
         sb.append(", id=").append(id);
         sb.append(", title=").append(title);
+        sb.append(", messageMethod=").append(messageMethod);
+        sb.append(", messageCategory=").append(messageCategory);
         sb.append(", link=").append(link);
         sb.append(", sendTime=").append(sendTime);
         sb.append(", sendNumber=").append(sendNumber);
@@ -206,15 +256,15 @@ public class Message implements Serializable {
         return sb.toString();
     }
 
-    public static Message.Builder builder() {
-        return new Message.Builder();
+    public static MessageTemplate.Builder builder() {
+        return new MessageTemplate.Builder();
     }
 
     public static class Builder {
-        private Message obj;
+        private MessageTemplate obj;
 
         public Builder() {
-            this.obj = new Message();
+            this.obj = new MessageTemplate();
         }
 
         /**
@@ -236,6 +286,26 @@ public class Message implements Serializable {
         }
 
         /**
+         * 设置消息形式
+         *
+         * @param messageMethod 消息形式
+         */
+        public Builder messageMethod(String messageMethod) {
+            obj.setMessageMethod(messageMethod);
+            return this;
+        }
+
+        /**
+         * 设置消息类型
+         *
+         * @param messageCategory 消息类型
+         */
+        public Builder messageCategory(String messageCategory) {
+            obj.setMessageCategory(messageCategory);
+            return this;
+        }
+
+        /**
          * 设置消息链接
          *
          * @param link 消息链接
@@ -293,7 +363,7 @@ public class Message implements Serializable {
             return this;
         }
 
-        public Message build() {
+        public MessageTemplate build() {
             return this.obj;
         }
     }

+ 291 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/ReadyArticle.java

@@ -0,0 +1,291 @@
+package com.qxgmat.data.dao.entity;
+
+import java.io.Serializable;
+import java.util.Date;
+import javax.persistence.*;
+
+@Table(name = "ready_article")
+public class ReadyArticle implements Serializable {
+    @Id
+    @Column(name = "`id`")
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Integer id;
+
+    /**
+     * 标题
+     */
+    @Column(name = "`title`")
+    private String title;
+
+    /**
+     * 二级标题
+     */
+    @Column(name = "`category_id`")
+    private Integer categoryId;
+
+    /**
+     * 一级标题
+     */
+    @Column(name = "`parent_category_id`")
+    private Integer parentCategoryId;
+
+    /**
+     * 访问量
+     */
+    @Column(name = "`view_number`")
+    private Integer viewNumber;
+
+    @Column(name = "`create_time`")
+    private Date createTime;
+
+    @Column(name = "`update_time`")
+    private Date updateTime;
+
+    /**
+     * 内容
+     */
+    @Column(name = "`content`")
+    private String content;
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * @return id
+     */
+    public Integer getId() {
+        return id;
+    }
+
+    /**
+     * @param id
+     */
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    /**
+     * 获取标题
+     *
+     * @return title - 标题
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * 设置标题
+     *
+     * @param title 标题
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
+     * 获取二级标题
+     *
+     * @return category_id - 二级标题
+     */
+    public Integer getCategoryId() {
+        return categoryId;
+    }
+
+    /**
+     * 设置二级标题
+     *
+     * @param categoryId 二级标题
+     */
+    public void setCategoryId(Integer categoryId) {
+        this.categoryId = categoryId;
+    }
+
+    /**
+     * 获取一级标题
+     *
+     * @return parent_category_id - 一级标题
+     */
+    public Integer getParentCategoryId() {
+        return parentCategoryId;
+    }
+
+    /**
+     * 设置一级标题
+     *
+     * @param parentCategoryId 一级标题
+     */
+    public void setParentCategoryId(Integer parentCategoryId) {
+        this.parentCategoryId = parentCategoryId;
+    }
+
+    /**
+     * 获取访问量
+     *
+     * @return view_number - 访问量
+     */
+    public Integer getViewNumber() {
+        return viewNumber;
+    }
+
+    /**
+     * 设置访问量
+     *
+     * @param viewNumber 访问量
+     */
+    public void setViewNumber(Integer viewNumber) {
+        this.viewNumber = viewNumber;
+    }
+
+    /**
+     * @return create_time
+     */
+    public Date getCreateTime() {
+        return createTime;
+    }
+
+    /**
+     * @param createTime
+     */
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    /**
+     * @return update_time
+     */
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    /**
+     * @param updateTime
+     */
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    /**
+     * 获取内容
+     *
+     * @return content - 内容
+     */
+    public String getContent() {
+        return content;
+    }
+
+    /**
+     * 设置内容
+     *
+     * @param content 内容
+     */
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", id=").append(id);
+        sb.append(", title=").append(title);
+        sb.append(", categoryId=").append(categoryId);
+        sb.append(", parentCategoryId=").append(parentCategoryId);
+        sb.append(", viewNumber=").append(viewNumber);
+        sb.append(", createTime=").append(createTime);
+        sb.append(", updateTime=").append(updateTime);
+        sb.append(", content=").append(content);
+        sb.append("]");
+        return sb.toString();
+    }
+
+    public static ReadyArticle.Builder builder() {
+        return new ReadyArticle.Builder();
+    }
+
+    public static class Builder {
+        private ReadyArticle obj;
+
+        public Builder() {
+            this.obj = new ReadyArticle();
+        }
+
+        /**
+         * @param id
+         */
+        public Builder id(Integer id) {
+            obj.setId(id);
+            return this;
+        }
+
+        /**
+         * 设置标题
+         *
+         * @param title 标题
+         */
+        public Builder title(String title) {
+            obj.setTitle(title);
+            return this;
+        }
+
+        /**
+         * 设置二级标题
+         *
+         * @param categoryId 二级标题
+         */
+        public Builder categoryId(Integer categoryId) {
+            obj.setCategoryId(categoryId);
+            return this;
+        }
+
+        /**
+         * 设置一级标题
+         *
+         * @param parentCategoryId 一级标题
+         */
+        public Builder parentCategoryId(Integer parentCategoryId) {
+            obj.setParentCategoryId(parentCategoryId);
+            return this;
+        }
+
+        /**
+         * 设置访问量
+         *
+         * @param viewNumber 访问量
+         */
+        public Builder viewNumber(Integer viewNumber) {
+            obj.setViewNumber(viewNumber);
+            return this;
+        }
+
+        /**
+         * @param createTime
+         */
+        public Builder createTime(Date createTime) {
+            obj.setCreateTime(createTime);
+            return this;
+        }
+
+        /**
+         * @param updateTime
+         */
+        public Builder updateTime(Date updateTime) {
+            obj.setUpdateTime(updateTime);
+            return this;
+        }
+
+        /**
+         * 设置内容
+         *
+         * @param content 内容
+         */
+        public Builder content(String content) {
+            obj.setContent(content);
+            return this;
+        }
+
+        public ReadyArticle build() {
+            return this.obj;
+        }
+    }
+}

+ 0 - 0
server/data/src/main/java/com/qxgmat/data/dao/entity/ReadyArticleCategory.java


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