Bladeren bron

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

# Conflicts:
#	front/project/www/components/Login/index.js
KaysonCui 5 jaren geleden
bovenliggende
commit
6b5bf2107a
100 gewijzigde bestanden met toevoegingen van 2092 en 2395 verwijderingen
  1. 28 5
      front/.eslintrc
  2. 5 0
      front/config/index.js
  3. 15 3
      front/config/local.json
  4. 16 16
      front/project/Constant.js
  5. 2 2
      front/project/admin/routes/course/data/page.js
  6. 0 1
      front/project/admin/routes/course/experience/page.js
  7. 2 0
      front/project/admin/routes/course/invoice/page.js
  8. 2 2
      front/project/admin/routes/interaction/askQuestion/index.js
  9. 13 14
      front/project/admin/routes/interaction/askQuestion/page.js
  10. 3 3
      front/project/admin/routes/interaction/askQuestionDetail/index.js
  11. 16 13
      front/project/admin/routes/interaction/askQuestionDetail/page.js
  12. 1 1
      front/project/admin/routes/interaction/comment/index.js
  13. 2 1
      front/project/admin/routes/interaction/comment/page.js
  14. 1 1
      front/project/admin/routes/interaction/faq/index.less
  15. 3 3
      front/project/admin/routes/setting/index/page.js
  16. 9 0
      front/project/admin/routes/setting/service/page.js
  17. 44 2
      front/project/admin/routes/setting/time/page.js
  18. 2 2
      front/project/admin/routes/show/faq/page.js
  19. 18 3
      front/project/admin/routes/student/askCourse/page.js
  20. 15 12
      front/project/admin/routes/student/askCourseDetail/page.js
  21. 15 3
      front/project/admin/routes/student/askQuestion/page.js
  22. 15 12
      front/project/admin/routes/student/askQuestionDetail/page.js
  23. 3 3
      front/project/admin/routes/student/studyDetail/page.js
  24. 1 3
      front/project/admin/routes/subject/examination/page.js
  25. 4 2
      front/project/admin/routes/subject/exercise/page.js
  26. 2 2
      front/project/admin/routes/subject/question/page.js
  27. 4 8
      front/project/admin/routes/subject/textbook/page.js
  28. 8 2
      front/project/admin/routes/user/abnormal/page.js
  29. 5 5
      front/project/admin/routes/user/detail/page.js
  30. 2 0
      front/project/admin/routes/user/list/page.js
  31. 3 2
      front/project/admin/routes/user/order/page.js
  32. 9 1
      front/project/admin/routes/user/recordAll/page.js
  33. 8 0
      front/project/admin/stores/system.js
  34. 6 3
      front/project/h5/components/Block/index.js
  35. 6 5
      front/project/h5/components/Item/index.js
  36. 1 1
      front/project/h5/routes/page/bind/page.js
  37. 2 2
      front/project/h5/routes/page/home/page.js
  38. 1 1
      front/project/h5/routes/page/id/page.js
  39. 5 1
      front/project/h5/routes/product/bought/page.js
  40. 1 0
      front/project/h5/routes/product/courseDetail/index.js
  41. 25 3
      front/project/h5/routes/product/courseDetail/page.js
  42. 1 0
      front/project/h5/routes/product/coursePackage/index.js
  43. 1 0
      front/project/h5/routes/product/courseVideo/index.js
  44. 1 0
      front/project/h5/routes/product/courseVs/index.js
  45. 25 3
      front/project/h5/routes/product/courseVs/page.js
  46. 1 0
      front/project/h5/routes/product/data/index.js
  47. 1 0
      front/project/h5/routes/product/dataDetail/index.js
  48. 24 7
      front/project/h5/routes/product/dataDetail/page.js
  49. 1 0
      front/project/h5/routes/product/dataHistory/index.js
  50. 1 0
      front/project/h5/routes/product/main/index.js
  51. 1 1
      front/project/h5/routes/product/main/index.less
  52. 2 2
      front/project/h5/routes/product/main/page.js
  53. 1 0
      front/project/h5/routes/product/serviceDetail/index.js
  54. 1 0
      front/project/h5/routes/textbook/detail/index.js
  55. 1 0
      front/project/h5/routes/textbook/library/index.js
  56. 1 0
      front/project/h5/routes/textbook/main/index.js
  57. 5 1
      front/project/h5/stores/course.js
  58. BIN
      front/project/www/assets/up - 副本.png
  59. 1 1
      front/project/www/components/AnswerList/index.js
  60. 1 1
      front/project/www/components/AnswerTable/index.less
  61. 2 2
      front/project/www/components/Icon/index.js
  62. 240 0
      front/project/www/components/Icon/index.less
  63. 3 7
      front/project/www/components/Login/index.js
  64. 7 7
      front/project/www/components/UserAction/index.js
  65. 17 0
      front/project/www/components/UserAction/index.less
  66. 8 8
      front/project/www/components/UserTable/index.js
  67. 10 2
      front/project/www/components/UserTable/index.less
  68. 1 0
      front/project/www/routes/examination/list/index.js
  69. 1 0
      front/project/www/routes/examination/main/index.js
  70. 1 0
      front/project/www/routes/exercise/list/index.js
  71. 1 0
      front/project/www/routes/exercise/main/index.js
  72. 2 1
      front/project/www/routes/index.js
  73. 2 0
      front/project/www/routes/my/course/index.less
  74. 25 38
      front/project/www/routes/my/course/page.js
  75. 18 5
      front/project/www/routes/my/error/page.js
  76. 22 3
      front/project/www/routes/my/main/index.less
  77. 12 7
      front/project/www/routes/my/main/page.js
  78. 111 2
      front/project/www/routes/my/report/page.js
  79. 1 0
      front/project/www/routes/page/home/index.js
  80. 1 0
      front/project/www/routes/paper/process/index.js
  81. 607 0
      front/project/www/routes/paper/question/detail/index.js
  82. 500 0
      front/project/www/routes/paper/question/detail/index.less
  83. 1 0
      front/project/www/routes/paper/question/index.js
  84. 0 493
      front/project/www/routes/paper/question/index.less
  85. 11 572
      front/project/www/routes/paper/question/page.js
  86. 1 0
      front/project/www/routes/paper/report/index.js
  87. 1 0
      front/project/www/routes/question/detail/index.js
  88. 8 452
      front/project/www/routes/question/detail/index.less
  89. 7 601
      front/project/www/routes/question/detail/page.js
  90. 1 0
      front/project/www/routes/sentence/read/index.js
  91. 1 0
      front/project/www/routes/textbook/list/index.js
  92. 27 8
      front/project/www/stores/course.js
  93. 9 1
      front/project/www/stores/main.js
  94. 2 2
      front/project/www/stores/order.js
  95. 3 3
      front/project/www/stores/question.js
  96. 8 0
      front/project/www/stores/textbook.js
  97. 1 1
      front/src/components/Assets/index.less
  98. 2 2
      front/src/containers/Async.js
  99. 24 19
      front/src/services/AsyncTools.js
  100. 0 0
      server/data/src/main/java/com/qxgmat/data/constants/enums/status/AskStatus.java

+ 28 - 5
front/.eslintrc

@@ -1,11 +1,30 @@
 {
   "parser": "babel-eslint",
-  "extends": ["standard-react", "airbnb-base"],
-  "plugins": ["react", "import"],
+  "extends": [
+    "standard-react",
+    "airbnb-base"
+  ],
+  "plugins": [
+    "react",
+    "import"
+  ],
   "settings": {
     "import/resolver": {
       "alias": {
-        "map": [["@src", "./src"], ["@project", "./project/admin"], ["@components", "./components"]]
+        "map": [
+          [
+            "@src",
+            "./src"
+          ],
+          [
+            "@project",
+            "./project/admin"
+          ],
+          [
+            "@components",
+            "./components"
+          ]
+        ]
       }
     }
   },
@@ -34,7 +53,11 @@
     "sha1": false,
     "wx": false,
     "CKEDITOR": false,
-    "Masonry": false
+    "Masonry": false,
+    "__PcUrl__": false,
+    "__H5Url__": false,
+    "__WechatPcAppId__": false,
+    "__WechatH5AppId__": false
   },
   "rules": {
     "camelcase": "off",
@@ -61,4 +84,4 @@
     "import/no-named-as-default": "off",
     "operator-linebreak": "off"
   }
-}
+}

+ 5 - 0
front/config/index.js

@@ -52,6 +52,11 @@ config.globals = {
   __DEBUG__: config.env === 'development' || config.env === 'test',
   __API_PATH__: `\'${config.api_path}\'`,
   __BASE_NAME__: `\'${config.basename}\'`,
+
+  __PcUrl__: `\'${config.PcUrl}\'`,
+  __WechatPcAppId__: `\'${config.WechatPcAppId}\'`,
+  __H5Url__: `\'${config.H5Url}\'`,
+  __WechatH5AppId__: `\'${config.WechatH5AppId}\'`,
 };
 
 debug(`Looking for environment overrides for NODE_ENV '${config.env}'.`);

+ 15 - 3
front/config/local.json

@@ -14,7 +14,11 @@
         "from": "/",
         "to": "/"
       }
-    ]
+    ],
+    "PcUrl": "http://www.baidu.com",
+    "WechatPcAppId": "wx324965bb6800f9b9",
+    "H5Url": "http://127.0.0.1:3000",
+    "WechatH5AppId": "wxbee75af2ece94ed7"
   },
   "test": {
     "serverHost": "127.0.0.1",
@@ -24,7 +28,11 @@
     "keyword": "",
     "scripts": [],
     "publicPath": "/",
-    "basename": "/"
+    "basename": "/",
+    "PcUrl": "http://www.baidu.com",
+    "WechatPcAppId": "wx324965bb6800f9b9",
+    "H5Url": "http://127.0.0.1:3000",
+    "WechatH5AppId": "wxbee75af2ece94ed7"
   },
   "production": {
     "serverHost": "127.0.0.1",
@@ -34,6 +42,10 @@
     "keyword": "",
     "scripts": [],
     "publicPath": "/",
-    "basename": "/"
+    "basename": "/",
+    "PcUrl": "http://www.baidu.com",
+    "WechatPcAppId": "wx324965bb6800f9b9",
+    "H5Url": "http://127.0.0.1:3000",
+    "WechatH5AppId": "wxbee75af2ece94ed7"
   }
 }

+ 16 - 16
front/project/Constant.js

@@ -1,10 +1,10 @@
-export const UserUrl = 'http://www.baidu.com';
+export const PcUrl = __PcUrl__;
 
-export const WechatUserAppId = 'wx324965bb6800f9b9';
+export const WechatPcAppId = __WechatPcAppId__;
 
-export const H5Url = 'http://127.0.0.1:3000';
+export const H5Url = __H5Url__;
 
-export const WechatH5AppId = 'wxbee75af2ece94ed7';
+export const WechatH5AppId = __WechatH5AppId__;
 
 export const QuestionDifficult = [{ label: 'Easy', value: 'easy', sort: 2 }, { label: 'Medium', value: 'medium', sort: 1 }, { label: 'Hard', value: 'hard', sort: 0 }];
 
