page.js 15 KB

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