1
0
Просмотр исходного кода

feat(front): 题库题目详情

Go 5 лет назад
Родитель
Сommit
2d06cd298c
28 измененных файлов с 1511 добавлено и 2160 удалено
  1. 1 1
      front/project/Constant.js
  2. 1 1
      front/project/h5/routes/page/bind/page.js
  3. 1 1
      front/project/h5/routes/page/id/page.js
  4. 1 1
      front/project/www/components/AnswerList/index.js
  5. 1 1
      front/project/www/components/AnswerTable/index.less
  6. 2 1
      front/project/www/routes/index.js
  7. 607 0
      front/project/www/routes/paper/question/detail/index.js
  8. 500 0
      front/project/www/routes/paper/question/detail/index.less
  9. 0 493
      front/project/www/routes/paper/question/index.less
  10. 11 572
      front/project/www/routes/paper/question/page.js
  11. 8 452
      front/project/www/routes/question/detail/index.less
  12. 7 601
      front/project/www/routes/question/detail/page.js
  13. 27 8
      front/project/www/stores/course.js
  14. 9 1
      front/project/www/stores/main.js
  15. 2 2
      front/project/www/stores/order.js
  16. 3 3
      front/project/www/stores/question.js
  17. 28 3
      server/gateway-api/src/main/java/com/qxgmat/controller/api/BaseController.java
  18. 54 3
      server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java
  19. 5 5
      server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java
  20. 19 8
      server/gateway-api/src/main/java/com/qxgmat/controller/api/TextbookController.java
  21. 13 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/CourseDataViewDto.java
  22. 53 0
      server/gateway-api/src/main/java/com/qxgmat/dto/request/UserCourseNoProgressDto.java
  23. 11 0
      server/gateway-api/src/main/java/com/qxgmat/dto/response/CourseDetailDto.java
  24. 36 0
      server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java
  25. 27 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/ReadyRoomService.java
  26. 14 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/TextbookQuestionService.java
  27. 45 0
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserCourseProgressService.java
  28. 25 3
      server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderRecordService.java

+ 1 - 1
front/project/Constant.js

@@ -115,7 +115,7 @@ export const MessageCategory = [
   { label: '预习作业提醒', value: 'preview_notice', params: [''] },
   { label: '支付成功提醒', value: 'payed', params: [''] },
   { label: '资料更新', value: 'data_update', params: [''] },
-  { label: '题目提问回复', value: 'ask_auestion', params: [''] },
+  { label: '题目提问回复', value: 'ask_question', params: [''] },
   { label: '课程提问回复', value: 'ask_course', params: [''] },
   { label: '咨询回复', value: 'faq_callback', params: [''] },
   { label: '纠错回复', value: 'feedback_callback', params: [''] },

+ 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) {

+ 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() {

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

+ 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.change({ userQuestion });
+      });
+    } else {
+      My.delQuestionCollect(userQuestion.questionModule, questionNo.id).then(() => {
+        userQuestion.collect = true;
+        flow.change({ 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;
+            }
+          }
+        }
+      }
+    }
+  }
+}

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

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

