practiceData.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. import { Ref, computed, defineComponent, reactive, ref } from 'vue';
  2. import styles from '../index2.module.less';
  3. import { NButton, NDataTable, NIcon, NNumberAnimation, NTooltip, useMessage } from 'naive-ui';
  4. import { useECharts } from '@/hooks/web/useECharts';
  5. // import Pagination from '/src/components/pagination';
  6. import { getPracticePageStat, getTestStat } from '@/views/data-module/api';
  7. import { formateSeconds, getHours, getLastMinutes, getMinutes, getSecend, getTimes } from '/src/utils/dateFormat';
  8. import { api_practiceStatPage } from '../../classList/api';
  9. import TheEmpty from '/src/components/TheEmpty';
  10. import iconSortDefault from '@/common/images/icon-sort-default.png';
  11. import iconSortDesc from '@/common/images/icon-sort-desc.png';
  12. import iconSortAsc from '@/common/images/icon-sort-asc.png';
  13. import { convertToChineseNumeral } from '/src/utils';
  14. import { useRouter } from 'vue-router';
  15. import { setTabsCaches } from '/src/hooks/use-async';
  16. import iconQuestion from '/src/common/images/icon-question.png'
  17. export default defineComponent({
  18. name: 'home-practiceData',
  19. props: {
  20. timer: {
  21. type: Array,
  22. defaut: () => []
  23. }
  24. },
  25. setup(props, { expose }) {
  26. const router = useRouter()
  27. const message = useMessage()
  28. const chartRef = ref<HTMLDivElement | null>(null);
  29. const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
  30. const practiceFlag = ref(true);
  31. const payForm = reactive({
  32. height: '360px',
  33. width: '100%',
  34. practiceDuration: 0,
  35. evaluateUserCount: 0,
  36. evaluateFrequency: 0,
  37. publishUserCount: 0,
  38. publishCount: 0,
  39. practiceUserCount: 0,
  40. paymentAmount: 0,
  41. practiceDurationAvg: 0,
  42. practiceDays: 0,
  43. practiceDurationTotal: 0,
  44. dateList: [],
  45. timeList: []
  46. });
  47. const state = reactive({
  48. loading: false,
  49. pagination: {
  50. page: 1,
  51. rows: 10,
  52. pageTotal: 4
  53. },
  54. searchForm: {
  55. orderBy: null as any,
  56. sort: null as any,
  57. },
  58. tableList: [] as any,
  59. goCourseVisiable: false
  60. });
  61. const currentTimer = computed(() => {
  62. return props.timer;
  63. });
  64. const toolTitleTips = (title: string, item: any) => {
  65. return <NTooltip showArrow={false} placement="top-start">
  66. {{
  67. trigger: () => (
  68. <div class={styles.cell}>
  69. {title}
  70. <img
  71. class={styles.sortIcon}
  72. src={
  73. item.sortOrder === 'descend'
  74. ? iconSortDesc
  75. : item.sortOrder === 'ascend'
  76. ? iconSortAsc
  77. : iconSortDefault
  78. }
  79. />
  80. </div>
  81. ),
  82. default:
  83. item.sortOrder === 'descend'
  84. ? '点击升序'
  85. : item.sortOrder === 'ascend'
  86. ? '取消排序'
  87. : '点击降序'
  88. }}
  89. </NTooltip>
  90. }
  91. const practiceDurationRef = reactive({
  92. title() {
  93. return (
  94. toolTitleTips('练习总时长', practiceDurationRef)
  95. );
  96. },
  97. key: 'practiceDuration',
  98. sorter: true,
  99. sortOrder: false as any,
  100. render(row: any) {
  101. return <>{formateSeconds((row.practiceDuration as any) || 0)}</>
  102. }
  103. });
  104. const practiceDaysRef = reactive({
  105. title() {
  106. return (
  107. toolTitleTips('练习天数', practiceDaysRef)
  108. );
  109. },
  110. key: 'practiceDays',
  111. sorter: true,
  112. sortOrder: false as any
  113. });
  114. const practiceDurationAvgRef = reactive({
  115. title() {
  116. return (
  117. toolTitleTips('平均练习时长', practiceDurationAvgRef)
  118. );
  119. },
  120. key: 'practiceDurationAvg',
  121. sorter: true,
  122. sortOrder: false as any,
  123. render(row: any) {
  124. return <>{formateSeconds((row.practiceDurationAvg as any) || 0)}</>
  125. }
  126. });
  127. const evaluateFrequencyRef = reactive({
  128. title() {
  129. return (
  130. toolTitleTips('评测次数', evaluateFrequencyRef)
  131. );
  132. },
  133. key: 'evaluateFrequency',
  134. sorter: true,
  135. sortOrder: false as any
  136. });
  137. const publishCountRef = reactive({
  138. title() {
  139. return (
  140. toolTitleTips('作品数量', publishCountRef)
  141. );
  142. },
  143. key: 'publishCount',
  144. sorter: true,
  145. sortOrder: false as any
  146. });
  147. const publishScoreRef = reactive({
  148. title() {
  149. return (
  150. toolTitleTips('最新作品分数', publishScoreRef)
  151. );
  152. },
  153. key: 'publishScore',
  154. sorter: true,
  155. sortOrder: false as any
  156. });
  157. const publishTimeRef = reactive({
  158. title() {
  159. return (
  160. toolTitleTips('最新作品时间', publishTimeRef)
  161. );
  162. },
  163. key: 'publishTime',
  164. sorter: true,
  165. sortOrder: false as any
  166. });
  167. const copyTo = (text: string) => {
  168. const input = document.createElement('input');
  169. input.value = text;
  170. document.body.appendChild(input);
  171. input.select();
  172. input.setSelectionRange(0, input.value.length);
  173. document.execCommand('Copy');
  174. document.body.removeChild(input);
  175. message.success('复制成功');
  176. };
  177. const columns = () => {
  178. return [
  179. {
  180. title: '学生姓名',
  181. key: 'studentName',
  182. render: (row: any) => {
  183. return (
  184. <NTooltip showArrow={false} placement="top-start">
  185. {{
  186. trigger: () => (
  187. <div
  188. style={{ userSelect: 'all', cursor: 'pointer' }}
  189. onClick={() => copyTo(row.studentName)}>
  190. {row.studentName}
  191. </div>
  192. ),
  193. default: '点击复制'
  194. }}
  195. </NTooltip>
  196. );
  197. }
  198. },
  199. {
  200. title: '年级班级',
  201. key: 'date',
  202. render(row: any) {
  203. return (
  204. <>
  205. {row.currentGradeNum && row.currentClass
  206. ? convertToChineseNumeral(row.currentGradeNum) + '年级' + row.currentClass + '班'
  207. : ''}
  208. </>
  209. )
  210. }
  211. },
  212. {
  213. title: '乐器',
  214. key: 'instrumentName'
  215. },
  216. practiceDurationRef,
  217. practiceDaysRef,
  218. practiceDurationAvgRef,
  219. evaluateFrequencyRef,
  220. {
  221. title: () => <span style={{ display: 'flex', alignItems: 'center' }}>发布作品 <NTooltip showArrow={false}>
  222. {{
  223. trigger: () => (
  224. <img src={iconQuestion} style={{ width: '16px', height: '16px', marginLeft: '4px', cursor: 'pointer' }} />
  225. ),
  226. default: () => '筛选时间段内评测是否发布作品'
  227. }}
  228. </NTooltip></span>,
  229. key: 'publishFlag',
  230. render: (row: any) => row.publishFlag ? '是' : '否'
  231. },
  232. publishCountRef,
  233. publishScoreRef,
  234. publishTimeRef,
  235. {
  236. title: '操作',
  237. key: 'titleImg',
  238. render: (row: any) => (
  239. <NButton
  240. type="primary"
  241. text
  242. onClick={() => {
  243. setTabsCaches('evaluatingRcode', 'tabName', {
  244. path: '/studentDetail'
  245. });
  246. router.push({
  247. path: '/studentDetail',
  248. query: { studentId: row.studentId, studentName: row.studentName, times: JSON.stringify(currentTimer.value) }
  249. });
  250. }}
  251. >
  252. 详情
  253. </NButton>
  254. )
  255. }
  256. ];
  257. };
  258. // 统计排序
  259. const handleSorterChange = (sorter: any) => {
  260. if (!sorter.order) {
  261. state.searchForm.orderBy = '' as string
  262. state.searchForm.sort = '' as string
  263. practiceDurationRef.sortOrder = false
  264. practiceDaysRef.sortOrder = false
  265. practiceDurationAvgRef.sortOrder = false
  266. evaluateFrequencyRef.sortOrder = false
  267. publishCountRef.sortOrder = false
  268. publishScoreRef.sortOrder = false
  269. publishTimeRef.sortOrder = false
  270. } else {
  271. state.searchForm.orderBy = sorter.columnKey
  272. practiceDurationRef.sortOrder = false
  273. practiceDaysRef.sortOrder = false
  274. practiceDurationAvgRef.sortOrder = false
  275. evaluateFrequencyRef.sortOrder = false
  276. publishCountRef.sortOrder = false
  277. publishScoreRef.sortOrder = false
  278. publishTimeRef.sortOrder = false
  279. if (sorter.columnKey == 'practiceDuration') {
  280. practiceDurationRef.sortOrder = sorter.order
  281. }
  282. if (sorter.columnKey == 'practiceDays') {
  283. practiceDaysRef.sortOrder = sorter.order
  284. }
  285. if (sorter.columnKey == 'practiceDurationAvg') {
  286. practiceDurationAvgRef.sortOrder = sorter.order
  287. }
  288. if (sorter.columnKey == 'evaluateFrequency') {
  289. evaluateFrequencyRef.sortOrder = sorter.order
  290. }
  291. if (sorter.columnKey == 'publishCount') {
  292. publishCountRef.sortOrder = sorter.order
  293. }
  294. if (sorter.columnKey == 'publishScore') {
  295. publishScoreRef.sortOrder = sorter.order
  296. }
  297. if (sorter.columnKey == 'publishTime') {
  298. publishTimeRef.sortOrder = sorter.order
  299. }
  300. state.searchForm.sort = sorter.order == 'ascend' ? 'asc' : 'desc'
  301. }
  302. getList2()
  303. }
  304. const getList2 = async () => {
  305. state.loading = true
  306. try {
  307. const res = await api_practiceStatPage({
  308. page: 1,
  309. rows: 999,
  310. ...state.searchForm,
  311. ...getTimes(
  312. currentTimer.value,
  313. ['startTime', 'endTime'],
  314. 'YYYY-MM-DD'
  315. )
  316. });
  317. state.tableList = res.data.rows;
  318. } catch (e) {
  319. console.log(e);
  320. }
  321. state.loading = false
  322. };
  323. const getTestStatList = async () => {
  324. state.loading = true
  325. try {
  326. const res2 = await getTestStat({
  327. page: 1,
  328. rows: 999,
  329. ...getTimes(
  330. currentTimer.value,
  331. ['startTime', 'endTime'],
  332. 'YYYY-MM-DD'
  333. )
  334. });
  335. payForm.dateList = res2.data.trainingStatDetailList.map((item: any) => {
  336. return item.date;
  337. });
  338. payForm.timeList = res2.data.trainingStatDetailList.map((item: any) => {
  339. return item.practiceUserCount;
  340. });
  341. setChart();
  342. } catch {
  343. //
  344. }
  345. state.loading = false
  346. }
  347. const getPracticePageStatList = async () => {
  348. state.loading = true
  349. try {
  350. const {data} = await getPracticePageStat({
  351. page: 1,
  352. rows: 999,
  353. ...getTimes(
  354. currentTimer.value,
  355. ['startTime', 'endTime'],
  356. 'YYYY-MM-DD'
  357. )
  358. });
  359. payForm.practiceDuration = data.practiceDuration;
  360. payForm.practiceDurationAvg = data.practiceDurationAvg;
  361. payForm.practiceUserCount = data.practiceUserCount;
  362. payForm.evaluateUserCount = data.evaluateUserCount
  363. payForm.evaluateFrequency = data.evaluateFrequency
  364. payForm.publishUserCount = data.publishUserCount
  365. payForm.publishCount = data.publishCount
  366. } catch {
  367. //
  368. }
  369. state.loading = false
  370. }
  371. const getList = async () => {
  372. await getPracticePageStatList()
  373. await getTestStatList()
  374. await getList2()
  375. }
  376. expose({ getList });
  377. const setChart = () => {
  378. setOptions({
  379. tooltip: {
  380. trigger: 'axis',
  381. axisPointer: {
  382. type: 'shadow'
  383. }
  384. },
  385. legend: {
  386. show: false,
  387. selected: {
  388. //在这里设置默认展示就ok了
  389. 练习人数: practiceFlag.value
  390. }
  391. },
  392. xAxis: {
  393. type: 'category',
  394. boundaryGap: true,
  395. axisLabel: {
  396. show: true
  397. // interval: 0
  398. },
  399. data: payForm.dateList
  400. },
  401. yAxis: [
  402. {
  403. type: 'value',
  404. axisLabel: {
  405. formatter: '{value}人'
  406. },
  407. axisTick: {
  408. show: false
  409. },
  410. splitArea: {
  411. show: false,
  412. areaStyle: {
  413. color: ['rgba(255,255,255,0.2)']
  414. }
  415. },
  416. minInterval: 1,
  417. splitNumber: 5
  418. }
  419. ],
  420. grid: {
  421. left: '1%',
  422. right: '1%',
  423. top: '2%',
  424. bottom: 0,
  425. containLabel: true
  426. },
  427. series: [
  428. {
  429. data: payForm.timeList,
  430. type: 'bar',
  431. barWidth: '48px',
  432. itemStyle: {
  433. normal: {
  434. //这里设置柱形图圆角 [左上角,右上角,右下角,左下角]
  435. barBorderRadius: [8, 8, 0, 0],
  436. color: '#CDE5FF'
  437. },
  438. emphasis: {
  439. focus: 'series',
  440. color: '#3583FA' //hover时改变柱子颜色
  441. }
  442. } as any
  443. }
  444. ],
  445. formatter: (item: any) => {
  446. if (Array.isArray(item)) {
  447. return [
  448. item[0].axisValueLabel,
  449. ...item.map((d: any) => {
  450. return `<br/>${d.marker}<span style="margin-top:10px;margin-left:5px;font-size: 13px;font-weight: 500;
  451. color: #131415;font-weight: 600;
  452. margin-top:12px
  453. line-height: 18px;">练习人数: ${d.value}人 </span>`;
  454. })
  455. ].join('');
  456. } else {
  457. return item;
  458. }
  459. }
  460. });
  461. };
  462. getList();
  463. return () => (
  464. <>
  465. <div class={styles.homeTrainData}>
  466. <div class={styles.TrainDataTop}>
  467. <div class={styles.TrainDataTopLeft}>
  468. <div class={styles.TrainDataItem}>
  469. <p class={styles.TrainDataItemTitle}>
  470. <div>
  471. <span>
  472. <NNumberAnimation
  473. from={0}
  474. to={payForm.practiceUserCount}></NNumberAnimation>
  475. </span>
  476. </div>
  477. </p>
  478. <p class={styles.TrainDataItemsubTitle}>练习人数</p>
  479. </div>
  480. <div class={styles.TrainDataItem}>
  481. <p class={styles.TrainDataItemTitle}>
  482. {getHours(payForm.practiceDurationAvg) > 0 ? (
  483. <div>
  484. <span>
  485. <NNumberAnimation
  486. from={0}
  487. to={getHours(
  488. payForm.practiceDurationAvg
  489. )}></NNumberAnimation>
  490. </span>
  491. </div>
  492. ) : null}
  493. {getHours(payForm.practiceDurationAvg) > 0 || getLastMinutes(payForm.practiceDurationAvg) > 0 ? (
  494. <div>
  495. <span>
  496. <NNumberAnimation
  497. from={0}
  498. to={getLastMinutes(
  499. payForm.practiceDurationAvg
  500. )}></NNumberAnimation>
  501. </span>
  502. </div>
  503. ) : null}
  504. <div>
  505. <span>
  506. <NNumberAnimation
  507. from={0}
  508. to={getSecend(
  509. payForm.practiceDurationAvg
  510. )}></NNumberAnimation>
  511. </span>
  512. </div>
  513. </p>
  514. <p class={styles.TrainDataItemsubTitle}>平均每天练习时长</p>
  515. </div>
  516. <div class={styles.TrainDataItem}>
  517. <p class={styles.TrainDataItemTitle}>
  518. {getHours(payForm.practiceDuration) > 0 ? (
  519. <div>
  520. <span>
  521. <NNumberAnimation
  522. from={0}
  523. to={getHours(
  524. payForm.practiceDuration
  525. )}></NNumberAnimation>
  526. </span>
  527. </div>
  528. ) : null}
  529. {getHours(payForm.practiceDuration) > 0 || getLastMinutes(payForm.practiceDuration) > 0 ? (
  530. <div>
  531. <span>
  532. <NNumberAnimation
  533. from={0}
  534. to={getLastMinutes(
  535. payForm.practiceDuration
  536. )}></NNumberAnimation>
  537. </span>
  538. </div>
  539. ) : null}
  540. <div>
  541. <span>
  542. <NNumberAnimation
  543. from={0}
  544. to={getSecend(
  545. payForm.practiceDuration
  546. )}></NNumberAnimation>
  547. </span>
  548. </div>
  549. </p>
  550. <p class={styles.TrainDataItemsubTitle}>练习总时长</p>
  551. </div>
  552. <div class={styles.TrainDataItem}>
  553. <p class={styles.TrainDataItemTitle}>
  554. <div>
  555. <span>
  556. <NNumberAnimation
  557. from={0}
  558. to={payForm.evaluateUserCount}></NNumberAnimation>/
  559. <NNumberAnimation
  560. from={0}
  561. to={payForm.evaluateFrequency}></NNumberAnimation>
  562. </span>
  563. </div>
  564. </p>
  565. <p class={styles.TrainDataItemsubTitle}>评测人数/次数</p>
  566. </div>
  567. <div class={styles.TrainDataItem}>
  568. <p class={styles.TrainDataItemTitle}>
  569. <div>
  570. <span>
  571. <NNumberAnimation
  572. from={0}
  573. to={payForm.publishUserCount}></NNumberAnimation>/
  574. <NNumberAnimation
  575. from={0}
  576. to={payForm.publishCount}></NNumberAnimation>
  577. </span>
  578. </div>
  579. </p>
  580. <p class={styles.TrainDataItemsubTitle}>作品人数/数量</p>
  581. </div>
  582. </div>
  583. <div class={styles.TrainDataTopRight}>
  584. {/* <div
  585. onClick={() => {
  586. practiceFlag.value = !practiceFlag.value;
  587. setChart();
  588. }}
  589. class={[
  590. styles.DataTopRightItem,
  591. practiceFlag.value ? '' : styles.DataTopRightItemDis
  592. ]}>
  593. <div
  594. class={[
  595. styles.DataTopRightDot,
  596. styles.DataTopRightDotBlue
  597. ]}></div>
  598. <p>练习人数</p>
  599. </div> */}
  600. </div>
  601. </div>
  602. <div class={styles.chatrs}>
  603. <div
  604. ref={chartRef}
  605. style={{ height: payForm.height, width: payForm.width }}></div>
  606. </div>
  607. <div class={[styles.tableWrap, styles.noSort]}>
  608. <NDataTable
  609. v-slots={{
  610. empty: () => <TheEmpty></TheEmpty>
  611. }}
  612. class={styles.classTable}
  613. loading={state.loading}
  614. columns={columns()}
  615. onUpdate:sorter={handleSorterChange}
  616. data={state.tableList}></NDataTable>
  617. {/* <Pagination
  618. v-model:page={state.pagination.page}
  619. v-model:pageSize={state.pagination.rows}
  620. v-model:pageTotal={state.pagination.pageTotal}
  621. onList={getList}
  622. sync
  623. saveKey="orchestraRegistration-key"
  624. /> */}
  625. </div>
  626. </div>
  627. </>
  628. );
  629. }
  630. });