index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. import React, { Component } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import './index.less';
  4. import { Checkbox, Icon as AntDIcon, Tooltip } from 'antd';
  5. import Assets from '@src/components/Assets';
  6. import { formatSeconds, formatMinuteSecond, getMap } from '@src/services/Tools';
  7. import Icon from '../../../../components/Icon';
  8. import Button from '../../../../components/Button';
  9. import Navigation from '../../../../components/Navigation';
  10. import Answer from '../../../../components/Answer';
  11. import Calculator from '../../../../components/Calculator';
  12. import AnswerSelect from '../../../../components/AnswerSelect';
  13. import AnswerTable from '../../../../components/AnswerTable';
  14. import Editor from '../../../../components/Editor';
  15. import { QuestionType, ExaminationOrder } from '../../../../../Constant';
  16. const QuestionTypeMap = getMap(QuestionType, 'value');
  17. export default class extends Component {
  18. constructor(props) {
  19. super(props);
  20. this.state = {
  21. showCalculator: false,
  22. disorder: false,
  23. order: [],
  24. step: 0,
  25. answer: {},
  26. modal: null,
  27. };
  28. }
  29. onChangeQuestion(index, value) {
  30. const { question } = this.props;
  31. const { content } = question;
  32. const { answer = {} } = this.state;
  33. const type = content.type === 'double' ? 'double' : 'single';
  34. if (!answer.questions) {
  35. answer.questions = content.questions.map(() => {
  36. return {};
  37. });
  38. }
  39. answer.questions[index] = { [type]: value };
  40. console.log(answer);
  41. this.setState({ answer });
  42. }
  43. onChangeAwa(value) {
  44. const { answer = {} } = this.state;
  45. answer.awa = value;
  46. this.setState({ answer });
  47. }
  48. showConfirm(title, desc, width, cb) {
  49. this.showModal('confirm', title, desc, width, cb);
  50. }
  51. showToast(title, desc, width, cb) {
  52. this.showModal('toast', title, desc, width, cb);
  53. }
  54. showModal(type, title, desc, width, cb) {
  55. this.setState({ modal: { type, title, desc, width, cb } });
  56. }
  57. timeOut(cb) {
  58. this.showToast('Time Expired', 'Time has expired for this section. \n Click OK to continue.', 440, cb);
  59. }
  60. checkAnswer() {
  61. const { question } = this.props;
  62. const { content } = question;
  63. const { answer } = this.state;
  64. let title = null;
  65. let desc = null;
  66. let width = null;
  67. if (question.questionType === 'awa') {
  68. if (!answer.awa) {
  69. desc = 'Please answer the question first.';
  70. }
  71. } else {
  72. let flag = false;
  73. if (!answer || !answer.questions) {
  74. flag = true;
  75. } else if (content.type === 'double') {
  76. answer.questions.forEach((row, index) => {
  77. if (flag) return;
  78. if (!row.double) {
  79. flag = true;
  80. return;
  81. }
  82. const { direction } = content.questions[index];
  83. switch (direction) {
  84. case 'landscape':
  85. flag = row.double.filter(r => (r || []).filter(rr => rr).length > 0).length < row.double.length;
  86. break;
  87. case 'portrait':
  88. flag = [0, 1].map(r => row.double.filter(rr => rr && rr[r]).length > 0) < 2;
  89. break;
  90. default:
  91. }
  92. });
  93. } else if (content.type === 'single') {
  94. answer.questions.forEach((row) => {
  95. if (flag) return;
  96. if (!row.single) {
  97. flag = true;
  98. return;
  99. }
  100. flag = row.single.filter(r => r).length === 0;
  101. });
  102. }
  103. if (flag) {
  104. title = 'Answer Required';
  105. desc = 'You can not continue with this question unanswered. ';
  106. width = 430;
  107. }
  108. }
  109. if (title || desc) return this.showToast(title, desc, width);
  110. return true;
  111. }
  112. hideModal(b) {
  113. if (b) {
  114. const { modal = {} } = this.state;
  115. if (modal.cb) modal.cb();
  116. }
  117. this.setState({ modal: null });
  118. }
  119. formatStrem(text) {
  120. if (!text) return '';
  121. const { question = { content: {} } } = this.props;
  122. const { table = {}, questions = [] } = question.content;
  123. text = text.replace(/#select#/g, "<span class='#select#' />");
  124. text = text.replace(/#table#/g, "<span class='#table#' />");
  125. setTimeout(() => {
  126. const selectList = document.getElementsByClassName('#select#');
  127. const tableList = document.getElementsByClassName('#table#');
  128. for (let i = 0; i < selectList.length; i += 1) {
  129. if (!questions[i]) break;
  130. ReactDOM.render(
  131. <AnswerSelect list={questions[i].select} type={'single'} onChange={v => this.onChangeQuestion(i, v)} />,
  132. selectList[i],
  133. );
  134. }
  135. if (table.row && table.col && table.header) {
  136. const columns = table.header.map((title, index) => {
  137. return { title, key: index };
  138. });
  139. for (let i = 0; i < tableList.length; i += 1) {
  140. ReactDOM.render(<AnswerTable list={columns} columns={columns} data={table.data} />, tableList[i]);
  141. }
  142. }
  143. }, 1);
  144. return text;
  145. }
  146. showHelp() {
  147. this.setState({ showHelp: true });
  148. }
  149. next() {
  150. const { flow } = this.props;
  151. const { answer } = this.state;
  152. if (this.checkAnswer()) {
  153. this.showConfirm('Answer Confirmation', 'Click Yes to confirm your answer and continue to the next \n question. ', 560, () => {
  154. flow.submit(answer).then(() => {
  155. return flow.next();
  156. });
  157. });
  158. }
  159. }
  160. stage() {
  161. const { flow } = this.props;
  162. flow.relaxToNextStage();
  163. }
  164. render() {
  165. const { modal, showHelp } = this.state;
  166. const { scene, paper } = this.props;
  167. let content = null;
  168. switch (scene) {
  169. case 'start':
  170. content = paper.paperModule === 'examination' ? this.renderExaminationStart() : this.renderExerciseStart();
  171. break;
  172. case 'relax':
  173. content = this.renderRelax();
  174. break;
  175. default:
  176. content = this.renderDetail();
  177. break;
  178. }
  179. return (
  180. <div id="paper-process-base">
  181. {content}
  182. {modal ? this.renderModal() : ''}
  183. {showHelp ? this.renderHelp() : ''}
  184. </div>
  185. );
  186. }
  187. renderContent() {
  188. const { question = { content: {} } } = this.props;
  189. const { step } = this.state;
  190. const { steps = [], typeset = 'one', type } = question.content;
  191. return (
  192. <div className="block block-content">
  193. {steps.length > 0 && (
  194. <Navigation
  195. theme="process"
  196. list={question.content.steps}
  197. active={step}
  198. onChange={v => this.setState({ step: v })}
  199. />
  200. )}
  201. <div
  202. className="text"
  203. style={{ paddingBottom: typeset === 'one' && type !== 'inline' ? '' : '100px' }}
  204. dangerouslySetInnerHTML={{ __html: this.formatStrem(steps.length > 0 ? steps[step].stem : question.stem) }}
  205. />
  206. </div>
  207. );
  208. }
  209. renderAnswer() {
  210. const { question = { content: {} } } = this.props;
  211. const { questions = [], type } = question.content;
  212. if (type === 'inline') return '';
  213. return (
  214. <div className="block block-answer">
  215. {question.questionType === 'awa' && <Editor onChange={v => this.onChangeAwa(v)} />}
  216. {questions.map((item, index) => {
  217. return (
  218. <div>
  219. <div className="text m-b-2" dangerouslySetInnerHTML={{ __html: item.description }} />
  220. <Answer
  221. list={item.select}
  222. type={type}
  223. first={item.first}
  224. second={item.second}
  225. direction={item.direction}
  226. onChange={v => this.onChangeQuestion(index, v)}
  227. />
  228. </div>
  229. );
  230. })}
  231. </div>
  232. );
  233. }
  234. renderDetail() {
  235. const { paper, userQuestion, question = { content: {} }, totalTime, stageTime, totalNumber, flow, showTime, showNo } = this.props;
  236. if (!userQuestion.id) return null;
  237. const { showCalculator } = this.state;
  238. const { typeset = 'one' } = question.content;
  239. return (
  240. <div className="layout">
  241. <div className="fixed">
  242. {QuestionTypeMap[question.questionType].long}
  243. {question.questionType === 'ir' && (
  244. <Assets
  245. className="calculator-icon"
  246. name="calculator_icon"
  247. onClick={() => this.setState({ showCalculator: !showCalculator })}
  248. />
  249. )}
  250. {/* <Assets className="collect-icon" name="collect_icon" onClick={() => {
  251. flow.toggleCollect();
  252. }} /> */}
  253. <div className="collect-icon">
  254. <Icon name="star" active={userQuestion.collect} onClick={() => flow.toggleCollect()} />
  255. </div>
  256. </div>
  257. <Calculator show={showCalculator} />
  258. <div className="layout-header">
  259. <div className="title">{paper.title}</div>
  260. <div className="right">
  261. <div
  262. className="block"
  263. onClick={() => {
  264. flow.switchTime();
  265. }}
  266. >
  267. <Assets name="timeleft_icon" />
  268. {showTime && stageTime && `Time left ${formatMinuteSecond(stageTime)}`}
  269. {showTime && !stageTime && totalTime && `Time cost ${formatMinuteSecond(totalTime)}`}
  270. </div>
  271. <div
  272. className="block"
  273. onClick={() => {
  274. flow.switchNo();
  275. }}
  276. >
  277. <Assets name="subjectnumber_icon" />
  278. {showNo && `${userQuestion.stageNo} of ${totalNumber}`}
  279. </div>
  280. </div>
  281. </div>
  282. <div className={'layout-body'}>
  283. <div className={typeset}>
  284. {this.renderContent()}
  285. {this.renderAnswer()}
  286. </div>
  287. </div>
  288. <div className="layout-footer">
  289. <div className="help" onClick={() => this.showHelp()}>
  290. <Assets name="help_icon" />
  291. Help
  292. </div>
  293. <div className="full">
  294. <Assets name="fullscreen_icon" onClick={() => flow.toggleFullscreen()} />
  295. </div>
  296. <div className="next" onClick={() => this.next()}>
  297. Next
  298. <Assets name="next_icon" />
  299. </div>
  300. </div>
  301. </div>
  302. );
  303. }
  304. renderExaminationStart() {
  305. // const { paper, userQuestion, singleTime, stageTime, flow } = this.props;
  306. const { paper, flow, startTime, setting, showTime } = this.props;
  307. const { disorder = true, order } = setting;
  308. return (
  309. <div className="layout">
  310. <div className="fixed" />
  311. <div className="layout-header">
  312. <div className="title">{paper.title}</div>
  313. <div className="right">
  314. <div
  315. className="block"
  316. onClick={() => {
  317. flow.switchTime();
  318. }}
  319. >
  320. <Assets name="timeleft_icon" />
  321. {showTime && startTime && `Time left ${formatMinuteSecond(startTime)}`}
  322. </div>
  323. {/* <div
  324. className="block"
  325. onClick={() => {
  326. this.setState({ showNo: !showNo });
  327. }}
  328. >
  329. <Assets name="subjectnumber_icon" />
  330. {showNo && `${userQuestion.no} of ${paper.questionNumber}`}
  331. </div> */}
  332. </div>
  333. </div>
  334. <div className={'layout-body'}>{paper.isAdapt > 1 ? this.renderExaminationStartCAT() : this.renderExaminationStartDefault()}</div>
  335. <div className="layout-footer">
  336. <div className="help" onClick={() => this.showHelp()}>
  337. <Assets name="help_icon" />
  338. Help
  339. </div>
  340. <div className="full">
  341. <Assets name="fullscreen_icon" onClick={() => flow.toggleFullscreen()} />
  342. </div>
  343. <div className="next" onClick={() => flow.start({ disorder: paper.finishTimes > 0 ? disorder : false, order: order.filter(row => row) })}>
  344. Next
  345. <Assets name="next_icon" />
  346. </div>
  347. </div>
  348. </div>
  349. );
  350. }
  351. renderExerciseStart() {
  352. const { paper, flow, setting } = this.props;
  353. const { disorder = true } = setting;
  354. return (
  355. <div className="start">
  356. <div className="bg" />
  357. <div className="fixed-content">
  358. <div className="title">{paper.title}</div>
  359. <div className="desc">
  360. <div className="block">
  361. <div className="desc-title">
  362. <Assets name="subject_icon" />
  363. 题目总数
  364. </div>
  365. <div className="desc-info">{paper.questionNumber}</div>
  366. </div>
  367. <div className="block">
  368. <div className="desc-title">
  369. <Assets name="time_icon" />
  370. 建议用时
  371. </div>
  372. <div className="desc-info">{formatSeconds(paper.time)}</div>
  373. </div>
  374. </div>
  375. {paper.finishTimes > 0 && <div className="tip">
  376. <Checkbox className="m-r-1" checked={disorder} onChange={() => flow.setSetting({ disorder: !disorder })} />
  377. 题目选项乱序显示
  378. </div>}
  379. <div className="submit">
  380. <Button size="lager" radius onClick={() => flow.start({ disorder: paper.finishTimes > 0 ? disorder : false })}>
  381. 开始练习
  382. </Button>
  383. </div>
  384. </div>
  385. </div>
  386. );
  387. }
  388. renderExaminationStartCAT() {
  389. const { paper, flow, setting } = this.props;
  390. const { disorder = true, order = [], orderIndex } = setting;
  391. return (
  392. <div className="exercise-start default">
  393. <div className="title">Section Ordering</div>
  394. <div className="desc">Select the order in which the exam sections are to be administered.</div>
  395. <div className="desc tip">
  396. NOTE: You have 1 minute to make your selection. If you do not make your selection within 1 minute, the first
  397. option listed will be selected and you will view the exam in the following order: Analytical Writing
  398. Assessment, Integrated Reasoning, Quantitative, Verbal.
  399. </div>
  400. <div className="desc">
  401. Once you select your section order, you must view ALL questions in each section, in the order you have
  402. selected, before moving on to the next section. You will NOT be able to return to this screen.
  403. </div>
  404. <div className="block-list">
  405. {ExaminationOrder.map((row, index) => {
  406. return <div className="block-item">
  407. <div className="block-title" onClick={() => {
  408. flow.setSetting({ order: row.value, orderIndex: index });
  409. }}>
  410. <div className="block-title-border">
  411. {orderIndex === index && <AntDIcon type="check" />}
  412. <span>{row.label}</span>
  413. </div>
  414. </div>
  415. {row.list.map((r, i) => {
  416. return <div className="block-text">{i + 1}.{r.label} </div>;
  417. })}
  418. </div>;
  419. })}
  420. </div>
  421. <div className="bottom">
  422. {paper.finishTimes > 0 && <div className="text">
  423. <Checkbox checked={disorder} onChange={() => flow.setSetting({ disorder: !disorder })} /> 题目选项乱序显示
  424. </div>}
  425. <div className="text">
  426. Click{' '}
  427. <div className="next" onClick={() => flow.start({ disorder: paper.finishTimes > 0 ? disorder : false, order: order.filter(row => row) })}>
  428. Next
  429. <Assets name="next_icon" />
  430. </div>{' '}
  431. button to start the exam. You will begin the GMAT exam on the next screen.{' '}
  432. </div>
  433. </div>
  434. </div>
  435. );
  436. }
  437. renderExaminationStartDefault() {
  438. const { paper, flow, setting } = this.props;
  439. const { disorder = true, order = [], orderIndex } = setting;
  440. const tmp = order.filter(row => row);
  441. return (
  442. <div className="exercise-start cat">
  443. <div className="title">Section Ordering</div>
  444. <div className="block-list">
  445. {ExaminationOrder.map((row, index) => {
  446. return <div className="block-item" onClick={() => {
  447. flow.setSetting({ order: row.value, orderIndex: index });
  448. }}>
  449. <div className={orderIndex === index ? 'block-item-body active' : 'block-item-body'}>
  450. {orderIndex === index && <AntDIcon type="check" style={{ color: '#fff' }} />}
  451. {row.list.map((r, i) => {
  452. return <div className="block-text" onClick={() => {
  453. if (order.indexOf(r.value) >= 0) {
  454. // 取消
  455. if (tmp.length > 1) {
  456. order[i] = '';
  457. }
  458. } else {
  459. // 选中
  460. order[i] = r.value;
  461. }
  462. console.log(order);
  463. flow.setSetting({ order });
  464. }}>
  465. <Checkbox checked={orderIndex === index ? order.indexOf(r.value) >= 0 : false} /> {r.label}{' '}
  466. </div>;
  467. })}
  468. </div>
  469. </div>;
  470. })}
  471. </div>
  472. <div className="bottom">
  473. {paper.finishTimes > 0 && <div className="text">
  474. <Checkbox checked={disorder} onChange={() => flow.setSetting({ disorder: !disorder })} /> 题目选项乱序显示
  475. </div>}
  476. <div className="text">
  477. Click{' '}
  478. <div className="next" onClick={() => flow.start({ disorder: paper.finishTimes > 0 ? disorder : false, order: order.filter(row => row) })}>
  479. Next
  480. <Assets name="next_icon" />
  481. </div>{' '}
  482. button to start the exam. You will begin the GMAT exam on the next screen.{' '}
  483. </div>
  484. <div className="text tip">*实战可选择考试顺序但无法选择考试内容。</div>
  485. </div>
  486. </div>
  487. );
  488. }
  489. renderRelax() {
  490. const { paper, stageTime, flow, showTime } = this.props;
  491. return (
  492. <div className="layout">
  493. <div className="layout-header">
  494. <div className="title">{paper.title}</div>
  495. <div className="right">
  496. <div
  497. className="block"
  498. onClick={() => {
  499. flow.switchTime();
  500. }}
  501. >
  502. <Assets name="timeleft_icon" />
  503. {showTime && stageTime && `Time left ${formatMinuteSecond(stageTime)}`}
  504. {/* {showTime && singleTime && `Time cost ${formatMinuteSecond(singleTime)}`} */}
  505. </div>
  506. {/* <div
  507. className="block"
  508. onClick={() => {
  509. this.setState({ showNo: !showNo });
  510. }}
  511. >
  512. <Assets name="subjectnumber_icon" />
  513. {showNo && `${userQuestion.no} of ${paper.questionNumber}`}
  514. </div> */}
  515. </div>
  516. </div>
  517. <div className={'layout-body'}>
  518. <div className="relax">
  519. <div className="title">
  520. Optional Break <Tooltip title={'点击「Next」可继续考试'} trigger="click"><Icon name="question" /></Tooltip>
  521. </div>
  522. <div className="time" dangerouslySetInnerHTML={{ __html: formatMinuteSecond(stageTime).split(':').map(row => row.replace(/([0-9])/g, '<div class="block">$1</div>')).join('<div class="div">:</div>') }} />
  523. </div>
  524. </div>
  525. <div className="layout-footer">
  526. <div className="help" onClick={() => this.showHelp()}>
  527. <Assets name="help_icon" />
  528. Help
  529. </div>
  530. <div className="full">
  531. <Assets name="fullscreen_icon" onClick={() => flow.toggleFullscreen()} />
  532. </div>
  533. <div className="next" onClick={() => this.stage()}>
  534. Next
  535. <Assets name="next_icon" />
  536. </div>
  537. </div>
  538. </div>
  539. );
  540. }
  541. renderModal() {
  542. const { modal } = this.state;
  543. return (
  544. <div className="modal">
  545. <div className="mask" />
  546. <div style={{ width: modal.width }} className="body">
  547. <div className="title">{modal.title}</div>
  548. <div className="desc">{modal.desc}</div>
  549. {modal.type === 'confirm' && (
  550. <div className="btn-list">
  551. <div className="btn" onClick={() => this.hideModal(true)}>
  552. <span className="t-d-l">Y</span>es
  553. </div>
  554. <div className="btn" onClick={() => this.hideModal(false)}>
  555. <span className="t-d-l">N</span>o
  556. </div>
  557. </div>
  558. )}
  559. {modal.type === 'toast' && (<div className="btn-list">
  560. <div className="btn" onClick={() => this.hideModal(true)}>
  561. <span className="t-d-l">O</span>k
  562. </div>
  563. </div>)}
  564. </div>
  565. </div>
  566. );
  567. }
  568. renderHelp() {
  569. return (
  570. <div className="modal show-help">
  571. <div className="mask" />
  572. <div style={{ width: 540 }} className="body">
  573. <div className="title">
  574. <div className="help">
  575. <Assets name="help_icon" />Help
  576. </div>
  577. <div className="close"><Assets name="close" onClick={() => this.setState({ showHelp: false })} /></div>
  578. </div>
  579. <div className="desc">
  580. <ul>
  581. <li>点击右上角的时钟图标<div className="icon"><Assets name="timeleft_icon" /></div>可以打开和关闭计时器(与实战一致);</li>
  582. <li>点击右上角的进度条<div className="icon"><Assets name="subjectnumber_icon" /></div>可以打开和关闭当前进度(与实战一致);</li>
  583. <li>点击右下角<div className="next">Next<Assets name="next_icon" /></div>进入下一题(与实战一致);</li>
  584. <li>点击右上角的<div className="icon"><Icon name="star" /></div>可以收藏本题,可至“个人中心-收藏” 查看。</li>
  585. </ul>
  586. </div>
  587. </div>
  588. </div>
  589. );
  590. }
  591. }