page.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. import React from 'react';
  2. import './index.less';
  3. import Page from '@src/containers/Page';
  4. // import LineChart from '@src/components/LineChart';
  5. import BarChart from '@src/components/BarChart';
  6. import PieChart from '@src/components/PieChart';
  7. // import { getMap } from '@src/services/Tools';
  8. import UserLayout from '../../../layouts/User';
  9. import UserTable from '../../../components/UserTable';
  10. import UserAction from '../../../components/UserAction';
  11. import Select from '../../../components/Select';
  12. import menu from '../index';
  13. import Tabs from '../../../components/Tabs';
  14. import { My } from '../../../stores/my';
  15. import { QuestionDifficult, QuestionType, TimeRange, TextbookMinYear, CourseModule } from '../../../../Constant';
  16. import { getMap, formatPercent, formatSeconds, timeRange, formatTreeData } from '../../../../../src/services/Tools';
  17. import { Main } from '../../../stores/main';
  18. const QuestionDifficultMap = getMap(QuestionDifficult, 'value', 'label');
  19. const QuestionTypeMap = getMap(QuestionType, 'value', 'label');
  20. const columns = [
  21. {
  22. key: 'title',
  23. title: '',
  24. render(text) {
  25. return <b>{text}</b>;
  26. },
  27. },
  28. {
  29. key: 'progress',
  30. title: '进度',
  31. render(text, record) {
  32. return (
  33. <div className="v">
  34. <div className="t">{formatPercent(record.userQuestion, record.questionNumber, false)}</div>
  35. <div className="d">已做{record.userQuestion}道</div>
  36. </div>
  37. );
  38. },
  39. },
  40. {
  41. key: 'ratio',
  42. title: '正确率',
  43. render(text, record) {
  44. return (
  45. <div className="v">
  46. <div className="t">{formatPercent(record.userCorrect, record.userNumber, false)}</div>
  47. <div className="d">{record.userCorrect}/{record.userNumber}</div>
  48. </div>
  49. );
  50. },
  51. },
  52. {
  53. key: 'time',
  54. title: '平均用时',
  55. help: '',
  56. render(text, record) {
  57. return formatSeconds(record.userTime / record.userNumber);
  58. },
  59. },
  60. ];
  61. // const QuestionDifficultMap = getMap(QuestionDifficult, 'value', 'label');
  62. function pieOption1(value, text, subtext) {
  63. return {
  64. title: {
  65. text,
  66. textAlign: 'center',
  67. textVerticalAlign: 'middle',
  68. subtext,
  69. top: '28%',
  70. left: '48%',
  71. },
  72. // value < 50 ? '#f19057' :
  73. color: ['#6966fb', '#f7f7f7'],
  74. series: [
  75. {
  76. type: 'pie',
  77. radius: ['90%', '100%'],
  78. label: {
  79. show: false,
  80. },
  81. data: [value, 100 - value],
  82. },
  83. ],
  84. };
  85. }
  86. function barOption1(avgTotal, avgCorrect, avgIncorrent) {
  87. return {
  88. xAxis: {
  89. type: 'category',
  90. axisTick: { show: false },
  91. axisLine: { lineStyle: { color: '#D1D6DF' } },
  92. splitLine: { show: false },
  93. data: [
  94. {
  95. value: 'Avg Time\nTotal',
  96. textStyle: { color: '#686872', fontWeight: '500', fontSize: 14, lineHeight: 20 },
  97. },
  98. {
  99. value: 'Avg Time\nCorrect',
  100. textStyle: { color: '#686872', fontWeight: '500', fontSize: 14, lineHeight: 20 },
  101. },
  102. {
  103. value: 'Avg Time\nIncorrect',
  104. textStyle: { color: '#686872', fontWeight: '500', fontSize: 14, lineHeight: 20 },
  105. },
  106. ],
  107. },
  108. yAxis: {
  109. show: false,
  110. min: 0,
  111. max: 100,
  112. axisTick: { show: false },
  113. axisLine: { show: false },
  114. splitLine: { show: false },
  115. },
  116. grid: { width: 300, left: '10%' },
  117. series: {
  118. type: 'bar',
  119. barWidth: 50,
  120. data: [
  121. {
  122. value: avgTotal,
  123. name: 'Avg Time\nTotal',
  124. itemStyle: { color: '#92AFD2' },
  125. label: {
  126. show: true,
  127. position: 'top',
  128. formatter: `{a|${formatSeconds(avgTotal)}}`,
  129. rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
  130. },
  131. },
  132. {
  133. value: avgCorrect,
  134. name: 'Avg Time\nCorrect',
  135. itemStyle: { color: '#989FC1' },
  136. label: {
  137. show: true,
  138. position: 'top',
  139. formatter: `{a|${formatSeconds(avgCorrect)}}`,
  140. rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
  141. },
  142. },
  143. {
  144. value: avgIncorrent,
  145. name: 'Avg Time\nIncorrect',
  146. itemStyle: { color: '#BFD4EE' },
  147. label: {
  148. show: true,
  149. position: 'top',
  150. formatter: `{a|${formatSeconds(avgIncorrent)}}`,
  151. rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
  152. },
  153. },
  154. ],
  155. },
  156. };
  157. }
  158. function barOption2(title, subTitle, data) {
  159. return {
  160. title: {
  161. text: title,
  162. subtext: subTitle,
  163. textStyle: { fontSize: 16 },
  164. },
  165. tooltip: {
  166. trigger: 'axis',
  167. },
  168. color: '#989FC1',
  169. dataset: {
  170. source: [['type', 'self'], ...data],
  171. },
  172. grid: { left: 30, right: 30, height: 250 },
  173. xAxis: {
  174. type: 'category',
  175. axisLabel: { color: '#686872' },
  176. axisLine: { lineStyle: { color: '#D1D6DF' } },
  177. },
  178. yAxis: {
  179. type: 'value',
  180. min: 0,
  181. max: 100,
  182. axisLabel: { color: '#686872' },
  183. axisLine: { lineStyle: { color: '#D1D6DF' } },
  184. },
  185. series: {
  186. type: 'bar',
  187. barWidth: 40,
  188. },
  189. };
  190. }
  191. function barOption3(titles, source, data1, data2, color1, color2) {
  192. return {
  193. title: [
  194. {
  195. text: titles[0],
  196. textStyle: { fontSize: 16, fontWeight: 'bold', color: '#686872' },
  197. left: 30,
  198. top: 15,
  199. },
  200. {
  201. text: titles[1],
  202. textStyle: { fontSize: 16, fontWeight: 'bold', color: '#686872' },
  203. left: 100,
  204. top: 15,
  205. },
  206. {
  207. text: titles[2],
  208. textStyle: { fontSize: 16, fontWeight: 'bold', color: '#686872' },
  209. left: 430,
  210. top: 15,
  211. },
  212. ],
  213. grid: [{ width: 200, x: 100, bottom: 30 }, { width: 200, x: 430, bottom: 30 }],
  214. xAxis: [
  215. {
  216. gridIndex: 0,
  217. show: false,
  218. axisTick: { show: false },
  219. axisLine: { show: false },
  220. splitLine: { show: false },
  221. },
  222. {
  223. gridIndex: 1,
  224. show: false,
  225. axisTick: { show: false },
  226. axisLine: { show: false },
  227. splitLine: { show: false },
  228. },
  229. ],
  230. yAxis: [
  231. {
  232. gridIndex: 0,
  233. type: 'category',
  234. axisTick: { show: false },
  235. axisLine: { show: false },
  236. splitLine: { show: false },
  237. offset: 15,
  238. data: source,
  239. axisLabel: { color: '#686872', fontSize: 12 },
  240. },
  241. {
  242. gridIndex: 1,
  243. type: 'category',
  244. axisTick: { show: false },
  245. axisLine: { show: false },
  246. splitLine: { show: false },
  247. axisLabel: { show: false },
  248. },
  249. ],
  250. series: [
  251. {
  252. type: 'bar',
  253. xAxisIndex: 0,
  254. yAxisIndex: 0,
  255. barWidth: 24,
  256. data: data1.map((item, index) => ({
  257. value: item,
  258. itemStyle: { color: index % 2 ? color1[0] : color1[1] },
  259. label: {
  260. show: true,
  261. color: '#303036',
  262. align: 'right',
  263. position: [250, 5],
  264. fontSize: 12,
  265. formatter: item,
  266. },
  267. })),
  268. },
  269. {
  270. type: 'bar',
  271. xAxisIndex: 1,
  272. yAxisIndex: 1,
  273. barWidth: 24,
  274. data: data2.map((item, index) => ({
  275. value: item,
  276. itemStyle: { color: index % 2 ? color2[0] : color2[1] },
  277. label: {
  278. show: true,
  279. color: '#303036',
  280. align: 'right',
  281. fontSize: 12,
  282. position: [260, 5],
  283. formatter: item,
  284. },
  285. })),
  286. },
  287. ],
  288. };
  289. }
  290. export default class extends Page {
  291. initState() {
  292. return {
  293. filterMap: {},
  294. selectList: [],
  295. tab: 'exercise',
  296. subject: 'verbal',
  297. questionType: '',
  298. timerange: 'today',
  299. info: 'base',
  300. };
  301. }
  302. initData() {
  303. const data = Object.assign(this.state, this.state.search);
  304. data.filterMap = this.state;
  305. if (data.order) {
  306. data.sortMap = { [data.order]: data.direction };
  307. }
  308. const [startTime, endTime] = timeRange(data.timerange);
  309. this.refreshQuestionType(data.subject, { needSentence: false, allSubject: false })
  310. .then(() => {
  311. return this.refreshStruct(data.tab, data.one, data.two, { needPreview: false, needTextbook: false });
  312. })
  313. .then(({ structIds }) => {
  314. My.getData(data.tab, data.subject, structIds, startTime, endTime).then(result => {
  315. this.data = result;
  316. this.setState({
  317. list: Object.values(result).map(row => {
  318. row.title = QuestionTypeMap[row.questionType];
  319. return row;
  320. }),
  321. });
  322. this.onQuestionTypeChange(Object.keys(result)[0]);
  323. });
  324. });
  325. }
  326. refreshQuestionType(subject, { needSentence, allSubject }) {
  327. return Main.getExercise().then(result => {
  328. const list = result.filter(row => (needSentence ? true : row.isExamination)).map(row => {
  329. row.title = `${row.titleZh}${row.titleEn}`;
  330. row.key = row.extend;
  331. return row;
  332. });
  333. const tree = formatTreeData(list, 'id', 'title', 'parentId');
  334. this.questionSubjectMap = getMap(tree, 'key', 'children');
  335. this.questionSubjectSelect = tree.filter(row => row.level === 1 && (allSubject ? true : row.children.length > 1));
  336. this.setState({
  337. questionSubjectSelect: this.questionSubjectSelect,
  338. questionSubjectMap: this.questionSubjectMap,
  339. });
  340. return {
  341. questionTypes: subject ? this.questionSubjectMap[subject].map(row => row.key) : null,
  342. };
  343. });
  344. }
  345. refreshStruct(module, one, two, { needTextbook, needPreview }) {
  346. switch (module) {
  347. case 'exercise':
  348. return Main.getExerciseAll().then(result => {
  349. const tmp = result.filter(row => row.level > 2).map(row => {
  350. row.title = `${row.titleZh}`;
  351. row.key = row.titleEn;
  352. return row;
  353. });
  354. const idsMap = getMap(tmp, 'id', 'key');
  355. const map = {};
  356. tmp.forEach(row => {
  357. if (!map[row.key]) {
  358. map[row.key] = {
  359. title: row.title,
  360. key: row.key,
  361. structIds: [],
  362. parentId: row.level > 3 ? idsMap[row.parentId] : null,
  363. subject: [],
  364. questionType: [],
  365. };
  366. }
  367. const item = map[row.key];
  368. item.structIds.push(row.id);
  369. if (item.subject.indexOf(row.subject) < 0) {
  370. item.subject.push(row.subject);
  371. }
  372. if (item.questionType.indexOf(row.questionType) < 0) {
  373. item.questionType.push(row.questionType);
  374. }
  375. });
  376. const list = Object.values(map);
  377. if (needPreview) {
  378. list.push({
  379. title: '预习作业',
  380. key: 'preview',
  381. id: 'preview',
  382. });
  383. CourseModule.forEach(row => {
  384. list.push({
  385. title: row.label,
  386. key: row.value,
  387. parentId: 'preview',
  388. });
  389. });
  390. }
  391. let courseModules = null;
  392. let structIds = null;
  393. if (one === 'preview') {
  394. if (!two) {
  395. courseModules = CourseModule.map(row => row.value);
  396. } else {
  397. courseModules = [two];
  398. }
  399. } else if (one) {
  400. const resultMap = getMap(list, 'key', 'structIds');
  401. if (!two) {
  402. structIds = resultMap[one];
  403. } else {
  404. structIds = resultMap[two];
  405. }
  406. }
  407. const tree = formatTreeData(list, 'key', 'title', 'parentId');
  408. const oneSelect = tree;
  409. const twoSelectMap = getMap(tree, 'key', 'children');
  410. this.setState({ oneSelect, twoSelectMap });
  411. return {
  412. structIds,
  413. courseModules,
  414. };
  415. });
  416. case 'examination':
  417. return Main.getExamination().then(result => {
  418. const list = result.map(row => {
  419. row.title = `${row.titleZh}${row.titleEn}`;
  420. row.key = `${row.id}`;
  421. return row;
  422. });
  423. if (needTextbook) {
  424. list.push({
  425. title: '数学机经',
  426. key: 'textbook',
  427. id: 'textbook',
  428. });
  429. list.push({
  430. title: '最新',
  431. key: 'latest',
  432. parentId: 'textbook',
  433. });
  434. const nowYear = new Date().getFullYear();
  435. for (let i = TextbookMinYear; i <= nowYear; i += 1) {
  436. list.push({
  437. title: i.toString(),
  438. key: i.toString(),
  439. parentId: 'textbook',
  440. });
  441. }
  442. }
  443. let latest = null;
  444. let year = null;
  445. let structIds = null;
  446. if (one === 'textbook') {
  447. if (!two) {
  448. latest = true;
  449. } else if (two === 'latest') {
  450. latest = true;
  451. } else {
  452. year = two;
  453. }
  454. } else if (one) {
  455. if (!two) {
  456. structIds = [Number(one)];
  457. } else {
  458. structIds = [Number(two)];
  459. }
  460. }
  461. const tree = formatTreeData(list, 'key', 'title', 'parentId');
  462. const oneSelect = tree;
  463. const twoSelectMap = getMap(tree, 'key', 'children');
  464. this.setState({ oneSelect, twoSelectMap });
  465. return {
  466. structIds,
  467. latest,
  468. year,
  469. };
  470. });
  471. default:
  472. return Promise.resolve({});
  473. }
  474. }
  475. onTabChange(tab) {
  476. const data = { tab };
  477. this.refreshQuery(data);
  478. }
  479. onQuestionTypeChange(questionType) {
  480. const data = this.data[questionType];
  481. this.setState({ questionType, data });
  482. }
  483. onInfoChange(info) {
  484. this.setState({ info });
  485. }
  486. onFilter(value) {
  487. this.search(value);
  488. }
  489. onAction() { }
  490. onSelect(selectList) {
  491. this.setState({ selectList });
  492. }
  493. renderView() {
  494. const { config } = this.props;
  495. return <UserLayout active={config.key} menu={menu} center={this.renderTable()} />;
  496. }
  497. renderTable() {
  498. const { tab, subject, questionType, info, questionSubjectSelect, questionSubjectMap = {}, oneSelect, twoSelectMap = {}, filterMap = {}, list = [] } = this.state;
  499. return (
  500. <div className="table-layout">
  501. <Tabs
  502. border
  503. type="division"
  504. theme="theme"
  505. size="small"
  506. space={2.5}
  507. width={100}
  508. active={tab}
  509. tabs={[{ key: 'exercise', title: '练习' }, { key: 'examination', title: '模考' }]}
  510. onChange={key => this.onTabChange(key)}
  511. />
  512. <UserAction
  513. selectList={[
  514. {
  515. key: 'subject',
  516. select: questionSubjectSelect,
  517. },
  518. {
  519. label: '范围',
  520. children: [
  521. {
  522. key: 'one',
  523. placeholder: '全部',
  524. select: oneSelect,
  525. },
  526. {
  527. key: 'two',
  528. be: 'one',
  529. placeholder: '全部',
  530. selectMap: twoSelectMap,
  531. },
  532. ],
  533. }, {
  534. right: true,
  535. key: 'timerange',
  536. select: TimeRange,
  537. }]}
  538. filterMap={filterMap}
  539. onFilter={value => this.onFilter(value)}
  540. />
  541. <div className="title">整体情况</div>
  542. <UserTable size="small" columns={columns} data={list} />
  543. <div className="title">
  544. 单项分析
  545. <Select
  546. size="small"
  547. theme="default"
  548. value={questionType}
  549. list={questionSubjectMap[subject]}
  550. onChange={({ key }) => this.onQuestionTypeChange(key)}
  551. />
  552. </div>
  553. <Tabs
  554. border
  555. type="line"
  556. theme="theme"
  557. size="small"
  558. width={80}
  559. active={info}
  560. tabs={[{ key: 'base', title: '基本情况' }, { key: 'difficult', title: '难度分析' }, { key: 'place', title: '考点分析' }]}
  561. onChange={key => this.onInfoChange(key)}
  562. />
  563. {this[`renderTab${info}`]()}
  564. </div>
  565. );
  566. }
  567. renderTabbase() {
  568. const { data = {} } = this.state;
  569. return (
  570. <div className="tab-1-layout">
  571. <div className="block">
  572. <div className="chart">
  573. <PieChart height={110} width={110} option={pieOption1(formatPercent(data.userQuestion, data.questionNumber), formatPercent(data.userQuestion, data.questionNumber, false), `全站${formatPercent(data.totalCorrect, data.totalNumber, false)}`)} />
  574. </div>
  575. <div className="value">
  576. <div className="total">共{data.questionNumber}题</div>
  577. <div className="item">
  578. <div className="t">已做</div>
  579. <div className="v">
  580. <b>{data.userQuestion}</b>题
  581. </div>
  582. </div>
  583. <div className="item">
  584. <div className="t">剩余</div>
  585. <div className="v">
  586. <b>{data.questionNumber - data.userQuestion}</b>题
  587. </div>
  588. </div>
  589. <div className="item">
  590. <div className="t">正确率</div>
  591. <div className="v">
  592. <b>{formatPercent(data.userCorrect, data.userNumber, false)}</b>
  593. </div>
  594. </div>
  595. <div className="item">
  596. <div className="t">全站</div>
  597. <div className="v">
  598. <b>{formatPercent(data.totalCorrect, data.totalNumber, false)}</b>
  599. </div>
  600. </div>
  601. </div>
  602. </div>
  603. <div className="block">
  604. <BarChart height={300} option={barOption1(data.userNumber ? data.userTime / data.userNumber : 0, data.userCorrect ? data.correctTime / data.userCorrect : 0, data.userNumber - data.userCorrect ? data.incorrectTime / (data.userNumber - data.userCorrect) : 0)} />
  605. </div>
  606. </div>
  607. );
  608. }
  609. renderTabdifficult() {
  610. const { data = {} } = this.state;
  611. return (
  612. <div className="tab-2-layout">
  613. <BarChart
  614. height={350}
  615. option={barOption2(`平均正确率${formatPercent(data.userCorrect, data.userNumber, false)}`, '正确率', data.difficult.map(row => {
  616. return [QuestionDifficultMap[row.key], formatPercent(row.userCorrect, row.userNumber)];
  617. }))}
  618. />
  619. </div>
  620. );
  621. }
  622. renderTabplace() {
  623. const { data = {} } = this.state;
  624. return (
  625. <div className="tab-3-layout">
  626. <BarChart
  627. height={350}
  628. option={barOption3(
  629. ['知识点', '正确率分析', '用时分析'],
  630. data.place.map(row => {
  631. return row.key;
  632. }),
  633. data.place.map(row => {
  634. return formatPercent(row.userCorrect, row.userNumber);
  635. }),
  636. data.place.map(row => {
  637. return row.userTime / row.userNumber;
  638. }),
  639. ['#92AFD2', '#BFD4EE'],
  640. ['#989FC1', '#CCCCDC'],
  641. )}
  642. />
  643. </div>
  644. );
  645. }
  646. }