+ 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 });
   }
 
   /**

+ 28 - 3
server/gateway-api/src/main/java/com/qxgmat/controller/api/BaseController.java

@@ -68,6 +68,15 @@ public class BaseController {
     private ReadyDataService readyDataService;
 
     @Autowired
+    private ReadyRoomService readyRoomService;
+
+    @Autowired
+    private ReadyArticleCategoryService readyArticleCategoryService;
+
+    @Autowired
+    private ReadyRoomAreaService readyRoomAreaService;
+
+    @Autowired
     private UsersService usersService;
 
     @RequestMapping(value = "/index", method = RequestMethod.GET)
@@ -274,11 +283,13 @@ public class BaseController {
         Setting entity = settingService.getByKey(SettingKey.READY_READ);
         JSONObject info = entity.getValue();
 
-        // todo
+        List<ReadyArticleCategory> articleCategoryList = readyArticleCategoryService.all();
+        List<ReadyRoomArea> roomAreaList = readyRoomAreaService.all();
+
         JSONObject dto = new JSONObject();
         dto.put("read", info);
-        dto.put("category", new JSONArray());
-        dto.put("area", new JSONArray());
+        dto.put("category", articleCategoryList);
+        dto.put("area", roomAreaList);
         return ResponseHelp.success(dto);
     }
 
@@ -295,6 +306,20 @@ public class BaseController {
         return ResponseHelp.success(p, page, size, p.getTotal());
     }
 
+    @RequestMapping(value = "/room/list", method = RequestMethod.GET)
+    @ApiOperation(value = "考场查询", httpMethod = "GET")
+    public Response<PageMessage<ReadyRoom>> listRoom(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "100") int size,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer areaId,
+            @RequestParam(required = false, defaultValue = "id") String order,
+            @RequestParam(required = false, defaultValue = "desc") String direction,
+            HttpSession session) {
+        Page<ReadyRoom> p = readyRoomService.list(page, size, keyword, areaId, order, DirectionStatus.ValueOf(direction));
+        return ResponseHelp.success(p, page, size, p.getTotal());
+    }
+
     @RequestMapping(value = "/data/all", method = RequestMethod.GET)
     @ApiOperation(value = "资料", httpMethod = "GET")
     public Response<List<ReadyData>> allData(

+ 54 - 3
server/gateway-api/src/main/java/com/qxgmat/controller/api/CourseController.java

@@ -3,6 +3,7 @@ package com.qxgmat.controller.api;
 
 import com.github.pagehelper.Page;
 import com.nuliji.tools.*;
+import com.nuliji.tools.exception.AuthException;
 import com.nuliji.tools.exception.ParameterException;
 import com.qxgmat.data.constants.enums.ExperienceDayRange;
 import com.qxgmat.data.constants.enums.ExperienceScoreRange;
@@ -13,11 +14,14 @@ import com.qxgmat.data.constants.enums.user.DataType;
 import com.qxgmat.data.dao.entity.*;
 import com.qxgmat.data.relation.entity.UserPreviewPaperRelation;
 import com.qxgmat.dto.extend.*;
+import com.qxgmat.dto.request.CourseDataViewDto;
 import com.qxgmat.dto.request.CourseTrailViewDto;
 import com.qxgmat.dto.request.ExperienceViewDto;
+import com.qxgmat.dto.request.UserCourseNoProgressDto;
 import com.qxgmat.dto.response.*;
 import com.qxgmat.help.ShiroHelp;
 import com.qxgmat.service.UsersService;
+import com.qxgmat.service.extend.CourseExtendService;
 import com.qxgmat.service.extend.PreviewService;
 import com.qxgmat.service.inline.*;
 import io.swagger.annotations.Api;
@@ -48,6 +52,9 @@ public class CourseController {
     private CourseService courseService;
 
     @Autowired
+    private CourseNoService courseNoService;
+
+    @Autowired
     private CourseDataService courseDataService;
 
     @Autowired
@@ -66,6 +73,9 @@ public class CourseController {
     private PreviewService previewService;
 
     @Autowired
+    private CourseExtendService courseExtendService;
+
+    @Autowired
     private PreviewPaperService previewPaperService;
 
     @Autowired
@@ -78,6 +88,12 @@ public class CourseController {
     private UserCourseService userCourseService;
 
     @Autowired
+    private UserCourseProgressService userCourseProgressService;
+
+    @Autowired
+    private UserCourseRecordService userCourseRecordService;
+
+    @Autowired
     private UserOrderRecordService userOrderRecordService;
 
     @Autowired
@@ -122,6 +138,11 @@ public class CourseController {
         Course course = courseService.get(courseId);
         CourseDetailDto dto = Transform.convert(course, CourseDetailDto.class);
 
+        // 课时
+        List<CourseNo> courseNoList = courseNoService.allCourse(course.getId());
+        courseExtendService.refreshNoResource(user, course.getId(), courseNoList);
+        dto.setCourseNos(Transform.convert(courseNoList, CourseNoExtendDto.class));
+
         // 评论
         List<Comment> commentList = commentService.list(1, 10, "course-"+course.getCourseModule(), course.getId().toString());
         dto.setComments(Transform.convert(commentList, CommentExtendDto.class));
@@ -144,6 +165,27 @@ public class CourseController {
         return ResponseHelp.success(course);
     }
 
+    @RequestMapping(value = "/no/progress", method = RequestMethod.PUT)
+    @ApiOperation(value = "更新课时进度", httpMethod = "PUT")
+    public Response<Boolean> noProgress(@RequestBody @Validated UserCourseNoProgressDto dto) {
+        UserSentenceProgress entity = Transform.dtoToEntity(dto);
+        User user = (User) shiroHelp.getLoginUser();
+        if (user == null) throw new AuthException("需要登录");
+
+        // 添加课时访问记录
+        if (dto.getCurrentCourseNoId() != null && dto.getTime() != null && dto.getTime() > 0){
+            userCourseRecordService.add(UserCourseRecord.builder()
+                    .userId(user.getId())
+                    .courseId(dto.getCourseId())
+                    .courseNoId(dto.getCurrentCourseNoId())
+                    .build());
+        }
+
+        // 更新课程进度
+        userCourseProgressService.updateProgress(user.getId(), dto.getCourseId(), dto.getCourseNoId(), dto.getProgress());
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/trail/view", method = RequestMethod.POST)
     @ApiOperation(value = "课程试用", httpMethod = "POST")
     public Response<Boolean> viewTrail(@RequestBody @Validated CourseTrailViewDto dto, HttpSession session) {
@@ -237,6 +279,7 @@ public class CourseController {
         User user = (User) shiroHelp.getLoginUser();
 
         CourseData courseData = courseDataService.get(dataId);
+        courseExtendService.refreshDataResource(user, courseData);
 
         CourseDataDetailDto dto = Transform.convert(courseData, CourseDataDetailDto.class);
 
@@ -250,6 +293,13 @@ public class CourseController {
         return ResponseHelp.success(dto);
     }
 
+    @RequestMapping(value = "/data/view", method = RequestMethod.POST)
+    @ApiOperation(value = "资料查看", httpMethod = "POST")
+    public Response<Boolean> viewData(@RequestBody @Validated CourseDataViewDto dto, HttpSession session) {
+        courseDataService.accumulation(dto.getId(), 0, 1);
+        return ResponseHelp.success(true);
+    }
+
     @RequestMapping(value = "/data/history", method = RequestMethod.GET)
     @ApiOperation(value = "资料详情", httpMethod = "GET")
     public Response<PageMessage<CourseDataHistory>> historyData(
@@ -302,9 +352,9 @@ public class CourseController {
 
     @RequestMapping(value = "/experience/detail", method = RequestMethod.GET)
     @ApiOperation(value = "心经查看", httpMethod = "GET")
-    public Response<CourseExperience> detailExperience(int id, HttpSession session) {
+    public Response<CourseExperience> detailExperience(int experienceId, HttpSession session) {
 
-        CourseExperience entity = courseExperienceService.get(id);
+        CourseExperience entity = courseExperienceService.get(experienceId);
         if(entity.getUserId() > 0){
             CourseExperienceDetailDto dto = Transform.convert(entity, CourseExperienceDetailDto.class);
             User user = usersService.get(dto.getUserId());
@@ -321,7 +371,7 @@ public class CourseController {
     }
 
     @RequestMapping(value = "/record", method = RequestMethod.GET)
-    @ApiOperation(value = "获取课程记录信息", notes = "获取所有课程及状态进度", httpMethod = "GET")
+    @ApiOperation(value = "获取课程记录信息", notes = "获取课程记录信息", httpMethod = "GET")
     public Response<UserCourseDetailDto> record(
             @RequestParam(required = true) Integer recordId
     )  {
@@ -336,6 +386,7 @@ public class CourseController {
         UserCourseDetailDto dto = Transform.convert(userOrderRecord, UserCourseDetailDto.class);
         Course course = courseService.get(userOrderRecord.getProductId());
         dto.setCourse(Transform.convert(course, CourseExtendDto.class));
+
         return ResponseHelp.success(dto);
     }
 

+ 5 - 5
server/gateway-api/src/main/java/com/qxgmat/controller/api/QuestionController.java

@@ -148,12 +148,14 @@ public class QuestionController {
     @RequestMapping(value = "/info", method = RequestMethod.GET)
     @ApiOperation(value = "获取题目信息", notes = "获取题目信息", httpMethod = "GET")
     public Response<UserQuestionDetailDto> info(
-            @RequestParam(required = true) Integer questionId,
             @RequestParam(required = true) Integer questionNoId
     )  {
         User user = (User) shiroHelp.getLoginUser();
+
+        QuestionNo questionNo = questionNoService.get(questionNoId);
+
         UserQuestion userQuestion = UserQuestion.builder()
-                .questionId(questionId)
+                .questionId(questionNo.getQuestionId())
                 .questionNoId(questionNoId)
                 .questionModule(QuestionModule.BASE.key)
                 .build();
@@ -162,6 +164,7 @@ public class QuestionController {
 
         Question question = questionService.get(userQuestion.getQuestionId());
         dto.setQuestion(Transform.convert(question, QuestionDetailExtendDto.class));
+        dto.setQuestionNo(Transform.convert(questionNo, QuestionNoExtendDto.class));
 
         UserCollectQuestion collect = userCollectQuestionService.getByUserAndQuestion(user.getId(), userQuestion.getQuestionId());
         dto.setCollect(collect != null);
@@ -180,8 +183,6 @@ public class QuestionController {
 
         List<QuestionNo> questionNoList = questionNoService.listByQuestion(userQuestion.getQuestionId());
         dto.setQuestionNos(Transform.convert(questionNoList, QuestionNoExtendDto.class));
-        QuestionNo questionNo = questionNoService.get(userQuestion.getQuestionNoId());
-        dto.setQuestionNo(Transform.convert(questionNo, QuestionNoExtendDto.class));
 
         // 获取提问权限
         Integer recordId = questionFlowService.questionRelationCourse(user.getId(), null, QuestionType.ValueOf(question.getQuestionType()));
@@ -206,7 +207,6 @@ public class QuestionController {
             }
         }
 
-
         return ResponseHelp.success(dto);
     }
 

+ 19 - 8
server/gateway-api/src/main/java/com/qxgmat/controller/api/TextbookController.java

@@ -97,16 +97,27 @@ public class TextbookController
         User user = (User) shiroHelp.getLoginUser();
 
         TextbookLibrary latest = textbookLibraryService.getLatest();
-        TextbookLibrary second = textbookLibraryService.getSecond();
         List<UserTextbookGroupDto> p = new ArrayList<>(2);
 
-        for(TextbookLibrary library : new ArrayList<TextbookLibrary>(2){{add(latest);add(second);}}){
-            UserTextbookGroupDto dto = Transform.convert(library, UserTextbookGroupDto.class);
-            dto.setIsLatest(library.getEndDate() == null ? 1 : 0);
-            dto.setNeedService(library.getEndDate() == null);
-            dto.setHasService(true);
-            // 获取第三层所有题目,并获取题目统计
-            List<TextbookQuestion> list = textbookQuestionService.listByLibrary(library.getId());
+        Integer latestId = latest.getId();
+        for(TextbookLibrary library : new ArrayList<TextbookLibrary>(2){{add(latest);add(null);}}){
+            UserTextbookGroupDto dto;
+            List<TextbookQuestion> list;
+            if (library == null){
+                dto = new UserTextbookGroupDto();
+                dto.setIsLatest(0);
+                dto.setNeedService(false);
+                dto.setHasService(false);
+                // 获取往期题目统计
+                list = textbookQuestionService.listByNoLibrary(latestId);
+            }else{
+                dto = Transform.convert(library, UserTextbookGroupDto.class);
+                dto.setIsLatest(library.getEndDate() == null ? 1 : 0);
+                dto.setNeedService(library.getEndDate() == null);
+                dto.setHasService(true);
+                // 获取第三层所有题目,并获取题目统计
+                list = textbookQuestionService.listByLibrary(library.getId());
+            }
             List<TextbookQuestionRelation> relations = textbookQuestionService.relation(list);
             dto.setStat(textbookQuestionService.statPaper(list));
             dto.setQuestionNumber(list.size());

+ 13 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/CourseDataViewDto.java

@@ -0,0 +1,13 @@
+package com.qxgmat.dto.request;
+
+public class CourseDataViewDto {
+    private Integer id;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+}

+ 53 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/request/UserCourseNoProgressDto.java

@@ -0,0 +1,53 @@
+package com.qxgmat.dto.request;
+
+public class UserCourseNoProgressDto {
+    private Integer courseId;
+
+    private Integer courseNoId;
+
+    private Integer progress;
+
+    private Integer time;
+
+    private Integer currentCourseNoId;
+
+    public Integer getCourseId() {
+        return courseId;
+    }
+
+    public void setCourseId(Integer courseId) {
+        this.courseId = courseId;
+    }
+
+    public Integer getCourseNoId() {
+        return courseNoId;
+    }
+
+    public void setCourseNoId(Integer courseNoId) {
+        this.courseNoId = courseNoId;
+    }
+
+    public Integer getProgress() {
+        return progress;
+    }
+
+    public void setProgress(Integer progress) {
+        this.progress = progress;
+    }
+
+    public Integer getTime() {
+        return time;
+    }
+
+    public void setTime(Integer time) {
+        this.time = time;
+    }
+
+    public Integer getCurrentCourseNoId() {
+        return currentCourseNoId;
+    }
+
+    public void setCurrentCourseNoId(Integer currentCourseNoId) {
+        this.currentCourseNoId = currentCourseNoId;
+    }
+}

+ 11 - 0
server/gateway-api/src/main/java/com/qxgmat/dto/response/CourseDetailDto.java

@@ -3,6 +3,7 @@ package com.qxgmat.dto.response;
 import com.nuliji.tools.annotation.Dto;
 import com.qxgmat.data.dao.entity.Course;
 import com.qxgmat.dto.extend.CommentExtendDto;
+import com.qxgmat.dto.extend.CourseNoExtendDto;
 import com.qxgmat.dto.extend.FaqExtendDto;
 
 import java.math.BigDecimal;
@@ -42,6 +43,8 @@ public class CourseDetailDto extends Course {
 
     private Integer useExpireTime;
 
+    private Collection<CourseNoExtendDto> courseNos;
+
     private Collection<CommentExtendDto> comments;
 
     private Collection<FaqExtendDto> faqs;
@@ -189,4 +192,12 @@ public class CourseDetailDto extends Course {
     public void setFaqs(Collection<FaqExtendDto> faqs) {
         this.faqs = faqs;
     }
+
+    public Collection<CourseNoExtendDto> getCourseNos() {
+        return courseNos;
+    }
+
+    public void setCourseNos(Collection<CourseNoExtendDto> courseNos) {
+        this.courseNos = courseNos;
+    }
 }

+ 36 - 0
server/gateway-api/src/main/java/com/qxgmat/service/extend/CourseExtendService.java

@@ -192,4 +192,40 @@ public class CourseExtendService {
         }
         return min;
     }
+
+    /**
+     * 根据用户权限更新资源信息
+     * @param user
+     * @param courseId
+     * @param courseNoList
+     */
+    public void refreshNoResource(User user, Integer courseId, List<CourseNo> courseNoList){
+        if (user != null){
+            if (userCourseService.getCourse(user.getId(), courseId) == null){
+                for(CourseNo courseNo : courseNoList){
+                    courseNo.setResource(courseNo.getTrailResource());
+                }
+            }
+        }else{
+            for(CourseNo courseNo : courseNoList){
+                courseNo.setResource(courseNo.getTrailResource());
+            }
+        }
+    }
+
+    /**
+     * 根据用户权限更新资源信息
+     * @param user
+     * @param courseData
+     */
+    public void refreshDataResource(User user, CourseData courseData){
+        // 处理权限
+        if (user != null){
+            if (!userOrderRecordService.hasData(user.getId(), courseData.getId())){
+                courseData.setResource(courseData.getTrailResource());
+            }
+        }else{
+            courseData.setResource(courseData.getTrailResource());
+        }
+    }
 }