@@ -109,19 +109,19 @@ export const ContractKey = [{ label: '注册', value: 'register' }, { label: '
 export const RoomPosition = [{ label: '北部', value: 'north' }, { label: '东部', value: 'east' }, { label: '中南', value: 'south_central' }, { label: '西部', value: 'west' }, { label: '海外', value: 'overseas' }];
 
 export const MessageCategory = [
-  { label: '注册消息', value: 'register', params: [''] },
-  { label: '登录异常', value: 'login_abnormal', params: [''] },
+  { label: '注册消息', value: 'register', params: ['mobile'] },
+  { label: '登录异常', value: 'login_abnormal', params: ['nickname', 'ip', 'city'] },
   { label: '机经更新', value: 'textbook_update', params: [''] },
-  { label: '预习作业提醒', value: 'preview_notice', params: [''] },
-  { label: '支付成功提醒', value: 'payed', params: [''] },
-  { label: '资料更新', value: 'data_update', params: [''] },
-  { label: '题目提问回复', value: 'ask_auestion', params: [''] },
-  { label: '课程提问回复', value: 'ask_course', params: [''] },
-  { label: '咨询回复', value: 'faq_callback', params: [''] },
-  { label: '纠错回复', value: 'feedback_callback', params: [''] },
-  { label: '邀请好友注册', value: 'invited', params: [''] },
-  { label: '邮箱变更', value: 'email_change', params: [''] },
-  { label: '自定义消息', value: 'custom', params: [''] },
+  { label: '预习作业提醒', value: 'preview_notice', params: ['courseTitle', 'title'] },
+  { label: '支付成功提醒', value: 'payed', params: ['money'] },
+  { label: '资料更新', value: 'data_update', params: ['title', 'time'] },
+  { label: '题目提问回复', value: 'ask_question', params: ['content', 'time', 'status'] },
+  { label: '课程提问回复', value: 'ask_course', params: ['content', 'time', 'status'] },
+  { label: '咨询回复', value: 'faq_callback', params: ['content', 'answer', 'channel'] },
+  { label: '纠错回复', value: 'feedback_callback', params: ['content', 'object', 'correct', 'status'] },
+  { label: '邀请好友注册', value: 'invited', params: ['code', 'nickname', 'url'] },
+  { label: '邮箱变更', value: 'email_change', params: ['nickname', 'email'] },
+  { label: '自定义消息', value: 'custom', params: ['nickname'] },
 ];
 
 export const MessageEmailStatus = [

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

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

+ 0 - 1
front/project/admin/routes/course/experience/page.js

@@ -189,7 +189,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}

+ 2 - 0
front/project/admin/routes/course/invoice/page.js

@@ -41,9 +41,11 @@ export default class extends Page {
     this.actionList = [{
       key: 'download',
       name: '下载',
+      needSelect: 1,
     }, {
       key: 'finish',
       name: '批量开票',
+      needSelect: 1,
     }];
     this.columns = [{
       title: '申请时间',

+ 2 - 2
front/project/admin/routes/interaction/askQuestion/index.js

@@ -2,8 +2,8 @@ import module from '../../module';
 import group from '../group';
 
 export default {
-  path: '/innteraction/ask/question',
-  key: 'innteraction-ask-question',
+  path: '/interaction/ask/question',
+  key: 'interaction-ask-question',
   title: '非学员提问',
   needLogin: true,
   module,

+ 13 - 14
front/project/admin/routes/interaction/askQuestion/page.js

@@ -6,7 +6,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, formatSeconds } from '@src/services/Tools';
+import { getMap, bindSearch, formatDate } from '@src/services/Tools';
 import { asyncSMessage, asyncDelConfirm } from '@src/services/AsyncTools';
 import { QuestionType, AskStatus, MoneyRange, SwitchSelect, AskTarget, AskModule } from '../../../../Constant';
 import { User } from '../../../stores/user';
@@ -81,6 +81,10 @@ export default class extends Page {
       select: [],
       number: true,
       placeholder: '请输入',
+    }, {
+      key: 'time',
+      type: 'daterange',
+      name: '提问时间',
     }];
     this.columns = [{
       title: '板块',
@@ -110,22 +114,11 @@ export default class extends Page {
         return formatDate(text);
       },
     }, {
-      title: '倒计时',
-      dataIndex: 'askTime',
-      render: (text, record) => {
-        const end = new Date(record.answerTime) || new Date();
-        const cost = (end.getTime() - new Date(record.createTime).getTime()) / 1000;
-        if (text) {
-          if (text - cost > 0) return `${formatSeconds(text - cost)}/${formatSeconds(text)}`;
-          return `0/${formatSeconds(text)}`;
-        }
-        return '-';
-      },
-    }, {
       title: '回答者',
       dataIndex: 'manager.username',
     }, {
       title: '回答时间',
+      sorter: true,
       dataIndex: 'answerTime',
       render: (text) => {
         return text ? formatDate(text) : '';
@@ -172,7 +165,13 @@ export default class extends Page {
   }
 
   initData() {
-    Question.listAsk(Object.assign({ hasRecord: false }, this.state.search)).then(result => {
+    const { search } = this.state;
+    const data = Object.assign({ hasRecord: false }, search);
+    if (data.time) {
+      data.startTime = data.time[0] || '';
+      data.endTime = data.time[1] || '';
+    }
+    Question.listAsk(data).then(result => {
       this.setTableData(result.list, result.total);
     });
   }

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

@@ -2,13 +2,13 @@ import module from '../../module';
 import group from '../group';
 
 export default {
-  path: '/innteraction/ask/question/detail/:id?',
-  key: 'innteraction-ask-question-detail',
+  path: '/interaction/ask/question/detail/:id?',
+  key: 'interaction-ask-question-detail',
   title: '提问详情',
   needLogin: true,
   module,
   group,
-  showKey: 'innteraction-ask-question',
+  showKey: 'interaction-ask-question',
   component() {
     return import('./page');
   },

+ 16 - 13
front/project/admin/routes/interaction/askQuestionDetail/page.js

@@ -8,7 +8,7 @@ import DragList from '@src/components/DragList';
 // import FileUpload from '@src/components/FileUpload';
 import { formatFormError, formatDate, getMap } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
-import { UserUrl, AskTarget, QuestionType, AskModule } from '../../../../Constant';
+import { PcUrl, AskTarget, QuestionType, AskModule } from '../../../../Constant';
 // import { User } from '../../../stores/user';
 import { Question } from '../../../stores/question';
 
@@ -75,7 +75,6 @@ export default class extends Page {
       if (!err) {
         const data = form.getFieldsValue();
         data.showStatus = data.showStatus ? 1 : 0;
-        data.ignoreStatus = data.ignoreStatus ? 1 : 0;
         data.other = this.state.data.others.map(row => row.id);
         Question.editAsk(data).then(() => {
           asyncSMessage('保存成功');
@@ -86,6 +85,14 @@ export default class extends Page {
     });
   }
 
+  ignore() {
+    const { data } = this.state;
+    Question.editAsk({ id: data.id, ignoreStatus: 1 }).then(() => {
+      asyncSMessage('操作成功');
+      goBack();
+    });
+  }
+
   renderBase() {
     const { data } = this.state;
     const { question = {}, questionNo = {} } = data;
@@ -96,7 +103,7 @@ export default class extends Page {
           {AskModuleMap[data.askModule]}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='题型'>
-          {QuestionTypeMap[question.type]}
+          {QuestionTypeMap[question.questionType]}
         </Form.Item>
         <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='题目id'>
           <a href='' target='_blank'>{questionNo.no}</a>
@@ -150,7 +157,7 @@ export default class extends Page {
           this.orderQuestion(oldIndex, newIndex);
         }}
         renderItem={(item) => (
-          <List.Item actions={[<Icon type='bars' className='icon' />, <Typography.Text copyable={{ text: `${UserUrl}/paper/question/${this.state.data.userQuestionId}?askId=${item.id}` }} />]}>
+          <List.Item actions={[<Icon type='bars' className='icon' />, <Typography.Text copyable={{ text: `${PcUrl}/paper/question/${this.state.data.userQuestionId}?askId=${item.id}` }} />]}>
             <Row style={{ width: '100%' }}>
               <Col span={11}>问题:<span dangerouslySetInnerHTML={{ __html: item.content }} /></Col>
               <Col span={11} offset={1}>答复:<span dangerouslySetInnerHTML={{ __html: item.answer }} /></Col>
@@ -187,15 +194,6 @@ export default class extends Page {
               )}
             </Form.Item>
           </Col>
-          <Col span={12}>
-            <Form.Item labelCol={{ span: 12 }} wrapperCol={{ span: 10 }} label='是否忽略'>
-              {getFieldDecorator('ignoreStatus', {
-                valuePropName: 'checked',
-              })(
-                <Switch checkedChildren='on' unCheckedChildren='off' />,
-              )}
-            </Form.Item>
-          </Col>
         </Row>
       </Form>
     </Block>;
@@ -210,6 +208,11 @@ export default class extends Page {
 
       <Row type="flex" justify="center">
         <Col>
+          <Button type="drange" onClick={() => {
+            this.ignore();
+          }}>忽略问题</Button>
+        </Col>
+        <Col>
           <Button type="primary" onClick={() => {
             this.submit();
           }}>保存</Button>

+ 1 - 1
front/project/admin/routes/interaction/comment/index.js

@@ -3,7 +3,7 @@ import group from '../group';
 
 export default {
   path: '/interaction/comment',
-  key: 'innteraction-comment',
+  key: 'interaction-comment',
   title: '评价',
   needLogin: true,
   module,

+ 2 - 1
front/project/admin/routes/interaction/comment/page.js

@@ -91,7 +91,8 @@ export default class extends Page {
         return `${text.nickname || record.nickname}${extend ? `(${extend})` : ''}`;
       },
     }, {
-      title: '时间',
+      title: '评价时间',
+      sorter: true,
       dataIndex: 'createTime',
       render: (text) => {
         return formatDate(text);

+ 1 - 1
front/project/admin/routes/interaction/faq/index.less

@@ -1,3 +1,3 @@
 @charset "utf-8";
 
-#innteraction-faq {}
+#interaction-faq {}

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

@@ -38,9 +38,9 @@ export default class extends Page {
     form.validateFields((err) => {
       if (!err) {
         const data = form.getFieldsValue();
-        data.class = Object.keys(data.class).map((key) => data.class[key]);
-        data.activity = Object.keys(data.activity).map((key) => data.activity[key]);
-        data.evaluation = Object.keys(data.evaluation).map((key) => data.evaluation[key]);
+        data.class = Object.keys(data.class || {}).map((key) => data.class[key]);
+        data.activity = Object.keys(data.activity || {}).map((key) => data.activity[key]);
+        data.evaluation = Object.keys(data.evaluation || {}).map((key) => data.evaluation[key]);
         System.setIndex(data)
           .then(() => {
             this.setState(data);

+ 9 - 0
front/project/admin/routes/setting/service/page.js

@@ -64,6 +64,12 @@ export default class extends Page {
     this.setState({ [field]: data });
   }
 
+  changeValue(field, key, value) {
+    const data = this.state[field] || {};
+    data[key] = value;
+    this.setStaet({ [field]: data });
+  }
+
   submit(tab) {
     let handler;
     if (tab === 'qx_cat') {
@@ -178,6 +184,7 @@ export default class extends Page {
               showUploadList={false}
               beforeUpload={(file) => System.uploadImage(file).then((result) => {
                 setFieldsValue({ 'qx_cat.image': result.url });
+                this.changeValue('qx_cat', 'image', result.url);
                 return Promise.reject();
               })}
             >
@@ -208,6 +215,7 @@ export default class extends Page {
               showUploadList={false}
               beforeUpload={(file) => System.uploadImage(file).then((result) => {
                 setFieldsValue({ 'textbook.image': result.url });
+                this.changeValue('textbook', 'image', result.url);
                 return Promise.reject();
               })}
             >
@@ -303,6 +311,7 @@ export default class extends Page {
             showUploadList={false}
             beforeUpload={(file) => System.uploadImage(file).then((result) => {
               setFieldsValue({ 'vip.image': result.url });
+              this.changeValue('vip', 'image', result.url);
               return Promise.reject();
             })}
           >

+ 44 - 2
front/project/admin/routes/setting/time/page.js

@@ -1,5 +1,5 @@
 import React from 'react';
-import { Tabs, Form, Row, Col, InputNumber, Button, Switch } from 'antd';
+import { Tabs, Form, Row, Col, InputNumber, Button, Switch, DatePicker } from 'antd';
 import './index.less';
 import Page from '@src/containers/Page';
 import Block from '@src/components/Block';
@@ -75,6 +75,9 @@ export default class extends Page {
     if (tab === 'filter') {
       return this.refreshFilter();
     }
+    if (tab === 'paper') {
+      return this.refreshExercisePaperAuto();
+    }
     return Promise.reject();
   }
 
@@ -90,6 +93,12 @@ export default class extends Page {
     });
   }
 
+  refreshExercisePaperAuto() {
+    return System.getExercisePaperAuto().then((result) => {
+      this.setState({ exercisePaperAuto: result });
+    });
+  }
+
   refreshSentence() {
     return System.getSentenceTime().then((result) => {
       this.setState({ sentence: result || {} });
@@ -165,7 +174,8 @@ export default class extends Page {
       handler = this.submitExercise()
         .then(() => {
           return this.submitTextbook();
-        }).then(() => {
+        })
+        .then(() => {
           return this.submitSentence();
         });
     }
@@ -175,6 +185,9 @@ export default class extends Page {
     if (tab === 'filter') {
       handler = this.submitFilter();
     }
+    if (tab === 'paper') {
+      handler = this.submitExercisePaperAuto();
+    }
     handler.then(() => {
       asyncSMessage('保存成功');
     });
@@ -190,6 +203,11 @@ export default class extends Page {
     return System.setExerciseTime(exercise);
   }
 
+  submitExercisePaperAuto() {
+    const { exercisePaperAuto } = this.state;
+    return System.setExercisePaperAuto(exercisePaperAuto);
+  }
+
   submitSentence() {
     const { sentence } = this.state;
     return System.setSentenceTime(sentence);
@@ -317,6 +335,27 @@ export default class extends Page {
     </Form>;
   }
 
+  renderExercisePaperAuto() {
+    const { getFieldDecorator } = this.props.form;
+    return <Form>
+      <Row>
+        <Col span={12}>
+          <Form.Item labelCol={{ span: 10 }} wrapperCol={{ span: 14 }} label='下次错误率组卷'>
+            {getFieldDecorator('exercisePaper.date', {
+              rules: [
+                { required: true, message: '请输入下次练习错误率组卷时间' },
+              ],
+            })(
+              <DatePicker placeholder='请输入下次练习错误率组卷时间' onChange={(value) => {
+                this.changeValue('exercisePaper', 'date', value);
+              }} />,
+            )}
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form>;
+  }
+
   renderExamination() {
     return <TableLayout
       columns={this.examinationColumns}
@@ -344,6 +383,9 @@ export default class extends Page {
         <Tabs.TabPane tab="预估考试时间" key="examination">
           {this.renderExamination()}
         </Tabs.TabPane>
+        <Tabs.TabPane tab="组卷" key="paper">
+          {this.renderExercisePaperAuto()}
+        </Tabs.TabPane>
       </Tabs>
       {tab !== 'examination' && <Row type="flex" justify="center">
         <Col>

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

@@ -284,8 +284,8 @@ export default class extends Page {
         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>
+              <Col span={8}>Q: {item.content}</Col>
+              <Col span={15} offset={1}>A: {item.answer}</Col>
             </Row>
           </List.Item>
         )}

+ 18 - 3
front/project/admin/routes/student/askCourse/page.js

@@ -68,6 +68,10 @@ export default class extends Page {
       name: '消费金额',
       select: MoneyRange,
       number: true,
+    }, {
+      key: 'time',
+      type: 'daterange',
+      name: '提问时间',
     }];
     this.columns = [{
       title: '学科',
@@ -101,12 +105,13 @@ export default class extends Page {
       },
     }, {
       title: '倒计时',
-      dataIndex: 'askTime',
+      sorter: true,
+      dataIndex: 'expireTime',
       render: (text, record) => {
         const end = new Date(record.answerTime) || new Date();
         const cost = (end.getTime() - new Date(record.createTime).getTime()) / 1000;
         if (text) {
-          if (text - cost > 0) return `${formatSeconds(text - cost)}/${formatSeconds(text)}`;
+          if (record.askTime - cost > 0) return `${formatSeconds(text - cost)}/${formatSeconds(text)}`;
           return `0/${formatSeconds(text)}`;
         }
         return '-';
@@ -116,6 +121,7 @@ export default class extends Page {
       dataIndex: 'manager.username',
     }, {
       title: '回答时间',
+      sorter: true,
       dataIndex: 'answerTime',
       render: (text) => {
         return text ? formatDate(text) : '';
@@ -172,7 +178,13 @@ export default class extends Page {
   }
 
   initData() {
-    Course.listAsk(this.state.search).then(result => {
+    const { search } = this.state;
+    const data = Object.assign({ hasRecord: true }, search);
+    if (data.time) {
+      data.startTime = data.time[0] || '';
+      data.endTime = data.time[1] || '';
+    }
+    Course.listAsk(data).then(result => {
       this.setTableData(result.list, result.total);
     });
   }
@@ -195,6 +207,9 @@ export default class extends Page {
         itemList={this.filterForm}
         data={this.state.search}
         onChange={data => {
+          if (data.time.length > 0) {
+            data.time = [data.time[0].format('YYYY-MM-DD HH:mm:ss'), data.time[1].format('YYYY-MM-DD HH:mm:ss')];
+          }
           this.search(data);
         }} />}
       {/* <ActionLayout

+ 15 - 12
front/project/admin/routes/student/askCourseDetail/page.js

@@ -8,7 +8,7 @@ import DragList from '@src/components/DragList';
 // import FileUpload from '@src/components/FileUpload';
 import { formatFormError, formatDate, formatTreeData, getMap } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
-import { UserUrl } from '../../../../Constant';
+import { PcUrl } from '../../../../Constant';
 import { Exercise } from '../../../stores/exercise';
 import { Course } from '../../../stores/course';
 
@@ -79,7 +79,6 @@ export default class extends Page {
       if (!err) {
         const data = form.getFieldsValue();
         data.showStatus = data.showStatus ? 1 : 0;
-        data.ignoreStatus = data.ignoreStatus ? 1 : 0;
         data.other = this.state.data.others.map(row => row.id);
         Course.editAsk(data).then(() => {
           asyncSMessage('保存成功');
@@ -90,6 +89,14 @@ export default class extends Page {
     });
   }
 
+  ignore() {
+    const { data } = this.state;
+    Course.editAsk({ id: data.id, ignoreStatus: 1 }).then(() => {
+      asyncSMessage('操作成功');
+      goBack();
+    });
+  }
+
   renderBase() {
     const { data } = this.state;
     const { course = {} } = data;
@@ -151,7 +158,7 @@ export default class extends Page {
           this.orderQuestion(oldIndex, newIndex);
         }}
         renderItem={(item) => (
-          <List.Item actions={[<Icon type='bars' className='icon' />, <Typography.Text copyable={{ text: `${UserUrl}/course/ask?askId=${item.id}` }} />]}>
+          <List.Item actions={[<Icon type='bars' className='icon' />, <Typography.Text copyable={{ text: `${PcUrl}/course/ask?askId=${item.id}` }} />]}>
             <Row style={{ width: '100%' }}>
               <Col span={11}>问题:<span dangerouslySetInnerHTML={{ __html: item.content }} /></Col>
               <Col span={11} offset={1}>答复:<span dangerouslySetInnerHTML={{ __html: item.answer }} /></Col>
@@ -188,15 +195,6 @@ export default class extends Page {
               )}
             </Form.Item>
           </Col>
-          <Col span={12}>
-            <Form.Item labelCol={{ span: 12 }} wrapperCol={{ span: 10 }} label='是否忽略'>
-              {getFieldDecorator('ignoreStatus', {
-                valuePropName: 'checked',
-              })(
-                <Switch checkedChildren='on' unCheckedChildren='off' />,
-              )}
-            </Form.Item>
-          </Col>
         </Row>
       </Form>
     </Block>;
@@ -211,6 +209,11 @@ export default class extends Page {
 
       <Row type="flex" justify="center">
         <Col>
+          <Button type="drange" onClick={() => {
+            this.ignore();
+          }}>忽略问题</Button>
+        </Col>
+        <Col>
           <Button type="primary" onClick={() => {
             this.submit();
           }}>保存</Button>

+ 15 - 3
front/project/admin/routes/student/askQuestion/page.js

@@ -81,6 +81,10 @@ export default class extends Page {
       select: [],
       number: true,
       placeholder: '请输入',
+    }, {
+      key: 'time',
+      type: 'daterange',
+      name: '提问时间',
     }];
     this.columns = [{
       title: '板块',
@@ -111,12 +115,13 @@ export default class extends Page {
       },
     }, {
       title: '倒计时',
-      dataIndex: 'askTime',
+      sorter: true,
+      dataIndex: 'expireTime',
       render: (text, record) => {
         const end = new Date(record.answerTime) || new Date();
         const cost = (end.getTime() - new Date(record.createTime).getTime()) / 1000;
         if (text) {
-          if (text - cost > 0) return `${formatSeconds(text - cost)}/${formatSeconds(text)}`;
+          if (record.askTime - cost > 0) return `${formatSeconds(text - cost)}/${formatSeconds(text)}`;
           return `0/${formatSeconds(text)}`;
         }
         return '-';
@@ -126,6 +131,7 @@ export default class extends Page {
       dataIndex: 'manager.username',
     }, {
       title: '回答时间',
+      sorter: true,
       dataIndex: 'answerTime',
       render: (text) => {
         return text ? formatDate(text) : '';
@@ -172,7 +178,13 @@ export default class extends Page {
   }
 
   initData() {
-    Question.listAsk(Object.assign({ hasRecord: true }, this.state.search)).then(result => {
+    const { search } = this.state;
+    const data = Object.assign({ hasRecord: true }, search);
+    if (data.time) {
+      data.startTime = data.time[0] || '';
+      data.endTime = data.time[1] || '';
+    }
+    Question.listAsk(data).then(result => {
       this.setTableData(result.list, result.total);
     });
   }

+ 15 - 12
front/project/admin/routes/student/askQuestionDetail/page.js

@@ -8,7 +8,7 @@ import DragList from '@src/components/DragList';
 // import FileUpload from '@src/components/FileUpload';
 import { formatFormError, formatDate, getMap } from '@src/services/Tools';
 import { asyncSMessage } from '@src/services/AsyncTools';
-import { UserUrl, AskTarget, QuestionType, AskModule } from '../../../../Constant';
+import { PcUrl, AskTarget, QuestionType, AskModule } from '../../../../Constant';
 // import { User } from '../../../stores/user';
 import { Question } from '../../../stores/question';
 
@@ -72,7 +72,6 @@ export default class extends Page {
       if (!err) {
         const data = form.getFieldsValue();
         data.showStatus = data.showStatus ? 1 : 0;
-        data.ignoreStatus = data.ignoreStatus ? 1 : 0;
         data.other = this.state.data.others.map(row => row.id);
         Question.editAsk(data).then(() => {
           asyncSMessage('保存成功');
@@ -83,6 +82,14 @@ export default class extends Page {
     });
   }
 
+  ignore() {
+    const { data } = this.state;
+    Question.editAsk({ id: data.id, ignoreStatus: 1 }).then(() => {
+      asyncSMessage('操作成功');
+      goBack();
+    });
+  }
+
   renderBase() {
     const { data } = this.state;
     const { question = {}, questionNo = {} } = data;
@@ -147,7 +154,7 @@ export default class extends Page {
           this.orderQuestion(oldIndex, newIndex);
         }}
         renderItem={(item) => (
-          <List.Item actions={[<Icon type='bars' className='icon' />, <Typography.Text copyable={{ text: `${UserUrl}/paper/question/${this.state.data.userQuestionId}?askId=${item.id}` }} />]}>
+          <List.Item actions={[<Icon type='bars' className='icon' />, <Typography.Text copyable={{ text: `${PcUrl}/paper/question/${this.state.data.userQuestionId}?askId=${item.id}` }} />]}>
             <Row style={{ width: '100%' }}>
               <Col span={11}>问题:<span dangerouslySetInnerHTML={{ __html: item.content }} /></Col>
               <Col span={11} offset={1}>答复:<span dangerouslySetInnerHTML={{ __html: item.answer }} /></Col>
@@ -184,15 +191,6 @@ export default class extends Page {
               )}
             </Form.Item>
           </Col>
-          <Col span={12}>
-            <Form.Item labelCol={{ span: 12 }} wrapperCol={{ span: 10 }} label='是否忽略'>
-              {getFieldDecorator('ignoreStatus', {
-                valuePropName: 'checked',
-              })(
-                <Switch checkedChildren='on' unCheckedChildren='off' />,
-              )}
-            </Form.Item>
-          </Col>
         </Row>
       </Form>
     </Block>;
@@ -207,6 +205,11 @@ export default class extends Page {
 
       <Row type="flex" justify="center">
         <Col>
+          <Button type="drange" onClick={() => {
+            this.ignore();
+          }}>忽略问题</Button>
+        </Col>
+        <Col>
           <Button type="primary" onClick={() => {
             this.submit();
           }}>保存</Button>

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

@@ -9,7 +9,7 @@ import ActionLayout from '@src/layouts/ActionLayout';
 import TableLayout from '@src/layouts/TableLayout';
 import { formatDate, formatSecond, formatPercent } from '@src/services/Tools';
 import { asyncSMessage, asyncForm } from '@src/services/AsyncTools';
-import { UserUrl } from '../../../../Constant';
+import { PcUrl } from '../../../../Constant';
 import { Course } from '../../../stores/course';
 import { User } from '../../../stores/user';
 import { System } from '../../../stores/system';
@@ -43,7 +43,7 @@ export default class extends Page {
       render: (text, record) => {
         return <div className="table-button">
           {<a onClick={() => {
-            User.locationUser(record.userId, `${UserUrl}/my/report`);
+            User.locationUser(record.userId, `${PcUrl}/my/report`);
           }}>查看</a>}
         </div>;
       },
@@ -104,7 +104,7 @@ export default class extends Page {
       dataIndex: 'userPaper',
       render: (text, record) => {
         return text ? <a onClick={() => {
-          User.locationUser(record.userId, `${UserUrl}/paper/report/${record.reportId}`);
+          User.locationUser(record.userId, `${PcUrl}/paper/report/${record.reportId}`);
         }}>查看{text.times > 0 ? '(已完成)' : ''}</a> : '';
       },
     }, {

+ 1 - 3
front/project/admin/routes/subject/examination/page.js

@@ -117,6 +117,7 @@ export default class extends Page {
       },
     }, {
       title: '修改时间',
+      sorter: true,
       dataIndex: 'updateTime',
       render: (text, record) => {
         return formatDate(record.question.updateTime);
@@ -185,9 +186,6 @@ export default class extends Page {
         itemList={filterForm}
         data={this.state.search}
         onChange={data => {
-          // if (data.time.length > 0) {
-          //   data.time = [data.time[0].format('YYYY-MM-DD HH:mm:ss'), data.time[1].format('YYYY-MM-DD HH:mm:ss')];
-          // }
           this.search(data);
         }} />}
       <ActionLayout

+ 4 - 2
front/project/admin/routes/subject/exercise/page.js

@@ -141,16 +141,18 @@ export default class extends Page {
       },
     }, {
       title: '序号',
+      sorter: this.state.search.paperId,
       dataIndex: 'no',
       render: (text, record) => {
         const { search } = this.state;
-        if (search.paper_id) {
-          return record.paper.no;
+        if (search.paperId) {
+          return record.no;
         }
         return '--';
       },
     }, {
       title: '修改时间',
+      sorter: true,
       dataIndex: 'updateTime',
       render: (text, record) => {
         return formatDate(record.question.updateTime);

+ 2 - 2
front/project/admin/routes/subject/question/page.js

@@ -340,7 +340,7 @@ export default class extends Page {
 
   searchStem() {
     const { form } = this.props;
-    const content = form.getFieldValue('stem').replace(/<[^>]+>/g, '');
+    const content = (form.getFieldValue('stem') || '').replace(/<[^>]+>/g, '');
     Question.searchStem({ content })
       .then(result => {
         if (result.list.length > 0) {
@@ -352,7 +352,7 @@ export default class extends Page {
               return Promise.resolve();
             });
         } else {
-          asyncSMessage('无可匹配题目');
+          asyncSMessage('无可匹配题目', 'warn');
         }
       });
   }

+ 4 - 8
front/project/admin/routes/subject/textbook/page.js

@@ -18,7 +18,7 @@ const TextbookTypeMap = getMap(TextbookType, 'value', 'label');
 
 const filterForm = [
   {
-    key: 'type',
+    key: 'questionType',
     type: 'select',
     allowClear: true,
     name: '题型',
@@ -60,16 +60,13 @@ export default class extends Page {
     this.categoryMap = {};
     this.columns = [{
       title: '题型',
-      dataIndex: 'type',
+      dataIndex: 'questionType',
       render: (text, record) => {
-        return TextbookTypeMap[record.question.type] || text;
+        return TextbookTypeMap[record.question.questionType] || text;
       },
     }, {
       title: '练习册',
-      dataIndex: 'paper',
-      render: (text) => {
-        return (text || {}).text.title;
-      },
+      dataIndex: 'paper.title',
     }, {
       title: '题目ID',
       dataIndex: 'title',
@@ -134,7 +131,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}

+ 8 - 2
front/project/admin/routes/user/abnormal/page.js

@@ -24,7 +24,7 @@ export default class extends Page {
       key: 'totalAlert',
       type: 'number',
       allowClear: true,
-      name: '实名认证',
+      name: '警告次数',
       number: true,
     }, {
       key: 'time',
@@ -108,7 +108,13 @@ export default class extends Page {
   }
 
   initData() {
-    User.listAbnormal(this.state.search).then(result => {
+    const { search } = this.state;
+    const data = Object.assign({}, search);
+    if (data.time) {
+      data.startTime = data.time[0] || '';
+      data.endTime = data.time[1] || '';
+    }
+    User.listAbnormal(data).then(result => {
       this.setTableData(result.list, result.total);
     });
   }

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

@@ -6,7 +6,7 @@ import Block from '@src/components/Block';
 import TableLayout from '@src/layouts/TableLayout';
 import { formatDate, getMap, formatMoney, formatSeconds } from '@src/services/Tools';
 import { asyncSMessage, asyncForm, asyncDelConfirm } from '@src/services/AsyncTools';
-import { UserUrl, PrepareStatus, PrepareExaminationTime, ServiceKey } from '../../../../Constant';
+import { PcUrl, PrepareStatus, PrepareExaminationTime, ServiceKey } from '../../../../Constant';
 import { User } from '../../../stores/user';
 import { System } from '../../../stores/system';
 import { Exercise } from '../../../stores/exercise';
@@ -97,7 +97,7 @@ export default class extends Page {
 
   noFrozenAction() {
     asyncDelConfirm('操作确认', '是否要取消封禁账户?取消后账户可以使用网站', () => {
-      return User.frozen({ id: this.state.data.id })
+      return User.noFrozen({ id: this.state.data.id })
         .then(() => {
           asyncSMessage('操作成功!');
           this.refresh();
@@ -158,7 +158,7 @@ export default class extends Page {
             </Col>
             <Col span={12}>
               <Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 16 }} label='注册时间'>
-                {formatDate(data.createTime)}
+                {data.createTime && formatDate(data.createTime)}
               </Form.Item>
             </Col>
             <Col span={12}>
@@ -233,7 +233,7 @@ export default class extends Page {
       <h1>服务开通</h1>
       <div className="group">
         <h2>累计消费金额</h2>
-        <span>{data.totalMoney}</span>
+        <p>{data.totalMoney}</p>
         <Button onClick={() => {
           this.addMoneyAction();
         }}>增加线下金额</Button>
@@ -268,7 +268,7 @@ export default class extends Page {
       <div className="group">
         <h2>学习数据</h2>
         <Button onClick={() => {
-          User.locationUser(this.params.id, `${UserUrl}/my/data`);
+          User.locationUser(this.params.id, `${PcUrl}/my/data`);
         }}>查看</Button>
       </div>
     </Block>;

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

@@ -43,6 +43,7 @@ export default class extends Page {
       required: true,
       option: {
         normalize: (value) => {
+          if (!value) return value;
           if (this.mobile === value) return value;
           if (this.timeout) {
             clearTimeout(this.timeout);
@@ -112,6 +113,7 @@ export default class extends Page {
       dataIndex: 'nickname',
     }, {
       title: '注册时间',
+      sorter: true,
       dataIndex: 'createTime',
       render: (text) => {
         return formatDate(text, 'YYYY-MM-DD');

+ 3 - 2
front/project/admin/routes/user/order/page.js

@@ -21,7 +21,8 @@ export default class extends Page {
     }, {
       key: 'transactionNo',
       type: 'input',
-      name: '请输入支付流水号',
+      required: true,
+      name: '支付流水号',
     }];
     this.filterF = null;
     this.filterForm = [{
@@ -94,7 +95,7 @@ export default class extends Page {
           <Link to={`/user/order/detail/${record.id}`}>查看</Link>
           {record.payStatus === 0 && (
             <a onClick={() => {
-              this.finishActionn(record);
+              this.finishAction(record);
             }}>确认收款</a>
           )}
         </div>;

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

@@ -43,13 +43,16 @@ export default class extends Page {
       type: 'select',
       name: '国际码',
       select: MobileArea,
+      required: true,
     }, {
       key: 'mobile',
       type: 'input',
       name: '手机号',
       placeholder: '请输入',
+      required: true,
       option: {
         normalize: (value) => {
+          if (!value) return value;
           if (this.mobile === value) return value;
           if (this.timeout) {
             clearTimeout(this.timeout);
@@ -71,6 +74,7 @@ export default class extends Page {
       name: '开通服务',
       select: ServiceKey,
       placeholder: '请选择',
+      required: true,
       onChange: (value) => {
         this.serviceList[4].select = ServiceParamList[value] || [];
         this.serviceList[4].disabled = !this.serviceList[4].select.length;
@@ -90,13 +94,16 @@ export default class extends Page {
       type: 'select',
       name: '国际码',
       select: MobileArea,
+      required: true,
     }, {
       key: 'mobile',
       type: 'input',
       name: '手机号',
       placeholder: '请输入',
+      required: true,
       option: {
         normalize: (value) => {
+          if (!value) return value;
           if (this.mobile === value) return value;
           if (this.timeout) {
             clearTimeout(this.timeout);
@@ -116,6 +123,7 @@ export default class extends Page {
       key: 'dataId',
       type: 'select',
       name: '开通资料',
+      required: true,
       select: [],
       placeholder: '请选择',
     }];
@@ -212,7 +220,7 @@ export default class extends Page {
       dataIndex: 'handler',
       render: (text, record) => {
         return <div className="table-button">
-          {record.isUsed > 0 && !record.isStop && (
+          {!record.isStop && (
             <a onClick={() => {
               this.stopAction(record.id);
             }}>停用</a>

+ 8 - 0
front/project/admin/stores/system.js

@@ -53,6 +53,14 @@ export default class SystemStore extends BaseStore {
     return this.apiPut('/setting/filter_time', params);
   }
 
+  getExercisePaperAuto() {
+    return this.apiGet('/setting/exercise_paper_auto');
+  }
+
+  setExercisePaperAuto(params) {
+    return this.apiPut('/setting/exercise_paper_auto', params);
+  }
+
   getExerciseTime() {
     return this.apiGet('/setting/exercise_time');
   }

+ 6 - 3
front/project/h5/components/Block/index.js

@@ -171,14 +171,17 @@ export class BuyBlock extends Component {
           {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>}
+          {data.service === 'textbook' && expire && <div className="desc">您可至PC端「我的工具」查看往期机经</div>}
         </div>
         <div className="block-right">
           <div className="btn">
-            {!data.isUsed && <Button radius onClick={() => onOpen && onOpen(data)}>开通</Button>}
-            {expire && data.service === 'vip' && <Button radius onClick={() => onBuy && onBuy(data)}>立即购买</Button>}
+            {data.service === 'textbook' && !data.isUsed && <Button radius onClick={() => onOpen && onOpen(data)}>开通</Button>}
+            {expire && data.service !== 'vip' && <Button radius onClick={() => onBuy && onBuy(data)}>续费</Button>}
+            {data.service === 'vip' && <Button radius onClick={() => onBuy && onBuy(data)}>续费</Button>}
             {data.productType === 'data' && data.data.resource && <Button radius onClick={() => onRead && onRead(data)}>阅读</Button>}
+            {!expire && data.service === 'textbook' && <Button radius onClick={() => onRead && onRead(data)}>阅读</Button>}
           </div>
-          {expire && data.service === 'vip' && <div className="tip">¥{price}/{title}</div>}
+          {data.service === 'vip' && <div className="tip">¥{price}/{title}</div>}
         </div>
       </TopBlock >
     );

+ 6 - 5
front/project/h5/components/Item/index.js

@@ -1,14 +1,15 @@
 import React, { Component } from 'react';
 import './index.less';
 import Assets from '@src/components/Assets';
+import { formatDate } from '@src/services/Tools';
 
 export class FAQItem extends Component {
   render() {
     const { className = '', data = {} } = this.props;
     return (
       <div className={`g-faq-item ${className}`}>
-        <div className="g-faq-item-title">{data.title}</div>
-        <div className="g-faq-item-desc">{data.desc}</div>
+        <div className="g-faq-item-title">{data.content}</div>
+        <div className="g-faq-item-desc">答:{data.answer}</div>
       </div>
     );
   }
@@ -23,10 +24,10 @@ export class CommentItem extends Component {
         </div>
         <div className="g-comment-item-right">
           <div className="g-comment-item-right-info">
-            <div className="g-comment-item-right-info-name">{data.name}</div>
-            <div className="g-comment-item-right-info-date">{data.date}</div>
+            <div className="g-comment-item-right-info-name">{data.nickname}</div>
+            <div className="g-comment-item-right-info-date">{formatDate(data.createTime, 'YYYY-MM-DD')}</div>
           </div>
-          <div className="g-comment-item-right-desc">{data.desc}</div>
+          <div className="g-comment-item-right-desc">{data.content}</div>
         </div>
       </div>
     );

+ 1 - 1
front/project/h5/routes/page/bind/page.js

@@ -76,7 +76,7 @@ export default class extends Page {
     if (!area || !mobile || !mobileVerifyCode) return;
     if (needEmail && !email) return;
     User.bind(area, mobile, mobileVerifyCode, email).then(() => {
-      linkTo('/');
+      linkTo('/product');
     })
       .catch(err => {
         if (err.message.indexOf('验证码') >= 0) {

+ 2 - 2
front/project/h5/routes/page/home/page.js

@@ -2,7 +2,7 @@ import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
 import Assets from '@src/components/Assets';
-import { UserUrl } from '../../../../Constant';
+import { PcUrl } from '../../../../Constant';
 import { Main } from '../../../stores/main';
 
 export default class extends Page {
@@ -24,7 +24,7 @@ export default class extends Page {
           <div className="copy-title">复制下方地址至浏览器打开</div>
           <div className="input">
             <div className="prefix">http://</div>
-            <div className="value">{UserUrl.replace('http://', '')}/id/{info.inviteCode}</div>
+            <div className="value">{PcUrl.replace('http://', '')}/id/{info.inviteCode}</div>
             <Assets name="copy" />
           </div>
         </div>

+ 1 - 1
front/project/h5/routes/page/id/page.js

@@ -7,7 +7,7 @@ export default class extends Page {
   init() {
     const { id } = this.params;
     User.originInviteCode(id);
-    replaceLink('/');
+    replaceLink('/bind');
   }
 
   renderView() {

+ 5 - 1
front/project/h5/routes/product/bought/page.js

@@ -61,7 +61,11 @@ export default class extends Page {
     }} onBuy={() => {
       linkTo(`/product/service/${rowData.service}`);
     }} onRead={() => {
-      openLink(rowData.data.resource);
+      if (rowData.service === 'textbook') {
+        linkTo('/textbook');
+      } else {
+        openLink(rowData.data.resource);
+      }
     }} />;
   }
 

+ 1 - 0
front/project/h5/routes/product/courseDetail/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-course-detail',
   title: '课程详情',
   needLogin: false,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 25 - 3
front/project/h5/routes/product/courseDetail/page.js

@@ -4,9 +4,10 @@ import { Tabs } from 'antd-mobile';
 import Page from '@src/containers/Page';
 import Money from '../../../components/Money';
 import Button from '../../../components/Button';
+import { FAQItem, CommentItem } from '../../../components/Item';
 import { Course } from '../../../stores/course';
 import { Order } from '../../../stores/order';
-import { UserUrl } from '../../../../Constant';
+import { PcUrl } from '../../../../Constant';
 
 export default class extends Page {
   initState() {
@@ -24,6 +25,27 @@ export default class extends Page {
     Order.speedPay({ productType: 'course', productId: this.params.id });
   }
 
+  renderText() {
+    const { tab, data } = this.state;
+    let content;
+    switch (tab) {
+      case 'teacherContent':
+      case 'baseContent':
+      case 'pointContent':
+        content = <div dangerouslySetInnerHTML={{ __html: data[tab] }} />;
+        break;
+      case 'faq':
+        content = <div>{(data.faqs || []).map(row => <FAQItem data={row} />)}</div>;
+        break;
+      case 'comment':
+        content = <div>{(data.comments || []).map(row => <CommentItem data={row} />)}</div>;
+        break;
+      default:
+        break;
+    }
+    return content;
+  }
+
   renderView() {
     const { data = {}, tab } = this.state;
     return (
@@ -31,7 +53,7 @@ export default class extends Page {
         <div className="b-g" style={{ backgroundImage: `url(${data.cover})` }}>
           <div className="title">{data.title}</div>
         </div>
-        <div className="tip">访问{UserUrl}/course/detail/{data.id},试听该课程</div>
+        <div className="tip">访问{PcUrl}/course/detail/{data.id},试听该课程</div>
         <div className="detail">
           <Tabs
             page={tab}
@@ -46,7 +68,7 @@ export default class extends Page {
               this.setState({ tab: value.key });
             }}
           />
-          <div dangerouslySetInnerHTML={{ __html: data[tab] }} />
+          {this.renderText()}
         </div>
         <div className="fixed">
           <div className="fee">

+ 1 - 0
front/project/h5/routes/product/coursePackage/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-course-package',
   title: '课程套餐',
   needLogin: false,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 0
front/project/h5/routes/product/courseVideo/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-course-video',
   title: '在线课程',
   needLogin: false,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 0
front/project/h5/routes/product/courseVs/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-course-vs',
   title: '1vs1课程',
   needLogin: false,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 25 - 3
front/project/h5/routes/product/courseVs/page.js

@@ -4,6 +4,7 @@ import { Tabs } from 'antd-mobile';
 import Page from '@src/containers/Page';
 import Money from '../../../components/Money';
 import Button from '../../../components/Button';
+import { FAQItem, CommentItem } from '../../../components/Item';
 import { Course } from '../../../stores/course';
 import { Order } from '../../../stores/order';
 
@@ -23,6 +24,27 @@ export default class extends Page {
     Order.speedPay({ productType: 'course', productId: this.params.id, number: 1 });
   }
 
+  renderText() {
+    const { tab, data } = this.state;
+    let content;
+    switch (tab) {
+      case 'serviceContent':
+      case 'crowdContent':
+      case 'processContent':
+        content = <div dangerouslySetInnerHTML={{ __html: data[tab] }} />;
+        break;
+      case 'faq':
+        content = <div>{(data.faqs || []).map(row => <FAQItem data={row} />)}</div>;
+        break;
+      case 'comment':
+        content = <div>{(data.comments || []).map(row => <CommentItem data={row} />)}</div>;
+        break;
+      default:
+        break;
+    }
+    return content;
+  }
+
   renderView() {
     const { data = {}, tab } = this.state;
     return (
@@ -30,13 +52,13 @@ export default class extends Page {
         <div className="b-g" style={{ backgroundImage: `url(${data.cover})` }}>
           <div className="title">{data.title}</div>
         </div>
-        {/* <div className="tip">访问{UserUrl}/course/detail/{data.id},试听该课程</div> */}
+        {/* <div className="tip">访问{PcUrl}/course/detail/{data.id},试听该课程</div> */}
         <div className="detail">
           <Tabs
             page={tab}
             tabs={[
               { title: '服务介绍', key: 'serviceContent' },
-              { title: '适合人群', key: 'crowdContent' },
+              { title: '适合人群', key: 'crowdContent' },
               { title: '授课流程', key: 'processContent' },
               { title: 'FAQs', key: 'faq' },
               { title: '用户评价', key: 'comment' },
@@ -45,7 +67,7 @@ export default class extends Page {
               this.setState({ tab: value.key });
             }}
           />
-          <div dangerouslySetInnerHTML={{ __html: data[tab] }} />
+          {this.renderText()}
         </div>
         <div className="fixed">
           <div className="fee">

+ 1 - 0
front/project/h5/routes/product/data/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-data',
   title: '全部资料',
   needLogin: false,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 0
front/project/h5/routes/product/dataDetail/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-data-detail',
   title: '资料详情',
   needLogin: true,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 24 - 7
front/project/h5/routes/product/dataDetail/page.js

@@ -30,6 +30,27 @@ export default class extends Page {
     Order.speedPay({ productType: 'data', productId: this.params.id });
   }
 
+  renderText() {
+    const { tab, data } = this.state;
+    let content;
+    switch (tab) {
+      case 'content':
+      case 'authorContent':
+      case 'methodContent':
+        content = <div dangerouslySetInnerHTML={{ __html: data[tab] }} />;
+        break;
+      case 'faq':
+        content = <div>{(data.faqs || []).map(row => <FAQItem data={row} />)}</div>;
+        break;
+      case 'comment':
+        content = <div>{(data.comments || []).map(row => <CommentItem data={row} />)}</div>;
+        break;
+      default:
+        break;
+    }
+    return content;
+  }
+
   renderView() {
     const { data = {}, tab } = this.state;
     return (
@@ -53,8 +74,8 @@ export default class extends Page {
           page={tab}
           tabs={[
             { title: '资料介绍', key: 'content' },
-            { title: '作者介绍', key: 'author_content' },
-            { title: '获取方式', key: 'method_content' },
+            { title: '作者介绍', key: 'authorContent' },
+            { title: '获取方式', key: 'methodContent' },
             { title: 'FAQs', key: 'faq' },
             { title: '用户评价', key: 'comment' },
           ]}
@@ -62,11 +83,7 @@ export default class extends Page {
             this.setState({ tab: value.key });
           }}
         />
-        <div dangerouslySetInnerHTML={{ __html: data[tab] }} />
-        <FAQItem data={{ title: '123', desc: '12312312321321' }} />
-        <FAQItem data={{ title: '123', desc: '12312312321321' }} />
-        <CommentItem data={{ name: '123', desc: '12312312321', date: '1231231231' }} />
-        <CommentItem data={{ name: '123', desc: '12312312321', date: '1231231231' }} />
+        {this.renderText()}
         <div className="fixed">
           <div className="fee">
             总额: <Money value={data.price} size="lager" />

+ 1 - 0
front/project/h5/routes/product/dataHistory/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-data-history',
   title: '资料更新',
   needLogin: true,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 0
front/project/h5/routes/product/main/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product',
   title: '全部商品',
   needLogin: false,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 1
front/project/h5/routes/product/main/index.less

@@ -18,7 +18,7 @@
 
   .list {
     padding: 0px 15px 20px 15px;
-    overflow: hidden;
+    overflow: auto;
     flex: 1;
 
     .body {

+ 2 - 2
front/project/h5/routes/product/main/page.js

@@ -19,7 +19,7 @@ export default class extends Page {
   initData() {
     Promise.all(ServiceKey.map(service => {
       return Main.getService(service.value).then(result => {
-        this.setState({ [service]: result });
+        this.setState({ [service.value]: result });
       });
     }));
     Course.allVs().then(list => {
@@ -72,7 +72,7 @@ export default class extends Page {
         <Assets name="banner" className="banner" />
         {CourseVsType.map((t, index) => {
           const course = this.courseVsMap[t.value] || {};
-          return <LinkBlock title={course.title} sub={course.comment} theme={index % 2 > 0 ? 'not' : ''} onClick={() => {
+          return <LinkBlock title={course.title} sub={course.comment} theme={index % 2 > 0 ? 'not' : 'default'} onClick={() => {
             linkTo(`/product/course/vs/${course.id}`);
           }} />;
         })}

+ 1 - 0
front/project/h5/routes/product/serviceDetail/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'product-service-detail',
   title: '服务详情',
   needLogin: true,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 0
front/project/h5/routes/textbook/detail/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'textbook-detail',
   title: '机经详情',
   needLogin: true,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 0
front/project/h5/routes/textbook/library/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'textbook-library',
   title: '换库表',
   needLogin: false,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 1 - 0
front/project/h5/routes/textbook/main/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'textbook',
   title: '机经主页',
   needLogin: true,
+  repeat: true,
   component() {
     return import('./page');
   },

+ 5 - 1
front/project/h5/stores/course.js

@@ -12,10 +12,14 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/video/list', params);
   }
 
-  get(courseId) {
+  simple(courseId) {
     return this.apiGet('/course/simple', { courseId });
   }
 
+  get(courseId) {
+    return this.apiGet('/course/detail', { courseId });
+  }
+
   listPackage(params) {
     return this.apiGet('/course/package/list', params);
   }

BIN
front/project/www/assets/up - 副本.png


+ 1 - 1
front/project/www/components/AnswerList/index.js

@@ -6,7 +6,7 @@ function getKey(type, index, selected, answer) {
   return `${(selected[type] || [])[index] ? 'selected' : ''} ${(answer[type] || [])[index] ? 'true' : 'false'}`;
 }
 function getDoubleKey(type, index, selected, answer, position) {
-  return `${(selected[type] || [])[index][position] ? 'selected' : ''} ${(answer[type] || [])[index][position] ? 'true' : 'false'}`;
+  return `${((selected[type] || [])[index] || [])[position] ? 'selected' : ''} ${((answer[type] || [])[index] || [])[position] ? 'true' : 'false'}`;
 }
 
 export default class AnswerList extends Component {

+ 1 - 1
front/project/www/components/AnswerTable/index.less

@@ -7,7 +7,7 @@
     text-align: right;
     margin-bottom: 10px;
 
-    .select {
+    .select-warp {
       text-align: right;
     }
   }

+ 2 - 2
front/project/www/components/Icon/index.js

@@ -2,10 +2,10 @@ import React from 'react';
 import './index.less';
 
 function GIcon(props) {
-  const { active, name, noHover, onClick, children } = props;
+  const { className, active, name, noHover, onClick, children } = props;
   return (
     <div
-      className={`icon ${name} ${active ? 'active' : ''} ${noHover ? 'no' : ''}`}
+      className={`icon ${className || ''} ${name} ${active ? 'active' : ''} ${noHover ? 'no' : ''}`}
       onClick={() => onClick && onClick()}
     >
       {children}

+ 240 - 0
front/project/www/components/Icon/index.less

@@ -76,6 +76,29 @@
   background-image: url('/assets/analysis_drop_down_highlight.png');
 }
 
+.icon.small-up {
+  width: 24px;
+  height: 24px;
+  background: url('/assets/dropdown_normal.png') no-repeat center;
+  transform: rotate(180deg)
+}
+
+.icon.small-up.active,
+.icon.small-up:hover {
+  background-image: url('/assets/dropdown_hover.png');
+}
+
+.icon.small-down {
+  width: 24px;
+  height: 24px;
+  background: url('/assets/dropdown_normal.png') no-repeat center;
+}
+
+.icon.small-down.active,
+.icon.small-down:hover {
+  background-image: url('/assets/dropdown_hover.png');
+}
+
 .icon.more {
   width: 16px;
   height: 16px;
@@ -109,6 +132,7 @@
 }
 
 .icon.star.active,
+.icon.star.no.active,
 .icon.star:hover {
   background-image: url('/assets/header_star_select.png');
 }
@@ -169,6 +193,29 @@
   background: url('/assets/option_right.png') no-repeat center;
 }
 
+.icon.arrow-right-small {
+  width: 20px;
+  height: 20px;
+  background: url('/assets/more3_normal.png') no-repeat center;
+}
+
+.icon.arrow-right-small.active,
+.icon.arrow-right-small:hover {
+  background-image: url('/assets/more3_hover.png');
+}
+
+.icon.arrow-left-small {
+  width: 20px;
+  height: 20px;
+  background: url('/assets/more3_normal.png') no-repeat center;
+  transform: rotate(180deg);
+}
+
+.icon.arrow-left-small.active,
+.icon.arrow-left-small:hover {
+  background-image: url('/assets/more3_hover.png');
+}
+
 .icon.arrow-up {
   width: 20px;
   height: 20px;
@@ -202,6 +249,199 @@
 }
 
 .icon.note.active,
+.icon.note.no.active,
 .icon.note:hover {
   background-image: url('/assets/note_highlight.png');
+}
+
+.icon.speed-block {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/speed_block.png') no-repeat center;
+}
+
+.icon.speed-block.no:hover {
+  background-image: url('/assets/speed_block.png');
+}
+
+.icon.speed-block.active,
+.icon.speed-block.no.active,
+.icon.speed-block:hover {
+  background-image: url('/assets/speed_normal.png');
+}
+
+.icon.question-block {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/question_block.png') no-repeat center;
+}
+
+.icon.question-block.no:hover {
+  background-image: url('/assets/question_block.png');
+}
+
+.icon.question-block.active,
+.icon.question-block.no.active,
+.icon.question-block:hover {
+  background-image: url('/assets/question_normal.png');
+}
+
+.icon.clockin-block {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/clockin_block.png') no-repeat center;
+}
+
+.icon.clockin-block.no:hover {
+  background-image: url('/assets/clockin_block.png');
+}
+
+.icon.clockin-block.active,
+.icon.clockin-block.no.active,
+.icon.clockin-block:hover {
+  background-image: url('/assets/clockin_normal.png');
+}
+
+.icon.note-block {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/note_block_normal.png') no-repeat center;
+}
+
+.icon.note-block.no:hover {
+  background-image: url('/assets/note_block_normal.png');
+}
+
+.icon.note-block.active,
+.icon.note-block.no.active,
+.icon.note-block:hover {
+  background-image: url('/assets/note_block_highlight.png');
+}
+
+.icon.gift-block {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/gift2.png') no-repeat center;
+}
+
+.icon.gift-block.no:hover {
+  background-image: url('/assets/gift2.png');
+}
+
+.icon.gift-block.active,
+.icon.gift-block.no.active,
+.icon.gift-block:hover {
+  background-image: url('/assets/gift2.png');
+}
+
+.icon.time-icon {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/time_gray.png') no-repeat center;
+}
+
+.icon.time-icon.no:hover {
+  background-image: url('/assets/time_gray.png');
+}
+
+.icon.time-icon.active,
+.icon.time-icon.no.active,
+.icon.time-icon:hover {
+  background-image: url('/assets/time_normal.png');
+}
+
+.icon.QA-icon {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/QA_gary.png') no-repeat center;
+}
+
+.icon.QA-icon.no:hover {
+  background-image: url('/assets/QA_gary.png');
+}
+
+.icon.QA-icon.active,
+.icon.QA-icon.no.active,
+.icon.QA-icon:hover {
+  background-image: url('/assets/QA_normal.png');
+}
+
+.icon.class-icon {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/class_gray.png') no-repeat center;
+}
+
+.icon.class-icon.no:hover {
+  background-image: url('/assets/class_gray.png');
+}
+
+.icon.class-icon.active,
+.icon.class-icon.no.active,
+.icon.class-icon:hover {
+  background-image: url('/assets/class_normal.png');
+}
+
+.icon.note-icon {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/note_gray.png') no-repeat center;
+}
+
+.icon.note-icon.no:hover {
+  background-image: url('/assets/note_gray.png');
+}
+
+.icon.note-icon.active,
+.icon.note-icon.no.active,
+.icon.note-icon:hover {
+  background-image: url('/assets/page_normal.png');
+}
+
+.icon.supplement-icon {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/supplement_gray.png') no-repeat center;
+}
+
+.icon.supplement-icon.no:hover {
+  background-image: url('/assets/supplement_gray.png');
+}
+
+.icon.supplement-icon.active,
+.icon.supplement-icon.no.active,
+.icon.supplement-icon:hover {
+  background-image: url('/assets/supplement_normal.png');
+}
+
+.icon.information-icon {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/information_gray.png') no-repeat center;
+}
+
+.icon.information-icon.no:hover {
+  background-image: url('/assets/information_gray.png');
+}
+
+.icon.information-icon.active,
+.icon.information-icon.no.active,
+.icon.information-icon:hover {
+  background-image: url('/assets/information_normal.png');
+}
+
+.icon.homework-icon {
+  width: 40px;
+  height: 40px;
+  background: url('/assets/homework_gray.png') no-repeat center;
+}
+
+.icon.homework-icon.no:hover {
+  background-image: url('/assets/homework_gray.png');
+}
+
+.icon.homework-icon.active,
+.icon.homework-icon.no.active,
+.icon.homework-icon:hover {
+  background-image: url('/assets/homework_normal.png');
 }

+ 3 - 7
front/project/www/components/Login/index.js

@@ -7,7 +7,7 @@ import { Icon as GIcon } from '../Icon';
 import { Button as GButton } from '../Button';
 import { User } from '../../stores/user';
 import { Common } from '../../stores/common';
-import { MobileArea, WechatUserAppId } from '../../../Constant';
+import { MobileArea, WechatPcAppId } from '../../../Constant';
 
 const LOGIN_PHONE = 'LOGIN_PHONE';
 const LOGIN_WX = 'LOGIN_WX';
@@ -267,9 +267,7 @@ export default class Login extends Component {
         <div className="qr-code">
           <iframe
             frameBorder="0"
-            src={`/login.html?appid=${WechatUserAppId}&redirectUri=${encodeURIComponent(
-              'http://www.duoshaojiaoyu.com',
-            )}`}
+            src={`/login.html?appid=${WechatPcAppId}&redirectUri=${encodeURIComponent('http://www.duoshaojiaoyu.com')}`}
             width="300"
             height="300"
           />
@@ -358,9 +356,7 @@ export default class Login extends Component {
         <div className="qr-code">
           <iframe
             frameBorder="0"
-            src={`/login.html?appid=${WechatUserAppId}&redirectUri=${encodeURIComponent(
-              'http://www.duoshaojiaoyu.com',
-            )}`}
+            src={`/login.html?appid=${WechatPcAppId}&redirectUri=${encodeURIComponent('http://www.duoshaojiaoyu.com')}`}
             width="300"
             height="300"
           />

+ 7 - 7
front/project/www/components/UserAction/index.js

@@ -1,10 +1,10 @@
 import React, { Component } from 'react';
 import './index.less';
 import { Icon } from 'antd';
+import Assets from '@src/components/Assets';
 import Select from '../Select';
 import CheckboxItem from '../CheckboxItem';
 import { Button } from '../Button';
-import { Icon as GIcon } from '../Icon';
 
 export default class UserAction extends Component {
   constructor(props) {
@@ -64,8 +64,9 @@ export default class UserAction extends Component {
         <div hidden={btnList.length === 0} className="button-list">
           {btnList.map(btn => {
             return (
-              <Button radius size="small" onClick={() => this.onAction(btn.key)}>
-                {btn.title}
+              <Button disabled={btn.disabled} radius size="small" onClick={() => this.onAction(btn.key)}>
+                {btn.title}{' '}
+                {btn.tag === 'vip' && <Assets name={btn.disabled ? 'VIP_small_gray' : 'VIP_small_yellow'} />}
               </Button>
             );
           })}
@@ -90,13 +91,12 @@ export default class UserAction extends Component {
               <div className={`item sort-item ${item.right ? 'right' : ''}`}>
                 {item.label}
                 {sortMap[item.key] ? (
-                  <GIcon
-                    active
-                    name={`arrow-${sortMap[item.key] === 'asc' ? 'up' : 'down'}`}
+                  <Assets
+                    name={sortMap[item.key] === 'asc' ? 'seqencing2_up_select' : 'seqencing2_down_select'}
                     onClick={() => this.onSort(item.key, sortMap[item.key] === 'asc' ? 'desc' : '')}
                   />
                 ) : (
-                  <GIcon name="arrow-up" onClick={() => this.onSort(item.key, 'asc')} />
+                  <Assets name="seqencing2_normal" onClick={() => this.onSort(item.key, 'asc')} />
                 )}
               </div>
             );

+ 17 - 0
front/project/www/components/UserAction/index.less

@@ -22,6 +22,17 @@
 
     .button {
       margin-right: 10px;
+      position: relative;
+
+      .assets {
+        position: absolute;
+        top: -4px;
+        left: -4px;
+      }
+    }
+
+    .button.disabled {
+      background: #8DABCE;
     }
   }
 
@@ -60,6 +71,12 @@
     color: #686872;
     font-size: 12px;
     margin-left: 15px;
+
+    .assets {
+      vertical-align: top;
+      margin-top: 14px;
+      margin-left: 3px;
+    }
   }
 
   .item.selct-item {

+ 8 - 8
front/project/www/components/UserTable/index.js

@@ -1,5 +1,6 @@
 import React, { Component } from 'react';
 import './index.less';
+import Assets from '@src/components/Assets';
 import CheckboxItem from '../CheckboxItem';
 import Icon from '../Icon';
 
@@ -52,7 +53,7 @@ export default class UserTable extends Component {
                 {columns.map((item, i) => {
                   return (
                     <th
-                      className={`${item.className} ${i === 0 && select ? 'select' : ''}`}
+                      className={`${item.className || ''} ${i === 0 && select ? 'check' : ''}`}
                       width={item.width}
                       align={item.align}
                     >
@@ -65,13 +66,12 @@ export default class UserTable extends Component {
                         ))}
                       {item.sort &&
                         (sortMap[item.key] ? (
-                          <Icon
-                            active
-                            name={`arrow-${sortMap[item.key] === 'asc' ? 'up' : 'down'}`}
+                          <Assets
+                            name={sortMap[item.key] === 'asc' ? 'seqencing2_up_select' : 'seqencing2_down_select'}
                             onClick={() => this.onSort(item.key, sortMap[item.key] === 'asc' ? 'desc' : '')}
                           />
                         ) : (
-                          <Icon name="arrow-up" onClick={() => this.onSort(item.key, 'asc')} />
+                          <Assets name="seqencing2_normal" onClick={() => this.onSort(item.key, 'asc')} />
                         ))}
                     </th>
                   );
@@ -88,7 +88,7 @@ export default class UserTable extends Component {
                   {columns.map((item, i) => {
                     return (
                       <td
-                        className={`${item.className} ${i === 0 && select ? 'select' : ''}`}
+                        className={`${item.className || ''} ${i === 0 && select ? 'check' : ''}`}
                         width={item.width}
                         align={item.align}
                       >
@@ -111,11 +111,11 @@ export default class UserTable extends Component {
         {data.length === 0 && <div className="empty">暂无数据</div>}
         {total && data.length > 0 && (
           <div className="page">
-            <Icon name="prev" onClick={() => this.onChangePage(current - 1)} />
+            <Icon name="arrow-left-small" onClick={() => this.onChangePage(current - 1)} />
             <span>
               <b>{current}</b>/{total}
             </span>
-            <Icon name="next" onClick={() => this.onChangePage(current + 1)} />
+            <Icon name="arrow-right-small" onClick={() => this.onChangePage(current + 1)} />
           </div>
         )}
       </div>

+ 10 - 2
front/project/www/components/UserTable/index.less

@@ -6,7 +6,7 @@
     margin-bottom: 10px;
     line-height: 20px;
 
-    .select {
+    .check {
       padding-left: 15px;
       text-align: left;
 
@@ -20,9 +20,16 @@
       padding: 0 20px;
       background: #ECEDEE;
       height: 60px;
+
+      .assets {
+        vertical-align: top;
+        margin-left: 3px;
+        margin-top: 3px;
+      }
     }
 
-    th.select {
+    th.check {
+      padding: 0 20px;
       padding-left: 40px;
     }
 
@@ -39,6 +46,7 @@
   }
 
   .page {
+    padding-top: 20px;
     text-align: center;
 
     span {

+ 1 - 0
front/project/www/routes/examination/list/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'examination-list',
   title: '模考列表',
   needLogin: false,
+  repeat: true,
   tab: 'examination',
   component() {
     return import('./page');

+ 1 - 0
front/project/www/routes/examination/main/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'examination',
   title: '模考',
   needLogin: false,
+  repeat: true,
   tab: 'examination',
   component() {
     return import('./page');

+ 1 - 0
front/project/www/routes/exercise/list/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'exercise-list',
   title: '练习列表',
   needLogin: false,
+  repeat: true,
   tab: 'exercise',
   component() {
     return import('./page');

+ 1 - 0
front/project/www/routes/exercise/main/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'exercise',
   title: '练习',
   needLogin: false,
+  repeat: true,
   tab: 'exercise',
   component() {
     return import('./page');

+ 2 - 1
front/project/www/routes/index.js

@@ -6,7 +6,8 @@ import Exercise from './exercise';
 import My from './my';
 import Paper from './paper';
 import Preview from './preview';
+import Question from './question';
 import Sentence from './sentence';
 import Textbook from './textbook';
 
-export default [...Page, ...Examination, ...Exercise, ...My, ...Paper, ...Preview, ...Sentence, ...Textbook];
+export default [...Page, ...Examination, ...Exercise, ...My, ...Paper, ...Preview, ...Question, ...Sentence, ...Textbook];

+ 2 - 0
front/project/www/routes/my/course/index.less

@@ -106,6 +106,7 @@
             height: 110px;
             background-color: #D8D8D8;
             margin-right: 10px;
+            vertical-align: top;
           }
 
           .info {
@@ -216,6 +217,7 @@
             height: 110px;
             background-color: #D8D8D8;
             margin-right: 10px;
+            vertical-align: top;
           }
 
           .item {

+ 25 - 38
front/project/www/routes/my/course/page.js

@@ -19,7 +19,7 @@ import Date from '../../../components/Date';
 export default class extends Page {
   initState() {
     return {
-      tab1: '2',
+      tab1: '1',
       tab2: '1',
       data: [
         {
@@ -225,25 +225,25 @@ class Course extends Component {
           </div>
           <div className="right">
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="speed-block" noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
             </div>
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="question-block" active noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
             </div>
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="clockin-block" noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
             </div>
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="note-block" noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
@@ -302,28 +302,8 @@ class Course extends Component {
           </div>
           <div className="right">
             <div className="item">
-              <Assets name="logic" />
-              <div className="text">
-                <span>12</span>/20
-              </div>
-            </div>
-            <div className="item">
-              <Assets name="logic" />
-              <div className="text">
-                <span>12</span>/20
-              </div>
-            </div>
-            <div className="item">
-              <Assets name="logic" />
-              <div className="text">
-                <span>12</span>/20
-              </div>
-            </div>
-            <div className="item">
-              <Assets name="logic" />
-              <div className="text">
-                <span>12</span>/20
-              </div>
+              <GIcon name="gift-block" active />
+              <div className="text">赠送7天</div>
             </div>
           </div>
         </div>
@@ -376,24 +356,23 @@ class Education extends Component {
         </div>
         <div className="detail">
           <div className="left">
-            <div className="item">
+            <Assets name="sun_blue" />
+            <div className="info">
               <div className="t1">授课老师</div>
               <div className="t2">李小小</div>
-            </div>
-            <div className="item">
               <div className="t1">有效期</div>
               <div className="t2">88Day</div>
             </div>
           </div>
           <div className="right">
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="speed-block" noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
             </div>
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="question-block" active noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
@@ -466,24 +445,23 @@ class Education extends Component {
         </div>
         <div className="detail">
           <div className="left">
-            <div className="item">
+            <Assets name="sun_blue" />
+            <div className="info">
               <div className="t1">授课老师</div>
               <div className="t2">李小小</div>
-            </div>
-            <div className="item">
               <div className="t1">有效期</div>
               <div className="t2">88Day</div>
             </div>
           </div>
           <div className="right">
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="speed-block" noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
             </div>
             <div className="item">
-              <Assets name="logic" />
+              <GIcon name="question-block" active noHover />
               <div className="text">
                 <span>12</span>/20
               </div>
@@ -529,6 +507,15 @@ class TimeLineItem extends Component {
       6: '备考信息',
       7: '完成作业',
     };
+    this.iconMap = {
+      1: 'time-icon',
+      2: 'QA-icon',
+      3: 'class-icon',
+      4: 'note-icon',
+      5: 'supplement-icon',
+      6: 'information-icon',
+      7: 'homework-icon',
+    };
   }
 
   onClick(key) {
@@ -544,7 +531,7 @@ class TimeLineItem extends Component {
     return (
       <div className={`time-line-item ${status}`}>
         <div className="icon-title">
-          <GIcon name="star" active={status !== 'not'} noHover />
+          <GIcon name={this.iconMap[type]} active={status !== 'not'} noHover />
           <div className="title">{this.titleMap[type]}</div>
         </div>
         <div className="time-line-detail">{this.renderDetail()}</div>

+ 18 - 5
front/project/www/routes/my/error/page.js

@@ -9,6 +9,7 @@ import menu from '../index';
 import Tabs from '../../../components/Tabs';
 import Modal from '../../../components/Modal';
 import Select from '../../../components/Select';
+import GIcon from '../../../components/Icon';
 
 const columns = [
   { key: '', title: '题型', fixSort: true },
@@ -17,7 +18,13 @@ const columns = [
   { key: '', title: '耗时', sort: true },
   { key: '', title: '错误率', sort: true },
   { key: '', title: '最近做题' },
-  { key: '', title: '' },
+  {
+    key: '',
+    title: '',
+    render() {
+      return [<GIcon name="star" className="m-r-5" />, <GIcon name="note" />];
+    },
+  },
 ];
 
 const exportType = [
@@ -32,9 +39,10 @@ const exportType = [
 export default class extends Page {
   initState() {
     return {
+      tab: '1',
       filterMap: {},
       sortMap: {},
-      data: [],
+      data: [{}, {}],
       selectList: [],
       allChecked: false,
       page: 1,
@@ -42,6 +50,10 @@ export default class extends Page {
     };
   }
 
+  onChangeTab(tab) {
+    this.setState({ tab });
+  }
+
   onFilter(value) {
     this.setState({ filterMap: value });
   }
@@ -79,7 +91,7 @@ export default class extends Page {
   }
 
   renderTable() {
-    const { filterMap = {}, sortMap = {}, selectList = [], data = [], allChecked, page, total } = this.state;
+    const { tab, filterMap = {}, sortMap = {}, selectList = [], data = [], allChecked, page, total } = this.state;
     return (
       <div className="table-layout">
         <Tabs
@@ -89,8 +101,9 @@ export default class extends Page {
           size="small"
           space={2.5}
           width={100}
-          active={'1'}
+          active={tab}
           tabs={[{ key: '1', title: '练习' }, { key: '2', title: '模考' }]}
+          onChange={key => this.onChangeTab(key)}
         />
         <UserAction
           search
@@ -122,7 +135,7 @@ export default class extends Page {
           btnList={[
             { title: '移除', key: 'remove' },
             { title: '组卷', key: 'group', tag: 'vip' },
-            { title: '导出', key: 'export', tag: 'vip' },
+            { title: '导出', key: 'export', tag: 'vip', disabled: true },
           ]}
           right={
             <div className="tip">

+ 22 - 3
front/project/www/routes/my/main/index.less

@@ -22,6 +22,10 @@
           margin-left: 25px;
           display: inline-block;
 
+          .assets {
+            margin-top: 5px;
+          }
+
           b {
             color: #303036;
             font-size: 20px;
@@ -71,7 +75,7 @@
       .open {
         position: absolute;
         right: 30px;
-        top: 25px;
+        top: 30px;
       }
 
       .table {
@@ -180,6 +184,21 @@
           }
         }
       }
+
+      .auth {
+        .invite {
+          display: inline-block;
+          padding-right: 5px;
+          margin-right: 5px;
+          border-right: 1px solid #eee;
+        }
+
+        .assets {
+          margin-top: 4px;
+          margin-right: 4px;
+          cursor: pointer;
+        }
+      }
     }
 
     .footer {
@@ -245,10 +264,10 @@
           background: rgba(252, 95, 95, 1);
         }
 
-        .assets {
+        .icon {
           position: absolute;
           right: 0;
-          top: 5px;
+          top: 3px;
           cursor: pointer;
         }
       }

+ 12 - 7
front/project/www/routes/my/main/page.js

@@ -39,7 +39,7 @@ class LogItem extends Component {
           })}
         </div>
         <div className="open">
-          <GIcon name={open ? 'up' : 'down'} onClick={() => this.setState({ open: !open })} />
+          <GIcon name={open ? 'small-up' : 'small-down'} onClick={() => this.setState({ open: !open })} />
         </div>
         <div hidden={!open} className="table">
           <UserTable size="small" columns={this.columns} data={detail} />
@@ -138,10 +138,10 @@ export default class extends Page {
               本周学习时间<b>23</b>Hour
             </span>
             <span>
-              同比上周<b>15</b>%
+              同比上周<b>15</b>% <Assets name="up" />
             </span>
             <span>
-              同比全站<b>15</b>%
+              同比全站<b>15</b>% <Assets name="down" />
             </span>
           </div>
         </div>
@@ -155,7 +155,7 @@ export default class extends Page {
           <Button size="small" radius>
             前天
           </Button>
-          <Assets className="right" name="calculator_icon" />
+          <Assets className="right" name="calendar" />
         </div>
         {logList.map((log, index) => {
           return <LogItem key={index} data={log} />;
@@ -223,10 +223,15 @@ export default class extends Page {
                 邀请
               </Button>
             </span>
+            <Assets name="wechat" />
+            <Assets name="phone_1" />
+            <Assets name="realname" />
+            <Assets name="email" />
+            <Assets name="information" />
           </div>
         </div>
         <div className="footer">
-          <span className="tag">VIP</span>
+          <Assets className="m-r-5" name="VIP" />
           <span className="date">2019-10-15到期</span>
           <Link to="">续费</Link>
         </div>
@@ -263,14 +268,14 @@ export default class extends Page {
     return (
       <div className="message-layout">
         <div className="header">
-          <Assets name="dot2" />
+          <Assets name="all" />
           全部已读
         </div>
         <div className="body">
           <div className="item">
             <div className="title dot">老师回答了您的提问</div>
             <div className="date">2019-05-15 16:21:06</div>
-            <Assets name="right_icon" onClick={() => {}} />
+            <GIcon name="arrow-right-small" onClick={() => {}} />
           </div>
           <div className="item">
             <div className="title dot">老师回答了您的提问</div>

+ 111 - 2
front/project/www/routes/my/report/page.js

@@ -2,11 +2,48 @@ import React from 'react';
 import './index.less';
 import Page from '@src/containers/Page';
 import UserLayout from '../../../layouts/User';
+import Tabs from '../../../components/Tabs';
+import UserAction from '../../../components/UserAction';
+import UserTable from '../../../components/UserTable';
+import IconButton from '../../../components/IconButton';
 import menu from '../index';
 
+const columns = [
+  { key: '', title: '练习册名称', fixSort: true },
+  { key: '', title: '做题时间', fixSort: true },
+  { key: '', title: '正确率', sort: true },
+  { key: '', title: '平均耗时', sort: true },
+  { key: '', title: '完成度' },
+  { key: '', title: '报告' },
+  {
+    key: '',
+    title: '',
+    render() {
+      return <IconButton type="report" tip="report" />;
+    },
+  },
+];
+
 export default class extends Page {
   initState() {
-    return {};
+    return {
+      tab: '1',
+      filterMap: {},
+      sortMap: {},
+      data: [{}, {}],
+      selectList: [],
+      allChecked: false,
+      page: 1,
+      total: 1,
+    };
+  }
+
+  onChangeTab(tab) {
+    this.setState({ tab });
+  }
+
+  onFilter(value) {
+    this.setState({ filterMap: value });
   }
 
   renderView() {
@@ -15,6 +52,78 @@ export default class extends Page {
   }
 
   renderTable() {
-    return <div className="table-layout">1</div>;
+    const { tab, filterMap = {}, sortMap = {}, selectList = [], data = [], page, total } = this.state;
+    return (
+      <div className="table-layout">
+        <Tabs
+          border
+          type="division"
+          theme="theme"
+          size="small"
+          space={2.5}
+          width={100}
+          active={tab}
+          tabs={[
+            { key: '1', title: '练习' },
+            { key: '2', title: '模考' },
+            { key: '3', title: '错题组卷' },
+            { key: '4', title: '收藏组卷' },
+          ]}
+          onChange={key => this.onChangeTab(key)}
+        />
+        <UserAction
+          search
+          selectList={[
+            {
+              label: '123',
+              children: [
+                {
+                  key: 'one',
+                  default: '1',
+                  select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
+                },
+                {
+                  key: 'two',
+                  be: 'one',
+                  placeholder: '全部',
+                  selectMap: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
+                },
+              ],
+            },
+            {
+              label: '123',
+              right: true,
+              children: [
+                {
+                  key: 'one',
+                  default: '1',
+                  select: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
+                },
+                {
+                  key: 'two',
+                  be: 'one',
+                  placeholder: '全部',
+                  selectMap: [{ title: '123', key: '1' }, { title: '123', key: '2' }, { title: '123', key: '2' }],
+                },
+              ],
+            },
+          ]}
+          filterMap={filterMap}
+          onFilter={value => this.onFilter(value)}
+          onChange={key => this.onChangeTab(key)}
+        />
+        <UserTable
+          columns={columns}
+          sortMap={sortMap}
+          data={data}
+          current={page}
+          total={total}
+          selectList={selectList}
+          onSelect={l => this.onSelect(l)}
+          onSort={v => this.onSort(v)}
+          onChange={p => this.onDataChange(p)}
+        />
+      </div>
+    );
   }
 }

+ 1 - 0
front/project/www/routes/page/home/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'index',
   title: '首页',
   needLogin: false,
+  repeat: true,
   tab: 'main',
   component() {
     return import('./page');

+ 1 - 0
front/project/www/routes/paper/process/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'paper-process',
   title: '考试',
   needLogin: true,
+  repeat: true,
   hideHeader: true,
   component() {
     return import('./page');

+ 607 - 0
front/project/www/routes/paper/question/detail/index.js

@@ -0,0 +1,607 @@
+import React, { Component } from 'react';
+import ReactDOM from 'react-dom';
+import { Carousel, Tooltip } from 'antd';
+import { Link } from 'react-router-dom';
+import Fullscreen from 'react-fullscreen-crossbrowser';
+import './index.less';
+import { formatSeconds, formatPercent, formatDate } from '@src/services/Tools';
+import Assets from '@src/components/Assets';
+import Navigation from '../../../../components/Navigation';
+import Tabs from '../../../../components/Tabs';
+import Icon from '../../../../components/Icon';
+import Switch from '../../../../components/Switch';
+import Select from '../../../../components/Select';
+import { Button } from '../../../../components/Button';
+import AnswerSelect from '../../../../components/AnswerSelect';
+import AnswerList from '../../../../components/AnswerList';
+import AnswerButton from '../../../../components/AnswerButton';
+import AnswerTable from '../../../../components/AnswerTable';
+import OtherAnswer from '../../../../components/OtherAnswer';
+import { AskTarget } from '../../../../../Constant';
+import { Question } from '../../../../stores/question';
+import { My } from '../../../../stores/my';
+import Sentence from '../../process/sentence';
+
+export default class extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      step: 0,
+      hideAnalysis: true,
+      analysisTab: 'official',
+      showAnswer: false,
+      noteField: AskTarget[0].key,
+      showIds: false,
+    };
+  }
+
+
+  prevQuestion() {
+    const { userQuestion } = this.props;
+    if (userQuestion.no === 1) return;
+    Question.getDetailByNo(userQuestion.reportId, userQuestion.no - 1).then((r) => {
+      linkTo(`/paper/question/${r.id}`);
+    });
+  }
+
+  nextQuestion() {
+    const { userQuestion } = this.props;
+    if (userQuestion.questionNumber === userQuestion.no) return;
+    Question.getDetailByNo(userQuestion.reportId, userQuestion.no + 1).then((r) => {
+      linkTo(`/paper/question/${r.id}`);
+    });
+  }
+
+  submitAsk() {
+    const { question = {}, questionNo = {}, paper = {} } = this.props;
+    const { ask = {} } = this.state;
+    if (ask.originContent === '' || ask.content === '' || ask.target === '') return;
+    My.addQuestionAsk(paper.id, ask.target, question.questionModule, questionNo.id, ask.originContent, ask.content).then(() => {
+      this.setState({ askModal: false, askOkModal: true });
+    }).catch(err => {
+      this.setState({ askError: err.message });
+    });
+  }
+
+  submitFeedbackError() {
+    const { userQuestion = {}, questionNo = {} } = this.props;
+    const { feedback = {} } = this.state;
+    if (feedback.originContent === '' || feedback.content === '' || feedback.target === '') return;
+    My.addFeedbackErrorQuestion(userQuestion.questionModule, questionNo.id, questionNo.title, feedback.target, feedback.originContent, feedback.content).then(() => {
+      this.setState({ feedbackModal: false, feedbackOkModal: true });
+    }).catch(err => {
+      this.setState({ feedbackError: err.message });
+    });
+  }
+
+  submitNote(close) {
+    const { userQuestion = {}, questionNo = {} } = this.props;
+    const { note = {} } = this.state;
+    My.updateQuestionNote(userQuestion.questionModule, questionNo.id, note).then(() => {
+      if (close) this.setState({ noteModal: false });
+    }).catch(err => {
+      this.setState({ noteError: err.message });
+    });
+  }
+
+  toggleFullscreen() {
+    const { isFullscreenEnabled } = this.state;
+    this.setState({ isFullscreenEnabled: !isFullscreenEnabled });
+  }
+
+  toggleCollect() {
+    const { userQuestion = {}, questionNo = {}, flow } = this.props;
+    if (!userQuestion.collect) {
+      My.addQuestionCollect(userQuestion.questionModule, questionNo.id).then(() => {
+        userQuestion.collect = true;
+        flow.setState({ userQuestion });
+      });
+    } else {
+      My.delQuestionCollect(userQuestion.questionModule, questionNo.id).then(() => {
+        userQuestion.collect = false;
+        flow.setState({ userQuestion });
+      });
+    }
+  }
+
+  switchNo(no) {
+    linkTo(`/question/detail/${no.id}`);
+  }
+
+  formatStem(text) {
+    if (!text) return '';
+    const { showAnswer, question = { content: {} }, userQuestion } = this.props;
+    const { table = {}, questions = [] } = question.content;
+    text = text.replace(/#select#/g, "<span class='#select#' />");
+    text = text.replace(/#table#/g, "<span class='#table#' />");
+    setTimeout(() => {
+      const selectList = document.getElementsByClassName('#select#');
+      const tableList = document.getElementsByClassName('#table#');
+      for (let i = 0; i < selectList.length; i += 1) {
+        if (!questions[i]) break;
+        ReactDOM.render(
+          <AnswerSelect
+            list={questions[i].select}
+            type={'single'}
+            selected={(userQuestion.userAnswer || { questions: [] }).questions[i]}
+            answer={(question.answer || { questions: [] }).questions[i]}
+            fix
+            show={showAnswer} />,
+          selectList[i],
+        );
+      }
+      if (table.row && table.col && table.header) {
+        const columns = table.header.map((title, index) => {
+          return { title, key: index };
+        });
+        for (let i = 0; i < tableList.length; i += 1) {
+          ReactDOM.render(<AnswerTable list={columns} columns={columns} data={table.data} />, tableList[i]);
+        }
+      }
+    }, 1);
+    return text;
+  }
+
+  render() {
+    return (
+      <Fullscreen
+        enabled={this.state.isFullscreenEnabled}
+        onChange={isFullscreenEnabled => this.setState({ isFullscreenEnabled })}
+      >
+        {this.renderDetail()}
+      </Fullscreen>
+    );
+  }
+
+  renderDetail() {
+    const { paper = {} } = this.props;
+    switch (paper.paperModule) {
+      case 'sentence':
+        return <Sentence {...this.props} {...this.state} flow={this} scene='answer' mode='question' />;
+      default:
+        return <div className='base'>{this.renderBase()}</div>;
+    }
+  }
+
+  renderHeader() {
+    const { userQuestion = {}, questionNo = {}, paper = {}, report = {}, questionNos = [], question = {}, info, detail } = this.props;
+    const { showIds } = this.state;
+    return <div className={'layout-header'}>
+      {detail && <div className="left">
+        {paper.paperModule && paper.paperModule !== 'examination' && <div className="btn"><Button radius onClick={() => {
+          linkTo(`/paper/report/${report.id}`);
+        }}>返回练习报告</Button></div>}
+        {paper.paperModule && paper.paperModule === 'examination' && <div className="btn"><Button radius onClick={() => {
+          linkTo(`/paper/report/${report.id}`);
+        }}>返回成绩单</Button></div>}
+        <div className="no">No.{userQuestion.stageNo || userQuestion.no}</div>
+        <div className="title"><Assets name='book' />{paper.title}</div>
+      </div>}
+      <div className="center">
+        <div className="menu-wrap">
+          ID:{questionNo.title}
+          {questionNos && questionNos.length > 0 && <Icon name="more" onClick={() => {
+            this.setState({ showIds: true });
+          }} />}
+          {showIds && <div className='menu-content'>
+            <p>题源汇总</p>
+            {(questionNos || []).map((row) => <p onClick={() => info && this.switchNo(row)}>ID:{row.title}</p>)}
+          </div>}
+        </div>
+      </div>
+      <div className="right" hidden={question.questionType === 'awa'}>
+        {detail && <span className="b" hidden={!userQuestion.id}>
+          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
+          {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
+        </span>}
+        <span className="b">
+          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
+          {/* 全站:<span className="s">1</span>m<span className="s">39</span>s */}
+        </span>
+        <span className="b">
+          <span className="s">{formatPercent(questionNo.totalCorrect, questionNo.totalNumber)}</span>%
+        </span>
+        <Icon name="question" />
+        <Icon name="star" active={userQuestion.collect} onClick={() => this.toggleCollect()} />
+      </div>
+    </div>;
+  }
+
+  renderBase() {
+    const { questionStatus, userQuestion = {}, paper = {}, detail } = this.props;
+    const { showIds } = this.state;
+    return <div className={`layout ${paper.paperModule}`} onClick={() => {
+      if (showIds) this.setState({ showIds: false });
+    }}>
+      {this.renderHeader()}
+      <div className="layout-body">{this.renderBody()}</div>
+      <div className="layout-footer">
+        <div className="left">
+          <Tooltip overlayClassName='gray' placement='top' title='全屏'>
+            <a>
+              <Icon name={this.state.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => this.toggleFullscreen()} />
+            </a>
+          </Tooltip>
+        </div>
+        <div className="center">
+          <AnswerButton className="item" onClick={() => this.setState({ noteModal: true })}>笔记</AnswerButton>
+          {questionStatus >= 0 && <AnswerButton className="item" onClick={() => {
+            if (questionStatus > 0) {
+              this.setState({ askModal: true });
+            } else {
+              this.setState({ askFailModal: true });
+            }
+          }}>提问</AnswerButton>}
+          <AnswerButton className="item" onClick={() => this.setState({ feedbackModal: true })}>纠错</AnswerButton>
+        </div>
+        {detail && <div className="right">
+          {userQuestion.no !== 1 && <Icon name="prev" onClick={() => this.prevQuestion()} />}
+          {userQuestion.questionNumber !== userQuestion.no && <Icon name="next" onClick={() => this.nextQuestion()} />}
+        </div>}
+      </div>
+      {this.state.askModal && this.renderAsk()}
+      {this.state.askOkModal && this.renderAskOk()}
+      {this.state.askFailModal && this.renderAskFail()}
+      {this.state.feedbackModal && this.renderFeedbackError()}
+      {this.state.feedbackOkModal && this.renderFeedbackErrorOk()}
+      {this.state.noteModal && this.renderNote()}
+    </div>;
+  }
+
+  renderBody() {
+    const { question = { content: {} } } = this.props;
+    const { typeset = 'one' } = question.content;
+    const { hideAnalysis } = this.state;
+    const show = typeset === 'one' ? true : !hideAnalysis;
+    return (
+      <div className="layout-content">
+        <div className='two'>
+          {this.renderContent()}
+          {question.questionType !== 'awa' && this.renderAnswer()}
+          {question.questionType === 'awa' && this.renderAWA()}
+        </div>
+        {question.questionType !== 'awa' && this.renderAnalysis()}
+        {typeset === 'two' && question.questionType !== 'awa' && (
+          <div className="fixed-analysis" onClick={() => this.setState({ hideAnalysis: !hideAnalysis })}>
+            {show ? '收起解析 >' : '查看解析 <'}
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  renderAnalysis() {
+    const { question = { content: {} } } = this.props;
+    const { typeset = 'one' } = question.content;
+    const { hideAnalysis, analysisTab } = this.state;
+    const show = typeset === 'one' ? true : !hideAnalysis;
+    return (
+      <div className={`block block-analysis two-analysis ${show ? 'show' : ''}`}>
+        <Tabs
+          type="division"
+          active={analysisTab}
+          space={2}
+          tabs={[
+            { key: 'official', name: '官方解析' },
+            { key: 'qx', name: '千行解析' },
+            { key: 'association', name: '题源联想' },
+            { key: 'qa', name: '相关回答' },
+          ]}
+          onChange={(key) => {
+            this.setState({ analysisTab: key });
+          }}
+        />
+        <div className="detail">
+          {typeset === 'two' && this.renderAnswer()}
+          {this.renderText()}
+        </div>
+      </div>
+    );
+  }
+
+  renderText() {
+    const { question = {}, userQuestion = {} } = this.props;
+    const { asks = [], associations = [] } = userQuestion;
+    const { analysisTab } = this.state;
+    let content;
+    switch (analysisTab) {
+      case 'official':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.officialContent }} />;
+        break;
+      case 'qx':
+        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.qxContent }} />;
+        break;
+      case 'association':
+        content = <div className="detail-block">
+          <Carousel>
+            {associations.map(association => {
+              return <div className="text-block" dangerouslySetInnerHTML={{ __html: association.stem }} />;
+            })}
+          </Carousel>
+        </div>;
+        break;
+      case 'qa':
+        content = <div className="detail-block answer-block">
+          {asks.map((ask, index) => {
+            return <OtherAnswer key={index} data={ask} />;
+          })}
+        </div>;
+        break;
+      default:
+        break;
+    }
+    return content;
+  }
+
+  renderAnswer() {
+    const { question = { content: {} }, userQuestion = {}, detail } = this.props;
+    const { questions = [], type, typeset = 'one' } = question.content;
+    const { showAnswer } = this.state;
+    return <div className="block block-answer">
+      {detail && typeset === 'two' ? <Switch checked={showAnswer} onChange={(value) => {
+        this.setState({ showAnswer: value });
+      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
+      {questions.map((item, index) => {
+        return (
+          <div>
+            <div className="text m-b-2">{item.description}</div>
+            <AnswerList
+              show={showAnswer}
+              selected={(userQuestion.userAnswer || { questions: [] }).questions[index]}
+              answer={(question.answer || { questions: [] }).questions[index]}
+              distributed={(question.answerDistributed || { questions: [] }).questions[index]}
+              list={item.select}
+              type={type}
+              first={item.first}
+              second={item.second}
+              direction={item.direction}
+            />
+          </div>
+        );
+      })}
+    </div>;
+  }
+
+  renderContent() {
+    const { question = { content: {} }, detail } = this.props;
+    const { typeset = 'one' } = question.content;
+    const { steps = [] } = question.content;
+    const { showAnswer, step } = this.state;
+    return (
+      <div className="block block-content">
+        {detail && typeset === 'one' && question.questionType !== 'awa' ? <Switch checked={showAnswer} onChange={(value) => {
+          this.setState({ showAnswer: value });
+        }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
+        {question.questionType === 'awa' && <h2>Analytical Writing Assessment</h2>}
+        {steps.length > 0 && <Navigation theme='detail' list={question.content.steps} active={step} onChange={(v) => this.setState({ step: v })} />}
+        <div className="text" style={{ height: 2000 }} dangerouslySetInnerHTML={{ __html: this.formatStem(steps.length > 0 ? steps[step].stem : question.stem) }} />
+      </div>
+    );
+  }
+
+  renderAWA() {
+    const { userQuestion = { detail: {}, userAnswer: {} } } = this.state;
+    const { showAnswer } = this.state;
+    return <div className="block block-awa">
+      <Switch checked={showAnswer} onChange={(value) => {
+        this.setState({ showAnswer: value });
+      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch>
+      <div className="body">
+        <h2>Your Response</h2>
+        {showAnswer && <div className='detail'>
+          <div className='info'>
+            <span className="b">
+              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
+              {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
+            </span>
+            <span className="b">
+              单词数:<span className="s">{Number((userQuestion.detail || {}).words || 0)}</span>词
+            </span>
+          </div>
+          <div className='content-awa' dangerouslySetInnerHTML={{ __html: userQuestion.userAnswer.awa || '' }} />
+        </div>}
+        {!showAnswer && <div className='show-awa'>选择「显示答案」查看自己的作文</div>}
+      </div>
+    </div>;
+  }
+
+  renderAsk() {
+    const { ask = {} } = this.state;
+    return (
+      <div className="modal ask">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">提问</div>
+          <div className="desc">
+            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={ask.target} list={AskTarget} onChange={(item) => {
+              ask.target = item.value;
+              this.setState({ ask });
+            }} />进行提问</div>
+            <div className="label">有疑问的具体内容是:</div>
+            <textarea className="textarea" value={ask.originContent} placeholder="请复制粘贴有疑问的内容。" onChange={(e) => {
+              ask.originContent = e.target.value;
+              this.setState({ ask });
+            }} />
+            <div className="label">针对以上内容的问题是:</div>
+            <textarea className="textarea" value={ask.content} placeholder="提问频率高的问题会被优先回答哦。" onChange={(e) => {
+              ask.content = e.target.value;
+              this.setState({ ask });
+            }} />
+          </div>
+          <div className="bottom">
+            <AnswerButton theme="cancel" size="lager" onClick={() => this.setState({ askModal: false })}>
+              取消
+            </AnswerButton>
+            <AnswerButton size="lager" onClick={() => this.submitAsk()}>提交</AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderAskOk() {
+    return (
+      <div className="modal ask-ok">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">提问</div>
+          <div className="content">
+            <div className="left">
+              <div className="text">已提交成功!</div>
+              <div className="text">关注公众号,老师回答后会立即收到通知。</div>
+              <div className="text">我们也会通过站内信的方式通知你。</div>
+              <div className="small">成为学员享受极速答疑特权。<Link>了解更多</Link></div>
+            </div>
+            <div className="right">
+              <div className="text">扫码关注公众号</div>
+              <div className="text">千行GMAT</div>
+            </div>
+          </div>
+          <div className="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ askOkModal: false });
+            }}>
+              好的,知道了
+            </AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderAskFail() {
+    return (
+      <div className="modal ask-ok">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">提问</div>
+          <div className="content">
+            <div className="left">
+              <div className="text">提问功能正在维护中。</div>
+              <div className="text">可先查阅“相关问答” 或 成为学员享受极速 答疑特权。</div>
+              <Link to="/">了解更多></Link>
+            </div>
+            <div className="right">
+              <div className="text">扫码关注公众号</div>
+              <div className="text">千行GMAT</div>
+            </div>
+          </div>
+          <div className="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ askFailModal: false });
+            }}>
+              好的,知道了
+            </AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderFeedbackError() {
+    const { feedback = {} } = this.state;
+    return (
+      <div className="modal error">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">纠错</div>
+          <div className="desc">
+            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={feedback.target} list={AskTarget} onChange={(item) => {
+              feedback.target = item.value;
+              this.setState({ feedback });
+            }} />进行提问</div>
+            <div className="label">错误内容是:</div>
+            <textarea className="textarea" value={feedback.originContent} placeholder="你可以适当扩大复制范围以使我们准确定位,感谢。" />
+            <div className="label">应该改为:</div>
+            <textarea className="textarea" placeholder="只需提供正确内容即可" />
+          </div>
+          <div className="bottom">
+            <AnswerButton theme="cancel" size="lager" onClick={() => {
+              this.setState({ feedbackModal: false });
+            }}>
+              取消
+            </AnswerButton>
+            <AnswerButton size="lager" onClick={() => {
+              this.submitFeedbackError();
+            }}>提交</AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderFeedbackErrorOk() {
+    return (
+      <div className="modal error-ok">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">纠错</div>
+          <div className="content">
+            <div className="left">
+              <div className="text"><Assets name='right' svg />已提交成功!</div>
+              <div className="text">感谢您的耐心反馈,我们会尽快核实并以站内信的方式告知结果。</div>
+              <div className="text">您也可以关注公众号及时获取结果。</div>
+            </div>
+            <div className="right">
+              <div className="text">扫码关注公众号</div>
+              <div className="text">千行GMAT</div>
+            </div>
+          </div>
+          <div className="confirm">
+            <AnswerButton size="lager" theme="confirm" onClick={() => {
+              this.setState({ feedbackOkModal: false });
+            }}>
+              好的,知道了
+            </AnswerButton>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderNote() {
+    const { noteField, note = {} } = this.state;
+    return (
+      <div className="modal note">
+        <div className="mask" />
+        <div className="body">
+          <div className="title">笔记</div>
+          <div className="content">
+            <div className="tabs">
+              {AskTarget.map(item => {
+                return (
+                  <div className={`tab ${noteField === item.key ? 'active' : ''}`} onClick={() => {
+                    this.setState({ noteField: item.key });
+                  }}>
+                    <div className="text">{item.label}</div>
+                    <div className="date">{note[`${item.key}Time`] ? formatDate(note[`${item.key}Time`]) : ''}</div>
+                  </div>
+                );
+              })}
+            </div>
+            <div className="input">
+              <textarea className="textarea" value={note[`${noteField}Content`] || ''} placeholder="记下笔记,方便以后复习" onChange={(e) => {
+                note[`${noteField}Time`] = new Date();
+                note[`${noteField}Content`] = e.target.value;
+                this.setState({ note });
+              }} />
+              <div className="bottom">
+                <AnswerButton theme="cancel" size="lager" onClick={() => {
+                  this.setState({ noteModal: false });
+                }}>
+                  取消
+                </AnswerButton>
+                <AnswerButton size="lager" onClick={() => {
+                  this.submitNote();
+                }}>编辑</AnswerButton>
+                <AnswerButton size="lager" onClick={() => {
+                  this.submitNote(true);
+                }}>保存</AnswerButton>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}

+ 500 - 0
front/project/www/routes/paper/question/detail/index.less

@@ -0,0 +1,500 @@
+@charset "utf-8";
+
+#paper-question,
+#question-detail {
+  height: 100%;
+
+  .base {
+    height: 100%;
+
+    .layout {
+      background: #fff;
+      height: 100%;
+      display: flex;
+      flex-direction: row;
+
+      // &.exercise {
+      //   border-top: 20px solid #7775CA;
+      //   background: #fff;
+      // }
+
+      // &.sentence {
+      //   border-top: 20px solid #435C96;
+      //   background: #fff;
+      // }
+
+      &.examination {
+        border-top: 20px solid #8D909C;
+        background: #fff;
+
+        .layout-header {
+          height: 60px;
+          top: 20px;
+        }
+      }
+
+      .layout-header {
+        height: 60px;
+        line-height: 60px;
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        box-shadow: 0px 4px 14px 0px rgba(189, 199, 215, 0.16);
+        text-align: center;
+        z-index: 80;
+
+        .left {
+          position: absolute;
+          left: 50px;
+
+          .btn {
+            display: inline-block;
+            padding-right: 10px;
+            line-height: 60px;
+          }
+
+          .no {
+            font-size: 20px;
+            display: inline-block;
+            color: #303036;
+            font-size: 20px;
+            margin-right: 25px;
+          }
+
+          .title {
+            color: #A7A7B7;
+            display: inline-block;
+            font-size: 20px;
+
+            img {
+              margin-top: -3px;
+              margin-right: 5px;
+            }
+          }
+        }
+
+
+        .menu-wrap {
+          position: absolute;
+          right: 0;
+          text-align: left;
+          padding: 0 10px;
+          white-space: nowrap;
+
+          .menu-content {
+            position: absolute;
+            background: #fff;
+            text-align: left;
+            top: 50px;
+            right: 10px;
+            border: 1px solid #EAEDF2;
+            padding: 10px 20px;
+            min-width: 150px;
+
+            p {
+              line-height: 30px;
+              height: 30px;
+              text-align: left;
+              margin: 0;
+            }
+          }
+        }
+
+        .center {
+          position: absolute;
+          right: 50%;
+          transform: translateX(100%);
+
+          .icon {
+            margin-left: 20px;
+          }
+        }
+
+        .right {
+          position: absolute;
+          right: 50px;
+
+          .b {
+            margin-left: 30px;
+
+            .s {
+              color: #4299FF;
+            }
+          }
+
+          .icon {
+            margin-left: 10px;
+          }
+        }
+      }
+    }
+
+    .layout-footer {
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      height: 60px;
+      line-height: 60px;
+      box-shadow: 0px -4px 14px 0px rgba(189, 199, 215, 0.16);
+
+      .left {
+        width: 30%;
+        display: inline-block;
+        padding-left: 50px;
+      }
+
+      .right {
+        width: 30%;
+        display: inline-block;
+        text-align: right;
+        padding-right: 50px;
+
+        .icon {
+          margin-left: 10px;
+        }
+      }
+
+      .center {
+        width: 40%;
+        display: inline-block;
+        text-align: center;
+
+        .item {
+          margin: 0 10px;
+        }
+      }
+    }
+
+    .layout-body {
+      background: #fff;
+      flex: 1;
+      overflow: hidden;
+      margin: 60px 0;
+
+      .layout-content {
+        height: 100%;
+        position: relative;
+
+        .one {
+          flex: 1;
+          display: flex;
+          flex-direction: column;
+        }
+
+        .two {
+          flex: 1;
+          display: flex;
+          flex-direction: row;
+          overflow: hidden;
+          height: 100%;
+        }
+
+        .block {
+          flex: 1;
+        }
+
+        .block-content,
+        .block-answer,
+        .block-awa {
+          padding: 30px 60px;
+          color: #303036;
+          height: 100%;
+          overflow: hidden;
+          overflow-y: auto;
+        }
+
+        .block-content {
+          h2 {
+            padding: 65px 0 20px 0px;
+            font-size: 20px;
+            color: #303036;
+          }
+        }
+
+        .block-awa {
+          background: #EFF3F7;
+
+          h2 {
+            font-size: 20px;
+            color: #303036;
+            margin-top: 37px;
+            margin-bottom: 23px;
+          }
+
+          .detail {
+            .info {
+              font-weight: bold;
+              font-size: 18px;
+              color: #303036;
+
+              span.b {
+                margin-right: 80px;
+
+                .s {
+                  color: #4299FF;
+                }
+              }
+            }
+          }
+
+          .content-awa {
+            padding-top: 50px;
+            color: #686872;
+            font-size: 16px;
+          }
+
+          .show-awa {
+            font-size: 12px;
+            width: 100%;
+            height: 100%;
+            margin: 50% 0;
+            text-align: center;
+            line-height: 20px;
+            color: #A7A7B7;
+          }
+        }
+
+        .block-analysis {
+          background: #EFF3F7;
+          padding: 25px 25px 0 20px;
+          display: flex;
+          flex-direction: column;
+
+          .block-answer {
+            padding: 38px 50px;
+          }
+
+          .block {
+            background: #fff;
+          }
+
+          .detail {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+
+            .detail-block {
+              margin-top: 5px;
+              flex: 1;
+              padding: 30px 50px;
+              overflow: hidden;
+              overflow-y: auto;
+              font-size: 16px;
+              color: #686872;
+              background: #fff;
+            }
+
+            // .answer-block {
+  //   margin-bottom: 5px;
+  // }
+          }
+
+          .other {
+            flex: 1;
+            background: #fff;
+            padding: 30px 50px;
+            overflow: hidden;
+            overflow-y: auto;
+            font-size: 16px;
+            color: #686872;
+
+            .other-answer {
+              margin-bottom: 30px;
+            }
+          }
+        }
+
+        .two-analysis {
+          position: absolute;
+          height: 100%;
+          top: 0;
+          left: 0;
+          width: 50%;
+          transition: all 0.3s;
+          transform: translateX(200%);
+        }
+
+        .two-analysis.show {
+          transform: translateX(100%);
+        }
+
+        .fixed-analysis {
+          height: 110px;
+          line-height: 20px;
+          position: absolute;
+          width: 35px;
+          padding: 5px;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          border: 1px solid #E7E7E7;
+          background: #fff;
+          z-index: 9;
+          color: #787883;
+          cursor: pointer;
+          text-align: center;
+        }
+      }
+    }
+
+    .modal {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+
+      >.mask {
+        background: #000;
+        opacity: .2;
+        width: 100%;
+        height: 100%;
+      }
+
+      .body {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+        background: #fff;
+        width: 630px;
+        color: #686872;
+        padding: 20px 30px;
+
+        .title {
+          color: #303036;
+          font-size: 20px;
+          font-weight: 600;
+        }
+
+        .desc {
+          color: #686872;
+          font-size: 16px;
+          padding: 20px 0;
+
+          .select-inline {
+            margin-bottom: 15px;
+
+            .select {
+              display: inline-block;
+            }
+          }
+
+          .label {
+            margin-bottom: 5px;
+          }
+        }
+
+        .textarea {
+          width: 570px;
+          height: 80px;
+          background: rgba(247, 247, 247, 1);
+          margin-bottom: 15px;
+          border: none;
+          padding: 5px 10px;
+        }
+
+        .textarea::placeholder {
+          color: #A7A7B7;
+        }
+
+        .bottom {
+          border-top: 1px solid #E1E1E1;
+          padding-top: 10px;
+          text-align: right;
+        }
+      }
+    }
+
+    .modal.ask-ok,
+    .modal.error-ok {
+      .body {
+        .content {
+          width: 100%;
+          padding-top: 20px;
+          padding-bottom: 40px;
+          color: #686872;
+          overflow: hidden;
+
+          .left {
+            float: left;
+            width: 360px;
+            font-size: 18px;
+
+            a {
+              padding-top: 30px;
+              display: inline-block;
+              font-size: 14px;
+            }
+          }
+
+          .right {
+            float: right;
+            text-align: right;
+            font-size: 12px;
+          }
+        }
+
+        .confirm {
+          text-align: center;
+          padding-bottom: 10px;
+
+          .answer-button.lager {
+            font-size: 16px;
+          }
+        }
+      }
+    }
+
+    .modal.note {
+      .body {
+        width: 720px;
+
+        .content {
+          padding-top: 20px;
+
+          .tabs {
+            display: inline-block;
+            width: 170px;
+            vertical-align: top;
+            margin-left: -30px;
+            margin-right: 30px;
+
+            .tab {
+              padding: 5px 0px 5px 40px;
+              line-height: 20px;
+              color: #686872;
+              margin-bottom: 30px;
+              cursor: pointer;
+              transition: all 0.3s;
+              border-top-right-radius: 25px;
+              border-bottom-right-radius: 25px;
+
+              .date {
+                font-size: 12px;
+              }
+            }
+
+            .tab.active,
+            .tab:hover {
+              color: #fff;
+              background: #4299FF;
+            }
+          }
+
+          .input {
+            display: inline-block;
+
+            .textarea {
+              width: 490px;
+              height: 350px;
+              margin-bottom: 20px;
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 1 - 0
front/project/www/routes/paper/question/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'paper-question',
   title: '查看结果',
   needLogin: true,
+  repeat: true,
   hideHeader: true,
   component() {
     return import('./page');

+ 0 - 493
front/project/www/routes/paper/question/index.less

@@ -2,497 +2,4 @@
 
 #paper-question {
   height: 100%;
-
-  .base {
-    height: 100%;
-
-    .layout {
-      background: #fff;
-      height: 100%;
-      display: flex;
-      flex-direction: row;
-
-      // &.exercise {
-      //   border-top: 20px solid #7775CA;
-      //   background: #fff;
-      // }
-
-      // &.sentence {
-      //   border-top: 20px solid #435C96;
-      //   background: #fff;
-      // }
-
-      &.examination {
-        border-top: 20px solid #8D909C;
-        background: #fff;
-
-        .layout-header {
-          height: 60px;
-          top: 20px;
-        }
-      }
-
-      .layout-header {
-        height: 60px;
-        line-height: 60px;
-        margin: 0 50px;
-        position: fixed;
-        top: 0;
-        left: 0;
-        right: 0;
-        box-shadow: 0px 4px 14px 0px rgba(189, 199, 215, 0.16);
-        text-align: center;
-        z-index: 80;
-
-        .left {
-          position: absolute;
-
-          .btn {
-            display: inline-block;
-            padding-right: 10px;
-            line-height: 60px;
-          }
-
-          .no {
-            font-size: 20px;
-            display: inline-block;
-            color: #303036;
-            font-size: 20px;
-            margin-right: 25px;
-          }
-
-          .title {
-            color: #A7A7B7;
-            display: inline-block;
-            font-size: 20px;
-
-            img {
-              margin-top: -3px;
-              margin-right: 5px;
-            }
-          }
-        }
-
-
-        .menu-wrap {
-          position: absolute;
-          right: 0;
-          text-align: left;
-          padding: 0 10px;
-          white-space: nowrap;
-
-          .menu-content {
-            position: absolute;
-            background: #fff;
-            text-align: left;
-            top: 50px;
-            right: 10px;
-            border: 1px solid #EAEDF2;
-            padding: 10px 20px;
-            min-width: 150px;
-
-            p {
-              line-height: 30px;
-              height: 30px;
-              text-align: left;
-              margin: 0;
-            }
-          }
-        }
-
-        .center {
-          position: absolute;
-          right: 50%;
-          transform: translateX(100%);
-
-          .icon {
-            margin-left: 20px;
-          }
-        }
-
-        .right {
-          position: absolute;
-          right: 0;
-
-          .b {
-            margin-left: 30px;
-
-            .s {
-              color: #4299FF;
-            }
-          }
-
-          .icon {
-            margin-left: 10px;
-          }
-        }
-      }
-    }
-
-    .layout-footer {
-      position: fixed;
-      bottom: 0;
-      left: 0;
-      right: 0;
-      height: 60px;
-      line-height: 60px;
-      box-shadow: 0px -4px 14px 0px rgba(189, 199, 215, 0.16);
-
-      .left {
-        width: 30%;
-        display: inline-block;
-        padding-left: 50px;
-      }
-
-      .right {
-        width: 30%;
-        display: inline-block;
-        text-align: right;
-        padding-right: 50px;
-
-        .icon {
-          margin-left: 10px;
-        }
-      }
-
-      .center {
-        width: 40%;
-        display: inline-block;
-        text-align: center;
-
-        .item {
-          margin: 0 10px;
-        }
-      }
-    }
-
-    .layout-body {
-      background: #fff;
-      flex: 1;
-      overflow: hidden;
-      margin: 60px 0;
-
-      .layout-content {
-        height: 100%;
-        position: relative;
-
-        .one {
-          flex: 1;
-          display: flex;
-          flex-direction: column;
-        }
-
-        .two {
-          flex: 1;
-          display: flex;
-          flex-direction: row;
-          overflow: hidden;
-          height: 100%;
-        }
-
-        .block {
-          flex: 1;
-        }
-
-        .block-content,
-        .block-answer,
-        .block-awa {
-          padding: 30px 60px;
-          color: #303036;
-          height: 100%;
-          overflow: hidden;
-          overflow-y: auto;
-        }
-
-        .block-content {
-          h2 {
-            padding: 65px 0 20px 0px;
-            font-size: 20px;
-            color: #303036;
-          }
-        }
-
-        .block-awa {
-          background: #EFF3F7;
-
-          h2 {
-            font-size: 20px;
-            color: #303036;
-            margin-top: 37px;
-            margin-bottom: 23px;
-          }
-
-          .detail {
-            .info {
-              font-weight: bold;
-              font-size: 18px;
-              color: #303036;
-
-              span.b {
-                margin-right: 80px;
-
-                .s {
-                  color: #4299FF;
-                }
-              }
-            }
-          }
-
-          .content-awa {
-            padding-top: 50px;
-            color: #686872;
-            font-size: 16px;
-          }
-
-          .show-awa {
-            font-size: 12px;
-            width: 100%;
-            height: 100%;
-            margin: 50% 0;
-            text-align: center;
-            line-height: 20px;
-            color: #A7A7B7;
-          }
-        }
-
-        .block-analysis {
-          background: #EFF3F7;
-          padding: 25px 25px 0 20px;
-          display: flex;
-          flex-direction: column;
-
-          .block-answer {
-            padding: 38px 50px;
-          }
-
-          .block {
-            background: #fff;
-          }
-
-          .detail {
-            flex: 1;
-            display: flex;
-            flex-direction: column;
-
-            .detail-block {
-              margin-top: 5px;
-              flex: 1;
-              padding: 30px 50px;
-              overflow: hidden;
-              overflow-y: auto;
-              font-size: 16px;
-              color: #686872;
-            }
-
-            .answer-block {
-              margin-bottom: 5px;
-            }
-          }
-
-          .other {
-            flex: 1;
-            background: #fff;
-            padding: 30px 50px;
-            overflow: hidden;
-            overflow-y: auto;
-            font-size: 16px;
-            color: #686872;
-
-            .other-answer {
-              margin-bottom: 30px;
-            }
-          }
-        }
-
-        .two-analysis {
-          position: absolute;
-          height: 100%;
-          top: 0;
-          left: 0;
-          width: 50%;
-          transition: all 0.3s;
-          transform: translateX(200%);
-        }
-
-        .two-analysis.show {
-          transform: translateX(100%);
-        }
-
-        .fixed-analysis {
-          height: 110px;
-          line-height: 20px;
-          position: absolute;
-          width: 35px;
-          padding: 5px;
-          right: 0;
-          top: 50%;
-          transform: translateY(-50%);
-          border: 1px solid #E7E7E7;
-          background: #fff;
-          z-index: 9;
-          color: #787883;
-          cursor: pointer;
-          text-align: center;
-        }
-      }
-    }
-
-    .modal {
-      position: fixed;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-
-      >.mask {
-        background: #000;
-        opacity: .2;
-        width: 100%;
-        height: 100%;
-      }
-
-      .body {
-        position: absolute;
-        left: 50%;
-        top: 50%;
-        transform: translate(-50%, -50%);
-        background: #fff;
-        width: 630px;
-        color: #686872;
-        padding: 20px 30px;
-
-        .title {
-          color: #303036;
-          font-size: 20px;
-          font-weight: 600;
-        }
-
-        .desc {
-          color: #686872;
-          font-size: 16px;
-          padding: 20px 0;
-
-          .select-inline {
-            margin-bottom: 15px;
-
-            .select {
-              display: inline-block;
-            }
-          }
-
-          .label {
-            margin-bottom: 5px;
-          }
-        }
-
-        .textarea {
-          width: 570px;
-          height: 80px;
-          background: rgba(247, 247, 247, 1);
-          margin-bottom: 15px;
-          border: none;
-          padding: 5px 10px;
-        }
-
-        .textarea::placeholder {
-          color: #A7A7B7;
-        }
-
-        .bottom {
-          border-top: 1px solid #E1E1E1;
-          padding-top: 10px;
-          text-align: right;
-        }
-      }
-    }
-
-    .modal.ask-ok,
-    .modal.error-ok {
-      .body {
-        .content {
-          width: 100%;
-          padding-top: 20px;
-          padding-bottom: 40px;
-          color: #686872;
-          overflow: hidden;
-
-          .left {
-            float: left;
-            width: 360px;
-            font-size: 18px;
-
-            a {
-              padding-top: 30px;
-              display: inline-block;
-              font-size: 14px;
-            }
-          }
-
-          .right {
-            float: right;
-            text-align: right;
-            font-size: 12px;
-          }
-        }
-
-        .confirm {
-          text-align: center;
-          padding-bottom: 10px;
-
-          .answer-button.lager {
-            font-size: 16px;
-          }
-        }
-      }
-    }
-
-    .modal.note {
-      .body {
-        width: 720px;
-
-        .content {
-          padding-top: 20px;
-
-          .tabs {
-            display: inline-block;
-            width: 170px;
-            vertical-align: top;
-            margin-left: -30px;
-            margin-right: 30px;
-
-            .tab {
-              padding: 5px 0px 5px 40px;
-              line-height: 20px;
-              color: #686872;
-              margin-bottom: 30px;
-              cursor: pointer;
-              transition: all 0.3s;
-              border-top-right-radius: 25px;
-              border-bottom-right-radius: 25px;
-
-              .date {
-                font-size: 12px;
-              }
-            }
-
-            .tab.active,
-            .tab:hover {
-              color: #fff;
-              background: #4299FF;
-            }
-          }
-
-          .input {
-            display: inline-block;
-
-            .textarea {
-              width: 490px;
-              height: 350px;
-              margin-bottom: 20px;
-            }
-          }
-        }
-      }
-    }
-  }
 }

+ 11 - 572
front/project/www/routes/paper/question/page.js

@@ -1,27 +1,10 @@
 import React from 'react';
-import ReactDOM from 'react-dom';
-import { Carousel, Tooltip } from 'antd';
-import { Link } from 'react-router-dom';
-import Fullscreen from 'react-fullscreen-crossbrowser';
 import './index.less';
 import Page from '@src/containers/Page';
-import { formatSeconds, formatPercent, formatDate, sortListWithOrder } from '@src/services/Tools';
-import Assets from '@src/components/Assets';
-import Navigation from '../../../components/Navigation';
-import Tabs from '../../../components/Tabs';
-import Icon from '../../../components/Icon';
-import Switch from '../../../components/Switch';
-import Select from '../../../components/Select';
-import { Button } from '../../../components/Button';
-import AnswerSelect from '../../../components/AnswerSelect';
-import AnswerList from '../../../components/AnswerList';
-import AnswerButton from '../../../components/AnswerButton';
-import AnswerTable from '../../../components/AnswerTable';
-import OtherAnswer from '../../../components/OtherAnswer';
+import { sortListWithOrder } from '@src/services/Tools';
 import { AskTarget } from '../../../../Constant';
 import { Question } from '../../../stores/question';
-import { My } from '../../../stores/my';
-import Sentence from '../process/sentence';
+import Detail from './detail';
 
 export default class extends Page {
   initState() {
@@ -64,8 +47,9 @@ export default class extends Page {
 
   initData() {
     const { id } = this.params;
+    const { search } = this.state;
     Question.getDetailById(id).then(userQuestion => {
-      const { question, questionNos, paper, note, report, setting } = userQuestion;
+      const { question, questionNos, paper, note, report, setting, questionStatus } = userQuestion;
       paper.paperModule = 'examination';
       let { questionNo } = userQuestion;
       if (!questionNo) ([questionNo] = questionNos);
@@ -94,563 +78,18 @@ export default class extends Page {
           });
         });
       }
-      this.setState({ userQuestion, question, questionNo, note, paper, questionNos });
-    });
-  }
-
-  prevQuestion() {
-    const { userQuestion } = this.state;
-    if (userQuestion.no === 1) return;
-    Question.getDetailByNo(userQuestion.reportId, userQuestion.no - 1).then((r) => {
-      linkTo(`/paper/question/${r.id}`);
-    });
-  }
-
-  nextQuestion() {
-    const { userQuestion } = this.state;
-    if (userQuestion.questionNumber === userQuestion.no) return;
-    Question.getDetailByNo(userQuestion.reportId, userQuestion.no + 1).then((r) => {
-      linkTo(`/paper/question/${r.id}`);
-    });
-  }
-
-  submitAsk() {
-    const { question = {}, questionNo = {}, paper = {}, ask = {} } = this.state;
-    if (ask.originContent === '' || ask.content === '' || ask.target === '') return;
-    My.addQuestionAsk(paper.id, ask.target, question.questionModule, questionNo.id, ask.originContent, ask.content).then(() => {
-      this.setState({ askModal: false, askOkModal: true });
-    }).catch(err => {
-      this.setState({ askError: err.message });
-    });
-  }
-
-  submitFeedbackError() {
-    const { feedback = {}, userQuestion = {}, questionNo = {} } = this.state;
-    if (feedback.originContent === '' || feedback.content === '' || feedback.target === '') return;
-    My.addFeedbackErrorQuestion(userQuestion.questionModule, questionNo.id, questionNo.title, feedback.target, feedback.originContent, feedback.content).then(() => {
-      this.setState({ feedbackModal: false, feedbackOkModal: true });
-    }).catch(err => {
-      this.setState({ feedbackError: err.message });
-    });
-  }
-
-  submitNote(close) {
-    const { userQuestion = {}, questionNo = {}, note = {} } = this.state;
-    My.updateQuestionNote(userQuestion.questionModule, questionNo.id, note).then(() => {
-      if (close) this.setState({ noteModal: false });
-    }).catch(err => {
-      this.setState({ noteError: err.message });
-    });
-  }
-
-  toggleFullscreen() {
-    const { isFullscreenEnabled } = this.state;
-    this.setState({ isFullscreenEnabled: !isFullscreenEnabled });
-  }
-
-  toggleCollect() {
-    const { userQuestion = {}, questionNo = {} } = this.state;
-    if (!userQuestion.collect) {
-      My.addQuestionCollect(userQuestion.questionModule, questionNo.id).then(() => {
-        userQuestion.collect = true;
-        this.setState({ userQuestion });
-      });
-    } else {
-      My.delQuestionCollect(userQuestion.questionModule, questionNo.id).then(() => {
-        userQuestion.collect = false;
-        this.setState({ userQuestion });
-      });
-    }
-  }
-
-  formatStem(text) {
-    if (!text) return '';
-    const { showAnswer, question = { content: {} }, userQuestion } = this.state;
-    const { table = {}, questions = [] } = question.content;
-    text = text.replace(/#select#/g, "<span class='#select#' />");
-    text = text.replace(/#table#/g, "<span class='#table#' />");
-    setTimeout(() => {
-      const selectList = document.getElementsByClassName('#select#');
-      const tableList = document.getElementsByClassName('#table#');
-      for (let i = 0; i < selectList.length; i += 1) {
-        if (!questions[i]) break;
-        ReactDOM.render(
-          <AnswerSelect
-            list={questions[i].select}
-            type={'single'}
-            selected={(userQuestion.userAnswer || { questions: [] }).questions[i]}
-            answer={(question.answer || { questions: [] }).questions[i]}
-            fix
-            show={showAnswer} />,
-          selectList[i],
-        );
-      }
-      if (table.row && table.col && table.header) {
-        const columns = table.header.map((title, index) => {
-          return { title, key: index };
-        });
-        for (let i = 0; i < tableList.length; i += 1) {
-          ReactDOM.render(<AnswerTable list={columns} columns={columns} data={table.data} />, tableList[i]);
-        }
+      // 只显示单个提问
+      if (search.askId) {
+        const askId = Number(search.askId);
+        userQuestion.asks = (userQuestion.asks || []).filter(row => row.askId === askId);
       }
-    }, 1);
-    return text;
+      this.setState({ userQuestion, question, questionNo, note, paper, questionNos, questionStatus });
+    });
   }
 
   renderView() {
     return (
-      <Fullscreen
-        enabled={this.state.isFullscreenEnabled}
-        onChange={isFullscreenEnabled => this.setState({ isFullscreenEnabled })}
-      >
-        {this.renderDetail()}
-      </Fullscreen>
-    );
-  }
-
-  renderDetail() {
-    const { report = {} } = this.state;
-    switch (report.paperModule) {
-      case 'sentence':
-        return <Sentence {...this.state} flow={this} scene='answer' mode='question' />;
-      default:
-        return <div className='base'>{this.renderBase()}</div>;
-    }
-  }
-
-  renderHeader() {
-    const { userQuestion = {}, questionNo = {}, paper = {}, report = {}, showIds, questionNos = [], question = {} } = this.state;
-    return <div className={'layout-header'}>
-      <div className="left">
-        {paper.paperModule && paper.paperModule !== 'examination' && <div className="btn"><Button radius onClick={() => {
-          linkTo(`/paper/report/${report.id}`);
-        }}>返回练习报告</Button></div>}
-        {paper.paperModule && paper.paperModule === 'examination' && <div className="btn"><Button radius onClick={() => {
-          linkTo(`/paper/report/${report.id}`);
-        }}>返回成绩单</Button></div>}
-        <div className="no">No.{userQuestion.stageNo || userQuestion.no}</div>
-        <div className="title"><Assets name='book' />{paper.title}</div>
-      </div>
-      <div className="center">
-        <div className="menu-wrap">
-          ID:{questionNo.title}
-          {questionNos && questionNos.length > 0 && <Icon name="more" onClick={() => {
-            this.setState({ showIds: true });
-          }} />}
-          {showIds && <div className='menu-content'>
-            <p>题源汇总</p>
-            {(questionNos || []).map((row) => <p>ID:{row.title}</p>)}
-          </div>}
-        </div>
-      </div>
-      <div className="right" hidden={question.questionType === 'awa'}>
-        <span className="b" hidden={!userQuestion.id}>
-          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
-          {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
-        </span>
-        <span className="b">
-          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
-          {/* 全站:<span className="s">1</span>m<span className="s">39</span>s */}
-        </span>
-        <span className="b">
-          <span className="s">{formatPercent(questionNo.totalCorrect, questionNo.totalNumber)}</span>%
-        </span>
-        <Icon name="question" />
-        <Icon name="star" active={userQuestion.collect} onClick={() => this.toggleCollect()} />
-      </div>
-    </div>;
-  }
-
-  renderBase() {
-    const { questionStatus, userQuestion = {}, paper = {}, showIds } = this.state;
-    return <div className={`layout ${paper.paperModule}`} onClick={() => {
-      if (showIds) this.setState({ showIds: false });
-    }}>
-      {this.renderHeader()}
-      <div className="layout-body">{this.renderBody()}</div>
-      <div className="layout-footer">
-        <div className="left">
-          <Tooltip overlayClassName='gray' placement='top' title='全屏'>
-            <a>
-              <Icon name={this.state.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => this.toggleFullscreen()} />
-            </a>
-          </Tooltip>
-        </div>
-        <div className="center">
-          <AnswerButton className="item" onClick={() => this.setState({ noteModal: true })}>笔记</AnswerButton>
-          {questionStatus >= 0 && <AnswerButton className="item" onClick={() => {
-            if (questionStatus > 0) {
-              this.setState({ askModal: true });
-            } else {
-              this.setState({ askFailModal: true });
-            }
-          }}>提问</AnswerButton>}
-          <AnswerButton className="item" onClick={() => this.setState({ feedbackModal: true })}>纠错</AnswerButton>
-        </div>
-        <div className="right">
-          {userQuestion.no !== 1 && <Icon name="prev" onClick={() => this.prevQuestion()} />}
-          {userQuestion.questionNumber !== userQuestion.no && <Icon name="next" onClick={() => this.nextQuestion()} />}
-        </div>
-      </div>
-      {this.state.askModal && this.renderAsk()}
-      {this.state.askOkModal && this.renderAskOk()}
-      {this.state.askFailModal && this.renderAskFail()}
-      {this.state.feedbackModal && this.renderFeedbackError()}
-      {this.state.feedbackOkModal && this.renderFeedbackErrorOk()}
-      {this.state.noteModal && this.renderNote()}
-    </div>;
-  }
-
-  renderBody() {
-    const { question = { content: {} } } = this.state;
-    const { typeset = 'one' } = question.content;
-    const { hideAnalysis } = this.state;
-    const show = typeset === 'one' ? true : !hideAnalysis;
-    return (
-      <div className="layout-content">
-        <div className='two'>
-          {this.renderContent()}
-          {question.questionType !== 'awa' && this.renderAnswer()}
-          {question.questionType === 'awa' && this.renderAWA()}
-        </div>
-        {question.questionType !== 'awa' && this.renderAnalysis()}
-        {typeset === 'two' && question.questionType !== 'awa' && (
-          <div className="fixed-analysis" onClick={() => this.setState({ hideAnalysis: !hideAnalysis })}>
-            {show ? '收起解析 >' : '查看解析 <'}
-          </div>
-        )}
-      </div>
-    );
-  }
-
-  renderAnalysis() {
-    const { question = { content: {} }, analysisTab } = this.state;
-    const { typeset = 'one' } = question.content;
-    const { hideAnalysis } = this.state;
-    const show = typeset === 'one' ? true : !hideAnalysis;
-    return (
-      <div className={`block block-analysis two-analysis ${show ? 'show' : ''}`}>
-        <Tabs
-          type="division"
-          active={analysisTab}
-          space={2}
-          tabs={[
-            { key: 'official', name: '官方解析' },
-            { key: 'qx', name: '千行解析' },
-            { key: 'association', name: '题源联想' },
-            { key: 'qa', name: '相关回答' },
-          ]}
-          onChange={(key) => {
-            this.setState({ analysisTab: key });
-          }}
-        />
-        <div className="detail">
-          {typeset === 'two' && this.renderAnswer()}
-          {this.renderText()}
-        </div>
-      </div>
-    );
-  }
-
-  renderText() {
-    const { analysisTab, question = {}, userQuestion = {} } = this.state;
-    const { asks = [], associations = [] } = userQuestion;
-    let content;
-    switch (analysisTab) {
-      case 'official':
-        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.officialContent }} />;
-        break;
-      case 'qx':
-        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.qxContent }} />;
-        break;
-      case 'association':
-        content = <div className="detail-block">
-          <Carousel>
-            {associations.map(association => {
-              return <div className="text-block" dangerouslySetInnerHTML={{ __html: association.stem }} />;
-            })}
-          </Carousel>
-        </div>;
-        break;
-      case 'qa':
-        content = <div className="detail-block answer-block">
-          {asks.map((ask, index) => {
-            return <OtherAnswer key={index} data={ask} />;
-          })}
-        </div>;
-        break;
-      default:
-        break;
-    }
-    return content;
-  }
-
-  renderAnswer() {
-    const { question = { content: {} }, showAnswer, userQuestion = {} } = this.state;
-    const { questions = [], type, typeset = 'one' } = question.content;
-    return <div className="block block-answer">
-      {typeset === 'two' ? <Switch checked={showAnswer} onChange={(value) => {
-        this.setState({ showAnswer: value });
-      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
-      {questions.map((item, index) => {
-        return (
-          <div>
-            <div className="text m-b-2">{item.description}</div>
-            <AnswerList
-              show={showAnswer}
-              selected={(userQuestion.userAnswer || { questions: [] }).questions[index]}
-              answer={(question.answer || { questions: [] }).questions[index]}
-              distributed={(question.answerDistributed || { questions: [] }).questions[index]}
-              list={item.select}
-              type={type}
-              first={item.first}
-              second={item.second}
-              direction={item.direction}
-            />
-          </div>
-        );
-      })}
-    </div>;
-  }
-
-  renderContent() {
-    const { question = { content: {} }, showAnswer, step } = this.state;
-    const { typeset = 'one' } = question.content;
-    const { steps = [] } = question.content;
-    return (
-      <div className="block block-content">
-        {typeset === 'one' && question.questionType !== 'awa' ? <Switch checked={showAnswer} onChange={(value) => {
-          this.setState({ showAnswer: value });
-        }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
-        {question.questionType === 'awa' && <h2>Analytical Writing Assessment</h2>}
-        {steps.length > 0 && <Navigation theme='detail' list={question.content.steps} active={step} onChange={(v) => this.setState({ step: v })} />}
-        <div className="text" style={{ height: 2000 }} dangerouslySetInnerHTML={{ __html: this.formatStem(steps.length > 0 ? steps[step].stem : question.stem) }} />
-      </div>
-    );
-  }
-
-  renderAWA() {
-    const { showAnswer, userQuestion = { detail: {}, userAnswer: {} } } = this.state;
-    return <div className="block block-awa">
-      <Switch checked={showAnswer} onChange={(value) => {
-        this.setState({ showAnswer: value });
-      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch>
-      <div className="body">
-        <h2>Your Response</h2>
-        {showAnswer && <div className='detail'>
-          <div className='info'>
-            <span className="b">
-              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
-              {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
-            </span>
-            <span className="b">
-              单词数:<span className="s">{Number((userQuestion.detail || {}).words || 0)}</span>词
-            </span>
-          </div>
-          <div className='content-awa' dangerouslySetInnerHTML={{ __html: userQuestion.userAnswer.awa || '' }} />
-        </div>}
-        {!showAnswer && <div className='show-awa'>选择「显示答案」查看自己的作文</div>}
-      </div>
-    </div>;
-  }
-
-  renderAsk() {
-    const { ask = {} } = this.state;
-    return (
-      <div className="modal ask">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">提问</div>
-          <div className="desc">
-            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={ask.target} list={AskTarget} onChange={(item) => {
-              ask.target = item.value;
-              this.setState({ ask });
-            }} />进行提问</div>
-            <div className="label">有疑问的具体内容是:</div>
-            <textarea className="textarea" value={ask.originContent} placeholder="请复制粘贴有疑问的内容。" onChange={(e) => {
-              ask.originContent = e.target.value;
-              this.setState({ ask });
-            }} />
-            <div className="label">针对以上内容的问题是:</div>
-            <textarea className="textarea" value={ask.content} placeholder="提问频率高的问题会被优先回答哦。" onChange={(e) => {
-              ask.content = e.target.value;
-              this.setState({ ask });
-            }} />
-          </div>
-          <div className="bottom">
-            <AnswerButton theme="cancel" size="lager" onClick={() => this.setState({ askModal: false })}>
-              取消
-            </AnswerButton>
-            <AnswerButton size="lager" onClick={() => this.submitAsk()}>提交</AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderAskOk() {
-    return (
-      <div className="modal ask-ok">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">提问</div>
-          <div className="content">
-            <div className="left">
-              <div className="text">已提交成功!</div>
-              <div className="text">关注公众号,老师回答后会立即收到通知。</div>
-              <div className="text">我们也会通过站内信的方式通知你。</div>
-              <div className="small">成为学员享受极速答疑特权。<Link>了解更多</Link></div>
-            </div>
-            <div className="right">
-              <div className="text">扫码关注公众号</div>
-              <div className="text">千行GMAT</div>
-            </div>
-          </div>
-          <div className="confirm">
-            <AnswerButton size="lager" theme="confirm" onClick={() => {
-              this.setState({ askOkModal: false });
-            }}>
-              好的,知道了
-            </AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderAskFail() {
-    return (
-      <div className="modal ask-ok">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">提问</div>
-          <div className="content">
-            <div className="left">
-              <div className="text">提问功能正在维护中。</div>
-              <div className="text">可先查阅“相关问答” 或 成为学员享受极速 答疑特权。</div>
-              <Link to="/">了解更多></Link>
-            </div>
-            <div className="right">
-              <div className="text">扫码关注公众号</div>
-              <div className="text">千行GMAT</div>
-            </div>
-          </div>
-          <div className="confirm">
-            <AnswerButton size="lager" theme="confirm" onClick={() => {
-              this.setState({ askFailModal: false });
-            }}>
-              好的,知道了
-            </AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderFeedbackError() {
-    const { feedback = {} } = this.state;
-    return (
-      <div className="modal error">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">纠错</div>
-          <div className="desc">
-            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={feedback.target} list={AskTarget} onChange={(item) => {
-              feedback.target = item.value;
-              this.setState({ feedback });
-            }} />进行提问</div>
-            <div className="label">错误内容是:</div>
-            <textarea className="textarea" value={feedback.originContent} placeholder="你可以适当扩大复制范围以使我们准确定位,感谢。" />
-            <div className="label">应该改为:</div>
-            <textarea className="textarea" placeholder="只需提供正确内容即可" />
-          </div>
-          <div className="bottom">
-            <AnswerButton theme="cancel" size="lager" onClick={() => {
-              this.setState({ feedbackModal: false });
-            }}>
-              取消
-            </AnswerButton>
-            <AnswerButton size="lager" onClick={() => {
-              this.submitFeedbackError();
-            }}>提交</AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderFeedbackErrorOk() {
-    return (
-      <div className="modal error-ok">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">纠错</div>
-          <div className="content">
-            <div className="left">
-              <div className="text"><Assets name='right' svg />已提交成功!</div>
-              <div className="text">感谢您的耐心反馈,我们会尽快核实并以站内信的方式告知结果。</div>
-              <div className="text">您也可以关注公众号及时获取结果。</div>
-            </div>
-            <div className="right">
-              <div className="text">扫码关注公众号</div>
-              <div className="text">千行GMAT</div>
-            </div>
-          </div>
-          <div className="confirm">
-            <AnswerButton size="lager" theme="confirm" onClick={() => {
-              this.setState({ feedbackOkModal: false });
-            }}>
-              好的,知道了
-            </AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderNote() {
-    const { noteField, note = {} } = this.state;
-    return (
-      <div className="modal note">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">笔记</div>
-          <div className="content">
-            <div className="tabs">
-              {AskTarget.map(item => {
-                return (
-                  <div className={`tab ${noteField === item.key ? 'active' : ''}`} onClick={() => {
-                    this.setState({ noteField: item.key });
-                  }}>
-                    <div className="text">{item.label}</div>
-                    <div className="date">{note[`${item.key}Time`] ? formatDate(note[`${item.key}Time`]) : ''}</div>
-                  </div>
-                );
-              })}
-            </div>
-            <div className="input">
-              <textarea className="textarea" value={note[`${noteField}Content`] || ''} placeholder="记下笔记,方便以后复习" onChange={(e) => {
-                note[`${noteField}Time`] = new Date();
-                note[`${noteField}Content`] = e.target.value;
-                this.setState({ note });
-              }} />
-              <div className="bottom">
-                <AnswerButton theme="cancel" size="lager" onClick={() => {
-                  this.setState({ noteModal: false });
-                }}>
-                  取消
-                </AnswerButton>
-                <AnswerButton size="lager" onClick={() => {
-                  this.submitNote();
-                }}>编辑</AnswerButton>
-                <AnswerButton size="lager" onClick={() => {
-                  this.submitNote(true);
-                }}>保存</AnswerButton>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
+      <Detail {...this.state} detail flow={this} />
     );
   }
 }

+ 1 - 0
front/project/www/routes/paper/report/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'paper-report',
   title: '报告',
   needLogin: true,
+  repeat: true,
   hideHeader: true,
   component() {
     return import('./page');

+ 1 - 0
front/project/www/routes/question/detail/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'question-detail',
   title: '题目详情',
   needLogin: true,
+  repeat: true,
   hideHeader: true,
   component() {
     return import('./page');

+ 8 - 452
front/project/www/routes/question/detail/index.less

@@ -1,468 +1,24 @@
 @charset "utf-8";
 
-#paper-question {
+#question-detail {
   height: 100%;
 
   .base {
-    height: 100%;
-
     .layout {
-      background: #fff;
-      height: 100%;
-      display: flex;
-      flex-direction: row;
 
       .layout-header {
-        height: 60px;
-        line-height: 60px;
-        margin: 0 50px;
-        position: fixed;
-        top: 0;
-        left: 0;
-        right: 0;
-        box-shadow: 0px 4px 14px 0px rgba(189, 199, 215, 0.16);
-        text-align: center;
-        z-index: 80;
-
-        .left {
-          position: absolute;
-
-          .no {
-            font-size: 20px;
-            display: inline-block;
-            color: #303036;
-            font-size: 20px;
-            margin-right: 25px;
-          }
-
-          .title {
-            color: #A7A7B7;
-            display: inline-block;
-            font-size: 20px;
-
-            img {
-              margin-top: -3px;
-              margin-right: 5px;
-            }
-          }
-        }
-
-
-        .menu-wrap {
-          position: absolute;
-          right: 0;
-          text-align: left;
-          padding: 0 10px;
-          white-space: nowrap;
-
-          .menu-content {
-            position: absolute;
-            background: #fff;
-            text-align: left;
-            top: 50px;
-            right: 10px;
-            border: 1px solid #EAEDF2;
-            padding: 10px 20px;
-            min-width: 150px;
-
-            p {
-              line-height: 30px;
-              height: 30px;
-              text-align: left;
-              margin: 0;
-            }
-          }
-        }
-
         .center {
-          position: absolute;
-          right: 50%;
-          transform: translateX(100%);
-
-          .icon {
-            margin-left: 20px;
-          }
-        }
-
-        .right {
-          position: absolute;
-          right: 0;
-
-          .b {
-            margin-left: 30px;
-
-            .s {
-              color: #4299FF;
-            }
-          }
-
-          .icon {
-            margin-left: 10px;
-          }
-        }
-      }
-    }
-
-    .layout-footer {
-      position: fixed;
-      bottom: 0;
-      left: 0;
-      right: 0;
-      height: 60px;
-      line-height: 60px;
-      box-shadow: 0px -4px 14px 0px rgba(189, 199, 215, 0.16);
-
-      .left {
-        width: 30%;
-        display: inline-block;
-        padding-left: 50px;
-      }
-
-      .right {
-        width: 30%;
-        display: inline-block;
-        text-align: right;
-        padding-right: 50px;
-
-        .icon {
-          margin-left: 10px;
-        }
-      }
-
-      .center {
-        width: 40%;
-        display: inline-block;
-        text-align: center;
-
-        .item {
-          margin: 0 10px;
-        }
-      }
-    }
-
-    .layout-body {
-      background: #fff;
-      flex: 1;
-      overflow: hidden;
-      margin: 60px 0;
-
-      .layout-content {
-        height: 100%;
-        position: relative;
-
-        .one {
-          flex: 1;
-          display: flex;
-          flex-direction: column;
-        }
-
-        .two {
-          flex: 1;
-          display: flex;
-          flex-direction: row;
-          overflow: hidden;
-          height: 100%;
-        }
-
-        .block {
-          flex: 1;
-        }
-
-        .block-content,
-        .block-answer,
-        .block-awa {
-          padding: 30px 60px;
-          color: #303036;
-          height: 100%;
-          overflow: hidden;
-          overflow-y: auto;
-        }
-
-        .block-content {
-          h2 {
-            padding: 65px 0 20px 0px;
-            font-size: 20px;
-            color: #303036;
-          }
-        }
-
-        .block-awa {
-          background: #EFF3F7;
-
-          h2 {
-            font-size: 20px;
-            color: #303036;
-            margin-top: 37px;
-            margin-bottom: 23px;
-          }
-
-          .detail {
-            .info {
-              font-weight: bold;
-              font-size: 18px;
-              color: #303036;
-
-              span.b {
-                margin-right: 80px;
-
-                .s {
-                  color: #4299FF;
-                }
-              }
-            }
-          }
-
-          .content-awa {
-            padding-top: 50px;
-            color: #686872;
-            font-size: 16px;
-          }
-
-          .show-awa {
-            font-size: 12px;
-            width: 100%;
-            height: 100%;
-            margin: 50% 0;
-            text-align: center;
-            line-height: 20px;
-            color: #A7A7B7;
-          }
-        }
+          left: 50px;
+          right: auto !important;
 
-        .block-analysis {
-          background: #EFF3F7;
-          padding: 25px 25px 0 20px;
-          display: flex;
-          flex-direction: column;
-
-          .block-answer {
-            padding: 38px 50px;
-          }
-
-          .block {
-            background: #fff;
-          }
-
-          .detail {
-            flex: 1;
-            display: flex;
-            flex-direction: column;
-
-            .detail-block {
-              margin-top: 5px;
-              flex: 1;
-              padding: 30px 50px;
-              overflow: hidden;
-              overflow-y: auto;
-              font-size: 16px;
-              color: #686872;
-            }
-
-            .answer-block {
-              margin-bottom: 5px;
-            }
+          .menu-wrap {
+            left: 0px;
           }
 
-          .other {
-            flex: 1;
-            background: #fff;
-            padding: 30px 50px;
-            overflow: hidden;
-            overflow-y: auto;
-            font-size: 16px;
-            color: #686872;
-
-            .other-answer {
-              margin-bottom: 30px;
-            }
-          }
-        }
-
-        .two-analysis {
-          position: absolute;
-          height: 100%;
-          top: 0;
-          left: 0;
-          width: 50%;
-          transition: all 0.3s;
-          transform: translateX(200%);
-        }
-
-        .two-analysis.show {
-          transform: translateX(100%);
-        }
-
-        .fixed-analysis {
-          height: 110px;
-          line-height: 20px;
-          position: absolute;
-          width: 35px;
-          padding: 5px;
-          right: 0;
-          top: 50%;
-          transform: translateY(-50%);
-          border: 1px solid #E7E7E7;
-          background: #fff;
-          z-index: 9;
-          color: #787883;
-          cursor: pointer;
-          text-align: center;
-        }
-      }
-    }
-
-    .modal {
-      position: fixed;
-      top: 0;
-      left: 0;
-      right: 0;
-      bottom: 0;
-
-      >.mask {
-        background: #000;
-        opacity: .2;
-        width: 100%;
-        height: 100%;
-      }
-
-      .body {
-        position: absolute;
-        left: 50%;
-        top: 50%;
-        transform: translate(-50%, -50%);
-        background: #fff;
-        width: 630px;
-        color: #686872;
-        padding: 20px 30px;
-
-        .title {
-          color: #303036;
-          font-size: 20px;
-          font-weight: 600;
-        }
-
-        .desc {
-          color: #686872;
-          font-size: 16px;
-          padding: 20px 0;
-
-          .select-inline {
-            margin-bottom: 15px;
-
-            .select {
-              display: inline-block;
-            }
-          }
-
-          .label {
-            margin-bottom: 5px;
-          }
-        }
-
-        .textarea {
-          width: 570px;
-          height: 80px;
-          background: rgba(247, 247, 247, 1);
-          margin-bottom: 15px;
-          border: none;
-          padding: 5px 10px;
-        }
-
-        .textarea::placeholder {
-          color: #A7A7B7;
-        }
-
-        .bottom {
-          border-top: 1px solid #E1E1E1;
-          padding-top: 10px;
-          text-align: right;
-        }
-      }
-    }
-
-    .modal.ask-ok,
-    .modal.error-ok {
-      .body {
-        .content {
-          width: 100%;
-          padding-top: 20px;
-          padding-bottom: 40px;
-          color: #686872;
-          overflow: hidden;
-
-          .left {
-            float: left;
-            width: 360px;
-            font-size: 18px;
-
-            a {
-              padding-top: 30px;
-              display: inline-block;
-              font-size: 14px;
-            }
-          }
-
-          .right {
-            float: right;
-            text-align: right;
-            font-size: 12px;
-          }
-        }
-
-        .confirm {
-          text-align: center;
-          padding-bottom: 10px;
-
-          .answer-button.lager {
-            font-size: 16px;
-          }
-        }
-      }
-    }
-
-    .modal.note {
-      .body {
-        width: 720px;
-
-        .content {
-          padding-top: 20px;
-
-          .tabs {
-            display: inline-block;
-            width: 170px;
-            vertical-align: top;
-            margin-left: -30px;
-            margin-right: 30px;
-
-            .tab {
-              padding: 5px 0px 5px 40px;
-              line-height: 20px;
-              color: #686872;
-              margin-bottom: 30px;
+          .menu-content {
+            left: 10px;
+            p{
               cursor: pointer;
-              transition: all 0.3s;
-              border-top-right-radius: 25px;
-              border-bottom-right-radius: 25px;
-
-              .date {
-                font-size: 12px;
-              }
-            }
-
-            .tab.active,
-            .tab:hover {
-              color: #fff;
-              background: #4299FF;
-            }
-          }
-
-          .input {
-            display: inline-block;
-
-            .textarea {
-              width: 490px;
-              height: 350px;
-              margin-bottom: 20px;
             }
           }
         }

+ 7 - 601
front/project/www/routes/question/detail/page.js

@@ -1,621 +1,27 @@
 import React from 'react';
-import ReactDOM from 'react-dom';
-import { Carousel, Tooltip } from 'antd';
-import { Link } from 'react-router-dom';
-import Fullscreen from 'react-fullscreen-crossbrowser';
 import './index.less';
 import Page from '@src/containers/Page';
-import { formatSeconds, formatPercent, formatDate, sortListWithOrder } from '@src/services/Tools';
-import Assets from '@src/components/Assets';
-import Navigation from '../../../components/Navigation';
-import Tabs from '../../../components/Tabs';
-import Icon from '../../../components/Icon';
-import Switch from '../../../components/Switch';
-import Select from '../../../components/Select';
-import AnswerSelect from '../../../components/AnswerSelect';
-import AnswerList from '../../../components/AnswerList';
-import AnswerButton from '../../../components/AnswerButton';
-import AnswerTable from '../../../components/AnswerTable';
-import OtherAnswer from '../../../components/OtherAnswer';
-import { AskTarget } from '../../../../Constant';
 import { Question } from '../../../stores/question';
-import { My } from '../../../stores/my';
-import Sentence from '../../paper/process/sentence';
+import Detail from '../../paper/question/detail';
 
 export default class extends Page {
-  initState() {
-    return {
-      step: 0,
-      hideAnalysis: true,
-      analysisTab: 'official',
-      showAnswer: false,
-      noteField: AskTarget[0].key,
-      showIds: false,
-    };
-  }
-
   initData() {
     const { id } = this.params;
-    Question.getDetailById(id).then(userQuestion => {
-      const { question, questionNos, paper, note, report, setting } = userQuestion;
-      let { questionNo } = userQuestion;
+    Question.getInfoById(id).then(userQuestion => {
+      const { question, questionNos, note, questionStatus } = userQuestion;
+      let { questionNo, paper } = userQuestion;
       if (!questionNo) ([questionNo] = questionNos);
       if (!question.answer) question.answer = { questions: [] };
       if (!question.answerDistributed) question.answerDistributed = { questions: [] };
       if (!userQuestion.userAnswer) userQuestion.userAnswer = { questions: [] };
-      if ((report.setting || {}).disorder) {
-        const { content } = question;
-        // 还原做题顺序
-        content.questions.forEach((q, i) => {
-          q.select = sortListWithOrder(question.select, setting.questions[i]);
-        });
-        question.answer.questions.forEach((q, i) => {
-          Object.keys(q).forEach((k) => {
-            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
-          });
-        });
-        question.answerDistributed.questions.forEach((q, i) => {
-          Object.keys(q).forEach((k) => {
-            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
-          });
-        });
-        userQuestion.userAnswer.questions.forEach((q, i) => {
-          Object.keys(q).forEach((k) => {
-            if (q[k]) q[k] = sortListWithOrder(q[k], setting.questions[i]);
-          });
-        });
-      }
-      this.setState({ userQuestion, question, questionNo, note, paper, questionNos });
-    });
-  }
-
-  prevQuestion() {
-    const { userQuestion } = this.state;
-    if (userQuestion.no === 1) return;
-    Question.getDetailByNo(userQuestion.reportId, userQuestion.no - 1).then((r) => {
-      linkTo(`/paper/question/${r.id}`);
-    });
-  }
-
-  nextQuestion() {
-    const { userQuestion } = this.state;
-    if (userQuestion.questionNumber === userQuestion.no) return;
-    Question.getDetailByNo(userQuestion.reportId, userQuestion.no + 1).then((r) => {
-      linkTo(`/paper/question/${r.id}`);
-    });
-  }
-
-  submitAsk() {
-    const { question = {}, questionNo = {}, paper = {}, ask = {} } = this.state;
-    if (ask.originContent === '' || ask.content === '' || ask.target === '') return;
-    My.addQuestionAsk(paper.id, ask.target, question.questionModule, questionNo.id, ask.originContent, ask.content).then(() => {
-      this.setState({ askModal: false, askOkModal: true });
-    }).catch(err => {
-      this.setState({ askError: err.message });
-    });
-  }
-
-  submitFeedbackError() {
-    const { feedback = {}, question = {}, questionNo = {} } = this.state;
-    if (feedback.originContent === '' || feedback.content === '' || feedback.target === '') return;
-    My.addFeedbackErrorQuestion(question.questionModule, questionNo.id, questionNo.title, feedback.target, feedback.originContent, feedback.content).then(() => {
-      this.setState({ feedbackModal: false, feedbackOkModal: true });
-    }).catch(err => {
-      this.setState({ feedbackError: err.message });
+      if (!paper) paper = {};
+      this.setState({ userQuestion, question, questionNo, note, paper, questionNos, questionStatus });
     });
   }
 
-  submitNote(close) {
-    const { question = {}, questionNo = {}, note = {} } = this.state;
-    My.updateQuestionNote(question.questionModule, questionNo.id, note).then(() => {
-      if (close) this.setState({ noteModal: false });
-    }).catch(err => {
-      this.setState({ noteError: err.message });
-    });
-  }
-
-  toggleFullscreen() {
-    const { isFullscreenEnabled } = this.state;
-    this.setState({ isFullscreenEnabled: !isFullscreenEnabled });
-  }
-
-  toggleCollect() {
-    const { userQuestion = {}, question = {}, questionNo = {} } = this.state;
-    if (!userQuestion.collect) {
-      My.addQuestionCollect(question.questionModule, questionNo.id).then(() => {
-        userQuestion.collect = true;
-        this.setState({ userQuestion });
-      });
-    } else {
-      My.delQuestionCollect(question.questionModule, questionNo.id).then(() => {
-        userQuestion.collect = false;
-        this.setState({ userQuestion });
-      });
-    }
-  }
-
-  formatStem(text) {
-    if (!text) return '';
-    const { showAnswer, question = { content: {} }, userQuestion } = this.state;
-    const { table = {}, questions = [] } = question.content;
-    text = text.replace(/#select#/g, "<span class='#select#' />");
-    text = text.replace(/#table#/g, "<span class='#table#' />");
-    setTimeout(() => {
-      const selectList = document.getElementsByClassName('#select#');
-      const tableList = document.getElementsByClassName('#table#');
-      for (let i = 0; i < selectList.length; i += 1) {
-        if (!questions[i]) break;
-        ReactDOM.render(
-          <AnswerSelect
-            list={questions[i].select}
-            type={'single'}
-            selected={(userQuestion.userAnswer || { questions: [] }).questions[i]}
-            answer={(question.answer || { questions: [] }).questions[i]}
-            fix
-            show={showAnswer} />,
-          selectList[i],
-        );
-      }
-      if (table.row && table.col && table.header) {
-        const columns = table.header.map((title, index) => {
-          return { title, key: index };
-        });
-        for (let i = 0; i < tableList.length; i += 1) {
-          ReactDOM.render(<AnswerTable list={columns} columns={columns} data={table.data} />, tableList[i]);
-        }
-      }
-    }, 1);
-    return text;
-  }
-
   renderView() {
     return (
-      <Fullscreen
-        enabled={this.state.isFullscreenEnabled}
-        onChange={isFullscreenEnabled => this.setState({ isFullscreenEnabled })}
-      >
-        {this.renderDetail()}
-      </Fullscreen>
-    );
-  }
-
-  renderDetail() {
-    const { report = {} } = this.state;
-    switch (report.paperModule) {
-      case 'sentence':
-        return <Sentence {...this.state} flow={this} scene='answer' mode='question' />;
-      default:
-        return <div className='base'>{this.renderBase()}</div>;
-    }
-  }
-
-  renderHeader() {
-    const { userQuestion = {}, questionNo = {}, paper = {}, showIds, questionNos = [], question = {} } = this.state;
-    return <div className="layout-header">
-      <div className="left">
-        <div className="no">No.{userQuestion.stageNo || userQuestion.no}</div>
-        <div className="title"><Assets name='book' />{paper.title}</div>
-      </div>
-      <div className="center">
-        <div className="menu-wrap">
-          ID:{questionNo.title}
-          {questionNos && questionNos.length > 0 && <Icon name="more" onClick={() => {
-            this.setState({ showIds: true });
-          }} />}
-          {showIds && <div className='menu-content'>
-            <p>题源汇总</p>
-            {(questionNos || []).map((row) => <p>ID:{row.title}</p>)}
-          </div>}
-        </div>
-      </div>
-      <div className="right" hidden={question.questionType === 'awa'}>
-        <span className="b" hidden={!userQuestion.id}>
-          用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
-          {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
-        </span>
-        <span className="b">
-          全站:<span dangerouslySetInnerHTML={{ __html: formatSeconds(questionNo.totalTime / questionNo.totalNumber).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
-          {/* 全站:<span className="s">1</span>m<span className="s">39</span>s */}
-        </span>
-        <span className="b">
-          <span className="s">{formatPercent(questionNo.totalCorrect, questionNo.totalNumber)}</span>%
-        </span>
-        <Icon name="question" />
-        <Icon name="star" active={userQuestion.collect} onClick={() => this.toggleCollect()} />
-      </div>
-    </div>;
-  }
-
-  renderBase() {
-    const { questionStatus, userQuestion = {}, showIds } = this.state;
-    return <div className="layout" onClick={() => {
-      if (showIds) this.setState({ showIds: false });
-    }}>
-      {this.renderHeader()}
-      <div className="layout-body">{this.renderBody()}</div>
-      <div className="layout-footer">
-        <div className="left">
-          <Tooltip overlayClassName='gray' placement='top' title='全屏'>
-            <a>
-              <Icon name={this.state.isFullscreenEnabled ? 'sceen-restore' : 'sceen-full'} onClick={() => this.toggleFullscreen()} />
-            </a>
-          </Tooltip>
-        </div>
-        <div className="center">
-          <AnswerButton className="item" onClick={() => this.setState({ noteModal: true })}>笔记</AnswerButton>
-          {questionStatus >= 0 && <AnswerButton className="item" onClick={() => {
-            if (questionStatus > 0) {
-              this.setState({ askModal: true });
-            } else {
-              this.setState({ askFailModal: true });
-            }
-          }}>提问</AnswerButton>}
-          <AnswerButton className="item" onClick={() => this.setState({ feedbackModal: true })}>纠错</AnswerButton>
-        </div>
-        <div className="right">
-          {userQuestion.no !== 1 && <Icon name="prev" onClick={() => this.prevQuestion()} />}
-          {userQuestion.questionNumber !== userQuestion.no && <Icon name="next" onClick={() => this.nextQuestion()} />}
-        </div>
-      </div>
-      {this.state.askModal && this.renderAsk()}
-      {this.state.askOkModal && this.renderAskOk()}
-      {this.state.askFailModal && this.renderAskFail()}
-      {this.state.feedbackModal && this.renderFeedbackError()}
-      {this.state.feedbackOkModal && this.renderFeedbackErrorOk()}
-      {this.state.noteModal && this.renderNote()}
-    </div>;
-  }
-
-  renderBody() {
-    const { question = { content: {} } } = this.state;
-    const { typeset = 'one' } = question.content;
-    const { hideAnalysis } = this.state;
-    const show = typeset === 'one' ? true : !hideAnalysis;
-    return (
-      <div className="layout-content">
-        <div className='two'>
-          {this.renderContent()}
-          {question.questionType !== 'awa' && this.renderAnswer()}
-          {question.questionType === 'awa' && this.renderAWA()}
-        </div>
-        {question.questionType !== 'awa' && this.renderAnalysis()}
-        {typeset === 'two' && question.questionType !== 'awa' && (
-          <div className="fixed-analysis" onClick={() => this.setState({ hideAnalysis: !hideAnalysis })}>
-            {show ? '收起解析 >' : '查看解析 <'}
-          </div>
-        )}
-      </div>
-    );
-  }
-
-  renderAnalysis() {
-    const { question = { content: {} }, analysisTab } = this.state;
-    const { typeset = 'one' } = question.content;
-    const { hideAnalysis } = this.state;
-    const show = typeset === 'one' ? true : !hideAnalysis;
-    return (
-      <div className={`block block-analysis two-analysis ${show ? 'show' : ''}`}>
-        <Tabs
-          type="division"
-          active={analysisTab}
-          space={2}
-          tabs={[
-            { key: 'official', name: '官方解析' },
-            { key: 'qx', name: '千行解析' },
-            { key: 'association', name: '题源联想' },
-            { key: 'qa', name: '相关回答' },
-          ]}
-          onChange={(key) => {
-            this.setState({ analysisTab: key });
-          }}
-        />
-        <div className="detail">
-          {typeset === 'two' && this.renderAnswer()}
-          {this.renderText()}
-        </div>
-      </div>
-    );
-  }
-
-  renderText() {
-    const { analysisTab, question = {}, userQuestion = {} } = this.state;
-    const { asks = [], associations = [] } = userQuestion;
-    let content;
-    switch (analysisTab) {
-      case 'official':
-        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.officialContent }} />;
-        break;
-      case 'qx':
-        content = <div className="detail-block text-block" dangerouslySetInnerHTML={{ __html: question.qxContent }} />;
-        break;
-      case 'association':
-        content = <div className="detail-block">
-          <Carousel>
-            {associations.map(association => {
-              return <div className="text-block" dangerouslySetInnerHTML={{ __html: association.stem }} />;
-            })}
-          </Carousel>
-        </div>;
-        break;
-      case 'qa':
-        content = <div className="detail-block answer-block">
-          {asks.map((ask, index) => {
-            return <OtherAnswer key={index} data={ask} />;
-          })}
-        </div>;
-        break;
-      default:
-        break;
-    }
-    return content;
-  }
-
-  renderAnswer() {
-    const { question = { content: {} }, showAnswer, userQuestion = {} } = this.state;
-    const { questions = [], type, typeset = 'one' } = question.content;
-    return <div className="block block-answer">
-      {typeset === 'two' ? <Switch checked={showAnswer} onChange={(value) => {
-        this.setState({ showAnswer: value });
-      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
-      {questions.map((item, index) => {
-        return (
-          <div>
-            <div className="text m-b-2">{item.description}</div>
-            <AnswerList
-              show={showAnswer}
-              selected={(userQuestion.userAnswer || { questions: [] }).questions[index]}
-              answer={(question.answer || { questions: [] }).questions[index]}
-              distributed={(question.answerDistributed || { questions: [] }).questions[index]}
-              list={item.select}
-              type={type}
-              first={item.first}
-              second={item.second}
-              direction={item.direction}
-            />
-          </div>
-        );
-      })}
-    </div>;
-  }
-
-  renderContent() {
-    const { question = { content: {} }, showAnswer, step } = this.state;
-    const { typeset = 'one' } = question.content;
-    const { steps = [] } = question.content;
-    return (
-      <div className="block block-content">
-        {typeset === 'one' && question.questionType !== 'awa' ? <Switch checked={showAnswer} onChange={(value) => {
-          this.setState({ showAnswer: value });
-        }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch> : ''}
-        {question.questionType === 'awa' && <h2>Analytical Writing Assessment</h2>}
-        {steps.length > 0 && <Navigation theme='detail' list={question.content.steps} active={step} onChange={(v) => this.setState({ step: v })} />}
-        <div className="text" style={{ height: 2000 }} dangerouslySetInnerHTML={{ __html: this.formatStem(steps.length > 0 ? steps[step].stem : question.stem) }} />
-      </div>
-    );
-  }
-
-  renderAWA() {
-    const { showAnswer, userQuestion = { detail: {}, userAnswer: {} } } = this.state;
-    return <div className="block block-awa">
-      <Switch checked={showAnswer} onChange={(value) => {
-        this.setState({ showAnswer: value });
-      }}>{showAnswer ? '显示答案' : '关闭答案'}</Switch>
-      <div className="body">
-        <h2>Your Response</h2>
-        {showAnswer && <div className='detail'>
-          <div className='info'>
-            <span className="b">
-              用时:<span dangerouslySetInnerHTML={{ __html: formatSeconds(userQuestion.userTime).replace(/([0-9]+)(m|min|h|hour|s)/g, '<span class="s">$1</span>$2') }} />
-              {/* 用时:<span className="s">1</span>m<span className="s">39</span>s */}
-            </span>
-            <span className="b">
-              单词数:<span className="s">{Number((userQuestion.detail || {}).words || 0)}</span>词
-            </span>
-          </div>
-          <div className='content-awa' dangerouslySetInnerHTML={{ __html: userQuestion.userAnswer.awa || '' }} />
-        </div>}
-        {!showAnswer && <div className='show-awa'>选择「显示答案」查看自己的作文</div>}
-      </div>
-    </div>;
-  }
-
-  renderAsk() {
-    const { ask = {} } = this.state;
-    return (
-      <div className="modal ask">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">提问</div>
-          <div className="desc">
-            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={ask.target} list={AskTarget} onChange={(item) => {
-              ask.target = item.value;
-              this.setState({ ask });
-            }} />进行提问</div>
-            <div className="label">有疑问的具体内容是:</div>
-            <textarea className="textarea" value={ask.originContent} placeholder="请复制粘贴有疑问的内容。" onChange={(e) => {
-              ask.originContent = e.target.value;
-              this.setState({ ask });
-            }} />
-            <div className="label">针对以上内容的问题是:</div>
-            <textarea className="textarea" value={ask.content} placeholder="提问频率高的问题会被优先回答哦。" onChange={(e) => {
-              ask.content = e.target.value;
-              this.setState({ ask });
-            }} />
-          </div>
-          <div className="bottom">
-            <AnswerButton theme="cancel" size="lager" onClick={() => this.setState({ askModal: false })}>
-              取消
-            </AnswerButton>
-            <AnswerButton size="lager" onClick={() => this.submitAsk()}>提交</AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderAskOk() {
-    return (
-      <div className="modal ask-ok">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">提问</div>
-          <div className="content">
-            <div className="left">
-              <div className="text">已提交成功!</div>
-              <div className="text">关注公众号,老师回答后会立即收到通知。</div>
-              <div className="text">我们也会通过站内信的方式通知你。</div>
-              <div className="small">成为学员享受极速答疑特权。<Link>了解更多</Link></div>
-            </div>
-            <div className="right">
-              <div className="text">扫码关注公众号</div>
-              <div className="text">千行GMAT</div>
-            </div>
-          </div>
-          <div className="confirm">
-            <AnswerButton size="lager" theme="confirm" onClick={() => {
-              this.setState({ askOkModal: false });
-            }}>
-              好的,知道了
-            </AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderAskFail() {
-    return (
-      <div className="modal ask-ok">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">提问</div>
-          <div className="content">
-            <div className="left">
-              <div className="text">提问功能正在维护中。</div>
-              <div className="text">可先查阅“相关问答” 或 成为学员享受极速 答疑特权。</div>
-              <Link to="/">了解更多></Link>
-            </div>
-            <div className="right">
-              <div className="text">扫码关注公众号</div>
-              <div className="text">千行GMAT</div>
-            </div>
-          </div>
-          <div className="confirm">
-            <AnswerButton size="lager" theme="confirm" onClick={() => {
-              this.setState({ askFailModal: false });
-            }}>
-              好的,知道了
-            </AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderFeedbackError() {
-    const { feedback = {} } = this.state;
-    return (
-      <div className="modal error">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">纠错</div>
-          <div className="desc">
-            <div className="select-inline">我想对<Select excludeSelf size="small" theme="white" value={feedback.target} list={AskTarget} onChange={(item) => {
-              feedback.target = item.value;
-              this.setState({ feedback });
-            }} />进行提问</div>
-            <div className="label">错误内容是:</div>
-            <textarea className="textarea" value={feedback.originContent} placeholder="你可以适当扩大复制范围以使我们准确定位,感谢。" />
-            <div className="label">应该改为:</div>
-            <textarea className="textarea" placeholder="只需提供正确内容即可" />
-          </div>
-          <div className="bottom">
-            <AnswerButton theme="cancel" size="lager" onClick={() => {
-              this.setState({ feedbackModal: false });
-            }}>
-              取消
-            </AnswerButton>
-            <AnswerButton size="lager" onClick={() => {
-              this.submitFeedbackError();
-            }}>提交</AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderFeedbackErrorOk() {
-    return (
-      <div className="modal error-ok">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">纠错</div>
-          <div className="content">
-            <div className="left">
-              <div className="text"><Assets name='right' svg />已提交成功!</div>
-              <div className="text">感谢您的耐心反馈,我们会尽快核实并以站内信的方式告知结果。</div>
-              <div className="text">您也可以关注公众号及时获取结果。</div>
-            </div>
-            <div className="right">
-              <div className="text">扫码关注公众号</div>
-              <div className="text">千行GMAT</div>
-            </div>
-          </div>
-          <div className="confirm">
-            <AnswerButton size="lager" theme="confirm" onClick={() => {
-              this.setState({ feedbackOkModal: false });
-            }}>
-              好的,知道了
-            </AnswerButton>
-          </div>
-        </div>
-      </div>
-    );
-  }
-
-  renderNote() {
-    const { noteField, note = {} } = this.state;
-    return (
-      <div className="modal note">
-        <div className="mask" />
-        <div className="body">
-          <div className="title">笔记</div>
-          <div className="content">
-            <div className="tabs">
-              {AskTarget.map(item => {
-                return (
-                  <div className={`tab ${noteField === item.key ? 'active' : ''}`} onClick={() => {
-                    this.setState({ noteField: item.key });
-                  }}>
-                    <div className="text">{item.label}</div>
-                    <div className="date">{note[`${item.key}Time`] ? formatDate(note[`${item.key}Time`]) : ''}</div>
-                  </div>
-                );
-              })}
-            </div>
-            <div className="input">
-              <textarea className="textarea" value={note[`${noteField}Content`] || ''} placeholder="记下笔记,方便以后复习" onChange={(e) => {
-                note[`${noteField}Time`] = new Date();
-                note[`${noteField}Content`] = e.target.value;
-                this.setState({ note });
-              }} />
-              <div className="bottom">
-                <AnswerButton theme="cancel" size="lager" onClick={() => {
-                  this.setState({ noteModal: false });
-                }}>
-                  取消
-                </AnswerButton>
-                <AnswerButton size="lager" onClick={() => {
-                  this.submitNote();
-                }}>编辑</AnswerButton>
-                <AnswerButton size="lager" onClick={() => {
-                  this.submitNote(true);
-                }}>保存</AnswerButton>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
+      <Detail {...this.state} info flow={this} />
     );
   }
 }

+ 1 - 0
front/project/www/routes/sentence/read/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'sentence-read',
   title: '长难句阅读',
   needLogin: true,
+  repeat: true,
   hideHeader: true,
   component() {
     return import('./page');

+ 1 - 0
front/project/www/routes/textbook/list/index.js

@@ -3,6 +3,7 @@ export default {
   key: 'textbook-list',
   title: '数学机经',
   needLogin: false,
+  repeat: true,
   tab: 'examination',
   component() {
     return import('./page');

+ 27 - 8
front/project/www/stores/course.js

@@ -16,6 +16,14 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/simple', { courseId });
   }
 
+  noProgress(courseId, courseNoId, progress, time, currentCourseNoId) {
+    return this.apiPut('/course/no/progress', { courseId, courseNoId, progress, time, currentCourseNoId });
+  }
+
+  trailView(id) {
+    return this.apiPost('/course/trail/view', { id });
+  }
+
   listPackage(params) {
     return this.apiGet('/course/package/list', params);
   }
@@ -32,6 +40,10 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/data/detail', { dataId });
   }
 
+  dataView(id) {
+    return this.apiPost('/course/data/view', { id });
+  }
+
   historyData(dataId) {
     return this.apiGet('/course/data/history', { dataId });
   }
@@ -40,6 +52,14 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/experience/list', { page, size, perpareStatus, experienceDay, experienceScore, experiencePercent, order, direction });
   }
 
+  getExperience(experienceId) {
+    return this.apiGet('/course/experience/detail', { experienceId });
+  }
+
+  experienceView(id) {
+    return this.apiPost('/course/experience/view', { id });
+  }
+
   /**
    * 获取课程进度
    */
@@ -47,6 +67,13 @@ export default class CourseStore extends BaseStore {
     return this.apiGet('/course/progress', { courseModule, structId, courseId });
   }
 
+  /**
+   * 课程记录信息
+   * @param {*} recordId
+   */
+  record(recordId) {
+    return this.apiGet('/course/record', { recordId });
+  }
 
   /**
    * 获取预习作业列表
@@ -55,14 +82,6 @@ export default class CourseStore extends BaseStore {
   listPreview({ page, size, recordId, finish, endTime }) {
     return this.apiGet('/course/preview/list', { page, size, recordId, endTime, times: finish });
   }
-
-  /**
-   * 课程记录信息
-   * @param {*} recordId
-   */
-  record(recordId) {
-    return this.apiGet('/course/record', { recordId });
-  }
 }
 
 export const Course = new CourseStore({ key: 'course' });

+ 9 - 1
front/project/www/stores/main.js

@@ -148,9 +148,17 @@ export default class MainStore extends BaseStore {
     return this.apiGet('/base/ready_info');
   }
 
-  listRead(page, size, plate) {
+  listRead({ page, size, plate }) {
     return this.apiGet('/base/read/list', { page, size, plate });
   }
+
+  listRoom({ page, size, keyword, areaId }) {
+    return this.apiGet('/base/room/list', { page, size, keyword, areaId });
+  }
+
+  allData(isOfficial) {
+    return this.apiGet('/base/data/all', { isOfficial });
+  }
 }
 
 export const Main = new MainStore({ key: 'main' });

+ 2 - 2
front/project/www/stores/order.js

@@ -42,8 +42,8 @@ export default class OrderStore extends BaseStore {
    * 获取所有已购记录
    * @param {*} param0
    */
-  listRecord({ page, size }) {
-    return this.apiGet('/my/record/list', { page, size });
+  listRecord({ page, size, productType, productId, service, isUsed }) {
+    return this.apiGet('/my/record/list', { page, size, productType, productId, service, isUsed });
   }
 
   /**

+ 3 - 3
front/project/www/stores/question.js

@@ -23,10 +23,10 @@ export default class QuestionStore extends BaseStore {
 
   /**
    * 通过题目Id获取详情
-   * @param {*} questionId
+   * @param {*} questionNoId
    */
-  getInfoById(questionId) {
-    return this.apiGet('/question/info', { questionId });
+  getInfoById(questionNoId) {
+    return this.apiGet('/question/info', { questionNoId });
   }
 
   /**

+ 8 - 0
front/project/www/stores/textbook.js

@@ -37,6 +37,14 @@ export default class TextbookStore extends BaseStore {
   subscribe(subscribe) {
     return this.apiPost('/textbook/subscribe', { subscribe });
   }
+
+  enroll(month) {
+    return this.apiPost('/textbook/enroll', { month });
+  }
+
+  listEnroll(year) {
+    return this.apiGet('/textbook/enroll/list', { year });
+  }
 }
 
 export const Textbook = new TextbookStore({ key: 'textbook' });

+ 1 - 1
front/src/components/Assets/index.less

@@ -1,6 +1,6 @@
 .assets {
   display: inline-block;
-  vertical-align: top;
+  vertical-align: middle;
   border: none;
   max-width: 100%;
   max-height: 100%;

+ 2 - 2
front/src/containers/Async.js

@@ -1,5 +1,5 @@
 import React, { Component } from 'react';
-import { createForm } from 'rc-form';
+import { Form } from 'antd';
 
 export default class extends Component {
   constructor(props) {
@@ -12,7 +12,7 @@ export default class extends Component {
       this.props
         .component()
         .then(({ default: component }) => {
-          this.setState({ C: this.props.isForm ? createForm()(component) : component });
+          this.setState({ C: this.props.isForm ? Form.create()(component) : component });
         })
         .catch(err => {
           console.log('async error: ', err);

+ 24 - 19
front/src/services/AsyncTools.js

@@ -37,10 +37,33 @@ function getAsyncAntd() {
   return import('../components/AsyncAntd');
 }
 
+export function asyncSMessage(title, type = 'success') {
+  return getAsyncAntd().then(({ Message }) => {
+    switch (type) {
+      case 'warn':
+      case 'warning':
+        Message.warning(title);
+        break;
+      case 'error':
+        Message.error(title);
+        break;
+      case 'success':
+      default:
+        Message.success(title);
+        break;
+    }
+  });
+}
+
 export function asyncModalConfirm(props, cb) {
   const params = {
     onOk: () => {
-      cb();
+      const result = cb();
+      if (result) {
+        result.catch((err) => {
+          asyncSMessage(err.message, 'error');
+        });
+      }
     },
     destroyOnClose: true,
   };
@@ -56,21 +79,3 @@ export function asyncConfirm(title, content, cb) {
 export function asyncDelConfirm(title, content, cb) {
   return asyncModalConfirm({ title, content, okType: 'danger', maskClosable: true }, cb);
 }
-
-export function asyncSMessage(title, type = 'success') {
-  return getAsyncAntd().then(({ Message }) => {
-    switch (type) {
-      case 'warn':
-      case 'warning':
-        Message.warning(title);
-        break;
-      case 'error':
-        Message.error(title);
-        break;
-      case 'success':
-      default:
-        Message.success(title);
-        break;
-    }
-  });
-}

+ 0 - 0
server/data/src/main/java/com/qxgmat/data/constants/enums/status/AskStatus.java


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