page.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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 { getMap, formatPercent, formatSeconds, formatMinuteSecond, timeRange } from '@src/services/Tools';
  9. import UserLayout from '../../../layouts/User';
  10. import UserTable from '../../../components/UserTable';
  11. import UserAction from '../../../components/UserAction';
  12. import Select from '../../../components/Select';
  13. import menu, { refreshQuestionType, refreshStruct } from '../index';
  14. import Tabs from '../../../components/Tabs';
  15. import { My } from '../../../stores/my';
  16. import { QuestionDifficult, QuestionType, TimeRange } from '../../../../Constant';
  17. const QuestionDifficultMap = getMap(QuestionDifficult, 'value', 'label');
  18. const QuestionTypeMap = getMap(QuestionType, 'value', 'label');
  19. const columns = [
  20. {
  21. key: 'title',
  22. title: '',
  23. render(text) {
  24. return <b>{text}</b>;
  25. },
  26. },
  27. {
  28. key: 'progress',
  29. title: '进度',
  30. render(text, record) {
  31. return (
  32. <div className="v">
  33. <div className="t">{formatPercent(record.userQuestion, record.questionNumber, false)}</div>
  34. <div className="d">已做{record.userQuestion}道</div>
  35. </div>
  36. );
  37. },
  38. },
  39. {
  40. key: 'ratio',
  41. title: '正确率',
  42. render(text, record) {
  43. return (
  44. <div className="v">
  45. <div className="t">{formatPercent(record.userCorrect, record.userNumber, false)}</div>
  46. <div className="d">{record.userCorrect}/{record.userNumber}</div>
  47. </div>
  48. );
  49. },
  50. },
  51. {
  52. key: 'time',
  53. title: '平均用时',
  54. help: '',
  55. render(text, record) {
  56. return formatSeconds(record.userTime / record.userNumber);
  57. },
  58. },
  59. ];
  60. // const QuestionDifficultMap = getMap(QuestionDifficult, 'value', 'label');
  61. function pieOption1(value, text, subtext) {
  62. return {
  63. title: {
  64. text,
  65. textAlign: 'center',
  66. textVerticalAlign: 'middle',
  67. subtext,
  68. top: '28%',
  69. left: '48%',
  70. },
  71. // value < 50 ? '#f19057' :
  72. color: ['#6966fb', '#f7f7f7'],
  73. series: [
  74. {
  75. type: 'pie',
  76. radius: ['90%', '100%'],
  77. label: {
  78. show: false,
  79. },
  80. data: [value, 100 - value],
  81. silent: true,
  82. },
  83. ],
  84. };
  85. }
  86. function barOption1(avgTotal, avgCorrect, avgIncorrent) {
  87. avgTotal = parseInt(avgTotal, 10);
  88. avgCorrect = parseInt(avgCorrect, 10);
  89. avgIncorrent = parseInt(avgIncorrent, 10);
  90. return {
  91. xAxis: {
  92. type: 'category',
  93. axisTick: { show: false },
  94. axisLine: { lineStyle: { color: '#D1D6DF' } },
  95. splitLine: { show: false },
  96. data: [
  97. {
  98. value: 'Avg Time\nTotal',
  99. textStyle: { color: '#686872', fontWeight: '500', fontSize: 14, lineHeight: 20 },
  100. },
  101. {
  102. value: 'Avg Time\nCorrect',
  103. textStyle: { color: '#686872', fontWeight: '500', fontSize: 14, lineHeight: 20 },
  104. },
  105. {
  106. value: 'Avg Time\nIncorrect',
  107. textStyle: { color: '#686872', fontWeight: '500', fontSize: 14, lineHeight: 20 },
  108. },
  109. ],
  110. },
  111. yAxis: {
  112. show: false,
  113. min: 0,
  114. max: 300,
  115. axisTick: { show: false },
  116. axisLine: { show: false },
  117. splitLine: { show: false },
  118. },
  119. grid: { width: 300, left: '10%' },
  120. series: {
  121. type: 'bar',
  122. barWidth: 50,
  123. data: [
  124. {
  125. value: avgTotal,
  126. name: 'Avg Time\nTotal',
  127. itemStyle: { color: '#92AFD2' },
  128. label: {
  129. show: true,
  130. position: 'top',
  131. formatter: `{a|${formatSeconds(avgTotal)}}`,
  132. rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
  133. },
  134. },
  135. {
  136. value: avgCorrect,
  137. name: 'Avg Time\nCorrect',
  138. itemStyle: { color: '#989FC1' },
  139. label: {
  140. show: true,
  141. position: 'top',
  142. formatter: `{a|${formatSeconds(avgCorrect)}}`,
  143. rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
  144. },
  145. },
  146. {
  147. value: avgIncorrent,
  148. name: 'Avg Time\nIncorrect',
  149. itemStyle: { color: '#BFD4EE' },
  150. label: {
  151. show: true,
  152. position: 'top',
  153. formatter: `{a|${formatSeconds(avgIncorrent)}}`,
  154. rich: { a: { fontSize: 16, fontWeight: 'bold', color: '#686872' } },
  155. },
  156. },
  157. ],
  158. },
  159. };
  160. }
  161. function barOption2(title, subTitle, data) {
  162. return {
  163. title: {
  164. text: title,
  165. subtext: subTitle,
  166. textStyle: { fontSize: 16 },
  167. },
  168. tooltip: {
  169. trigger: 'axis',
  170. },
  171. color: '#989FC1',
  172. dataset: {
  173. source: [['type', 'self'], ...data],
  174. },
  175. grid: { left: 30, right: 30, height: 250 },
  176. xAxis: {
  177. type: 'category',
  178. axisLabel: { color: '#686872' },
  179. axisLine: { lineStyle: { color: '#D1D6DF' } },
  180. },
  181. yAxis: {
  182. type: 'value',
  183. min: 0,
  184. max: 100,
  185. axisLabel: { color: '#686872' },
  186. axisLine: { lineStyle: { color: '#D1D6DF' } },
  187. },
  188. series: {
  189. type: 'bar',
  190. barWidth: 40,
  191. },
  192. };
  193. }
  194. function barOption3(titles, source, data1, data2, color1, color2) {
  195. return {
  196. title: [
  197. {
  198. text: titles[0],
  199. textStyle: { fontSize: 16, fontWeight: 'bold', color: '#686872' },
  200. left: 30,
  201. top: 15,
  202. },
  203. {
  204. text: titles[1],
  205. textStyle: { fontSize: 16, fontWeight: 'bold', color: '#686872' },
  206. left: 100,
  207. top: 15,
  208. },
  209. {
  210. text: titles[2],
  211. textStyle: { fontSize: 16, fontWeight: 'bold', color: '#686872' },
  212. left: 430,
  213. top: 15,
  214. },
  215. ],
  216. grid: [{ width: 200, x: 100, bottom: 30 }, { width: 200, x: 430, bottom: 30 }],
  217. xAxis: [
  218. {
  219. gridIndex: 0,
  220. show: false,
  221. axisTick: { show: false },
  222. axisLine: { show: false },
  223. splitLine: { show: false },
  224. },
  225. {
  226. gridIndex: 1,
  227. show: false,
  228. axisTick: { show: false },
  229. axisLine: { show: false },
  230. splitLine: { show: false },
  231. },
  232. ],
  233. yAxis: [
  234. {
  235. gridIndex: 0,
  236. type: 'category',
  237. axisTick: { show: false },
  238. axisLine: { show: false },
  239. splitLine: { show: false },
  240. offset: 15,
  241. data: source,
  242. axisLabel: { color: '#686872', fontSize: 12 },
  243. },
  244. {
  245. gridIndex: 1,
  246. type: 'category',
  247. axisTick: { show: false },
  248. axisLine: { show: false },
  249. splitLine: { show: false },
  250. axisLabel: { show: false },
  251. },
  252. ],
  253. series: [
  254. {
  255. type: 'bar',
  256. xAxisIndex: 0,
  257. yAxisIndex: 0,
  258. barWidth: 24,
  259. data: data1.map((item, index) => ({
  260. value: formatPercent(item[0], item[1], true),
  261. itemStyle: { color: index % 2 ? color1[0] : color1[1] },
  262. label: {
  263. show: true,
  264. color: '#303036',
  265. align: 'right',
  266. position: [250, 5],
  267. fontSize: 12,
  268. formatter: `${item[0]}/${item[1]} ${formatPercent(item[0], item[1], false)}`,
  269. },
  270. })),
  271. },
  272. {
  273. type: 'bar',
  274. xAxisIndex: 1,
  275. yAxisIndex: 1,
  276. barWidth: 24,
  277. data: data2.map((item, index) => ({
  278. value: parseInt(item, 10),
  279. itemStyle: { color: index % 2 ? color2[0] : color2[1] },
  280. label: {
  281. show: true,
  282. color: '#303036',
  283. align: 'right',
  284. fontSize: 12,
  285. position: [260, 5],
  286. formatter: formatMinuteSecond(parseInt(item, 10)),
  287. },
  288. })),
  289. },
  290. ],
  291. };
  292. }
  293. export default class extends Page {
  294. initState() {
  295. return {
  296. filterMap: {},
  297. selectList: [],
  298. tab: 'exercise',
  299. subject: 'verbal',
  300. questionType: '',
  301. timerange: 'all',
  302. info: 'base',
  303. };
  304. }
  305. initData() {
  306. delete this.state.search.page;
  307. delete this.state.search.size;
  308. const data = Object.assign(this.state, this.state.search);
  309. data.filterMap = this.state.search;
  310. if (data.order) {
  311. data.sortMap = { [data.order]: data.direction };
  312. }
  313. if (data.timerange) {
  314. data.filterMap.timerange = data.timerange;
  315. }
  316. if (data.subject) {
  317. data.filterMap.subject = data.subject;
  318. }
  319. const [startTime, endTime] = timeRange(data.timerange);
  320. refreshQuestionType(this, data.subject, data.questionType, { needSentence: false, allSubject: false })
  321. .then(({ questionTypes }) => {
  322. return refreshStruct(this, questionTypes, data.tab, data.one, data.two, {
  323. all: true, needPreview: false, needTextbook: false,
  324. });
  325. })
  326. .then(({ structIds }) => {
  327. My.getData(data.tab, data.subject, structIds, startTime, endTime).then(result => {
  328. this.data = result;
  329. this.setState({
  330. list: Object.values(result).map(row => {
  331. row.title = QuestionTypeMap[row.questionType];
  332. return row;
  333. }),
  334. });
  335. this.onQuestionTypeChange(Object.keys(result)[0]);
  336. });
  337. });
  338. }
  339. onTabChange(tab) {
  340. const data = { tab };
  341. this.refreshQuery(data);
  342. }
  343. onQuestionTypeChange(questionType) {
  344. const data = this.data[questionType];
  345. this.setState({ questionType, data });
  346. }
  347. onInfoChange(info) {
  348. this.setState({ info });
  349. }
  350. onFilter(value) {
  351. this.search(value);
  352. }
  353. onAction() { }
  354. onSelect(selectList) {
  355. this.setState({ selectList });
  356. }
  357. renderView() {
  358. const { config } = this.props;
  359. return <UserLayout active={config.key} menu={menu} center={this.renderTable()} />;
  360. }
  361. renderTable() {
  362. const { tab, subject, questionType, info, questionSubjectSelect, questionSubjectMap = {}, oneSelect, twoSelectMap = {}, filterMap = {}, list = [] } = this.state;
  363. return (
  364. <div className="table-layout">
  365. <Tabs
  366. border
  367. type="division"
  368. theme="theme"
  369. size="small"
  370. space={2.5}
  371. width={100}
  372. active={tab}
  373. tabs={[{ key: 'exercise', title: '练习' }, { key: 'examination', title: '模考' }]}
  374. onChange={key => this.onTabChange(key)}
  375. />
  376. <UserAction
  377. selectList={[
  378. {
  379. key: 'subject',
  380. select: questionSubjectSelect,
  381. },
  382. {
  383. label: '范围',
  384. children: [
  385. {
  386. key: 'one',
  387. placeholder: '全部',
  388. select: oneSelect,
  389. },
  390. {
  391. key: 'two',
  392. be: 'one',
  393. placeholder: '全部',
  394. selectMap: twoSelectMap,
  395. },
  396. ],
  397. }, {
  398. right: true,
  399. key: 'timerange',
  400. select: TimeRange,
  401. }]}
  402. filterMap={filterMap}
  403. onFilter={value => this.onFilter(value)}
  404. />
  405. <div className="title">整体情况</div>
  406. <UserTable size="small" columns={columns} data={list} />
  407. <div className="title">
  408. 单项分析
  409. <Select
  410. size="small"
  411. theme="default"
  412. value={questionType}
  413. list={questionSubjectMap[subject]}
  414. onChange={({ key }) => this.onQuestionTypeChange(key)}
  415. />
  416. </div>
  417. <Tabs
  418. border
  419. type="line"
  420. theme="theme"
  421. size="small"
  422. width={80}
  423. active={info}
  424. tabs={[{ key: 'base', title: '基本情况' }, { key: 'difficult', title: '难度分析' }, { key: 'place', title: '考点分析' }]}
  425. onChange={key => this.onInfoChange(key)}
  426. />
  427. {this[`renderTab${info}`]()}
  428. </div>
  429. );
  430. }
  431. renderTabbase() {
  432. const { data = {} } = this.state;
  433. return (
  434. <div className="tab-1-layout">
  435. <div className="block">
  436. <div className="chart">
  437. <PieChart height={110} width={110} option={pieOption1(formatPercent(data.userQuestion, data.questionNumber), formatPercent(data.userQuestion, data.questionNumber, false), `全站${formatPercent(data.totalCorrect, data.totalNumber, false)}`)} />
  438. </div>
  439. <div className="value">
  440. <div className="total">共{data.questionNumber}题</div>
  441. <div className="item">
  442. <div className="t">已做</div>
  443. <div className="v">
  444. <b>{data.userQuestion}</b>题
  445. </div>
  446. </div>
  447. <div className="item">
  448. <div className="t">剩余</div>
  449. <div className="v">
  450. <b>{data.questionNumber - data.userQuestion}</b>题
  451. </div>
  452. </div>
  453. <div className="item">
  454. <div className="t">正确率</div>
  455. <div className="v">
  456. <b>{formatPercent(data.userCorrect, data.userNumber, false)}</b>
  457. </div>
  458. </div>
  459. <div className="item">
  460. <div className="t">全站</div>
  461. <div className="v">
  462. <b>{formatPercent(data.totalCorrect, data.totalNumber, false)}</b>
  463. </div>
  464. </div>
  465. </div>
  466. </div>
  467. <div className="block">
  468. <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)} />
  469. </div>
  470. </div>
  471. );
  472. }
  473. renderTabdifficult() {
  474. const { data = {} } = this.state;
  475. return (
  476. <div className="tab-2-layout">
  477. <BarChart
  478. height={350}
  479. option={barOption2(`平均正确率${formatPercent(data.userCorrect, data.userNumber, false)}`, '正确率', data.difficult.map(row => {
  480. return [QuestionDifficultMap[row.key], formatPercent(row.userCorrect, row.userNumber)];
  481. }))}
  482. />
  483. </div>
  484. );
  485. }
  486. renderTabplace() {
  487. const { data = {} } = this.state;
  488. return (
  489. <div className="tab-3-layout">
  490. <BarChart
  491. height={350}
  492. option={barOption3(
  493. ['知识点', '正确率分析', '用时分析'],
  494. data.place.map(row => {
  495. return row.key;
  496. }),
  497. data.place.map(row => {
  498. return [row.userCorrect, row.userNumber];
  499. }),
  500. data.place.map(row => {
  501. return row.userTime / row.userNumber;
  502. }),
  503. ['#92AFD2', '#BFD4EE'],
  504. ['#989FC1', '#CCCCDC'],
  505. )}
  506. />
  507. </div>
  508. );
  509. }
  510. }