+ 27 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/ReadyRoomService.java

@@ -25,6 +25,33 @@ public class ReadyRoomService extends AbstractService {
     @Resource
     private ReadyRoomMapper readyRoomMapper;
 
+
+    public Page<ReadyRoom> list(int page, int size, String keyword, Integer areaId, String order, DirectionStatus direction){
+        Example example = new Example(ReadyRoom.class);
+        if (keyword != null)
+            example.and(
+                    example.createCriteria()
+                            .orLike("address", "%"+keyword+"%")
+                            .orLike("title", "%"+keyword+"%")
+                            .orLike("description", "%"+keyword+"%")
+            );
+        if (areaId != null)
+            example.and(
+                    example.createCriteria()
+                            .andEqualTo("areaId", areaId)
+            );
+        if(order == null || order.isEmpty()) order = "id";
+        switch(direction){
+            case ASC:
+                example.orderBy(order).asc();
+                break;
+            case DESC:
+            default:
+                example.orderBy(order).desc();
+        }
+        return select(readyRoomMapper, example, page, size);
+    }
+
     public Page<ReadyRoom> listAdmin(int page, int size, String position, Integer areaId, String order, DirectionStatus direction){
         Example example = new Example(ReadyRoom.class);
         if (position != null)

+ 14 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/TextbookQuestionService.java

@@ -145,6 +145,20 @@ public class TextbookQuestionService extends AbstractService {
     }
 
     /**
+     * 获取往期题目
+     * @param libraryId
+     * @return
+     */
+    public List<TextbookQuestion> listByNoLibrary(Integer libraryId){
+        Example example = new Example(TextbookQuestion.class);
+        example.and(
+                example.createCriteria()
+                        .andNotEqualTo("libraryId", libraryId)
+        );
+        return select(textbookQuestionMapper, example);
+    }
+
+    /**
      * 获取换库表中的最后一题
      * @param libraryId
      * @return

+ 45 - 0
server/gateway-api/src/main/java/com/qxgmat/service/inline/UserCourseProgressService.java

@@ -11,6 +11,7 @@ import com.qxgmat.data.dao.entity.UserCourseProgress;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.util.*;
@@ -22,6 +23,50 @@ public class UserCourseProgressService extends AbstractService {
     @Resource
     private UserCourseProgressMapper userCourseProgressMapper;
 
+    /**
+     * 更新课时进度
+     * @param userId
+     * @param courseId
+     * @param courseNoId
+     * @param progress
+     * @return
+     */
+    @Transactional
+    public UserCourseProgress updateProgress(Integer userId, Integer courseId, Integer courseNoId, Integer progress){
+        Example example = new Example(UserCourseProgress.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("courseId", courseId)
+                        .andEqualTo("courseNoId", courseNoId)
+        );
+        UserCourseProgress entity;
+        UserCourseProgress in = one(userCourseProgressMapper, example);
+        if (in != null){
+            if (in.getProgress() <= progress) {
+                in.setProgress(progress);
+                entity = edit(in);
+            }else if(in.getProgress() == 100){
+                in.setProgress(progress);
+                in.setTimes(in.getTimes() + 1);
+                entity = edit(in);
+                return entity;
+            }else{
+                entity = in;
+                // 不用更新进度
+                return entity;
+            }
+        }else{
+            entity = add(UserCourseProgress.builder()
+                    .userId(userId)
+                    .courseId(courseId)
+                    .courseNoId(courseNoId)
+                    .times(0)
+                    .progress(progress).build());
+        }
+        return entity;
+    }
+
     public List<UserCourseProgress> listCourse(Integer recordId, Integer courseId){
         Example example = new Example(UserCourseProgress.class);
         example.and(

+ 25 - 3
server/gateway-api/src/main/java/com/qxgmat/service/inline/UserOrderRecordService.java

@@ -253,20 +253,41 @@ public class UserOrderRecordService extends AbstractService {
     }
 
     /**
+     * 判断是否有资料权限
+     * @param userId
+     * @param dataId
+     * @return
+     */
+    public boolean hasData(Integer userId, Integer dataId){
+        Example example = new Example(UserOrderRecord.class);
+        example.and(
+                example.createCriteria()
+                        .andEqualTo("userId", userId)
+                        .andEqualTo("productType", ProductType.DATA.key)
+                        .andEqualTo("productId", dataId)
+        );
+        UserOrderRecord service = one(userOrderRecordMapper, example);
+        return service != null;
+    }
+
+    /**
      * 获取未使用的购买信息:根据模块
      * @param userId
-     * @param productType
+     * @param key
      * @return
      */
-    public List<UserOrderRecord> listUnUse(Integer userId, ProductType productType){
+    public List<UserOrderRecord> listUnUseService(Integer userId, ServiceKey key){
         Example example = new Example(UserOrderRecord.class);
         example.and(
                 example.createCriteria()
                         .andEqualTo("userId", userId)
-                        .andEqualTo("productType", productType.key)
+                        .andEqualTo("service", key.key)
+                        .andGreaterThanOrEqualTo("startTime", new Date())
+                        .andLessThan("endTime", new Date())
                         .andEqualTo("isUsed", 0)
                         .andEqualTo("isStop", 0)
         );
+        example.orderBy("startTime").asc();
         return select(userOrderRecordMapper, example);
     }
 
@@ -284,6 +305,7 @@ public class UserOrderRecordService extends AbstractService {
                         .andEqualTo("service", key.key)
                         .andGreaterThanOrEqualTo("startTime", new Date())
                         .andLessThan("endTime", new Date())
+                        .andEqualTo("isUsed", 0)
                         .andEqualTo("isStop", 0)
         );
         example.orderBy("startTime").asc();