index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. <template>
  2. <StudyLayout>
  3. <div class="grid h-full w-full grid-cols-2 gap-6 overflow-hidden p-6">
  4. <div
  5. class="col-span-1 flex h-full flex-col overflow-hidden rounded-xl bg-white shadow-md"
  6. >
  7. <div class="bg-[var(--czr-main-sub-color)] p-4 text-white">
  8. <div class="flex items-center text-xl font-bold">
  9. <i class="fas fa-cubes mr-2"></i>
  10. 板块提升
  11. <div
  12. class="subject-questions-btn bg-subject-color ml-auto flex items-center rounded-full px-3 py-1 text-sm"
  13. @click="$router.push({ name: $route.name + 'question' })"
  14. >
  15. <i class="fas fa-question-circle mr-1"></i>
  16. <span>更多</span>
  17. </div>
  18. </div>
  19. </div>
  20. <!-- 图表容器 -->
  21. <div
  22. class="flex flex-1 flex-col overflow-x-hidden overflow-y-auto p-4"
  23. style="flex-wrap: unset"
  24. >
  25. <!-- 图表切换标签 -->
  26. <div class="mt-2 mb-4 flex space-x-2">
  27. <button
  28. :class="
  29. state.statistic.type === 1
  30. ? 'bg-[var(--czr-main-color)] text-white'
  31. : 'bg-gray-100 text-gray-600'
  32. "
  33. class="chart-tab rounded-full px-3 py-1 text-sm"
  34. @click="state.statistic.type = 1"
  35. >
  36. 知识点掌握
  37. </button>
  38. <button
  39. :class="
  40. state.statistic.type === 2
  41. ? 'bg-[var(--czr-main-color)] text-white'
  42. : 'bg-gray-100 text-gray-600'
  43. "
  44. class="chart-tab rounded-full px-3 py-1 text-sm"
  45. @click="state.statistic.type = 2"
  46. >
  47. 成绩趋势
  48. </button>
  49. <button
  50. :class="
  51. state.statistic.type === 3
  52. ? 'bg-[var(--czr-main-color)] text-white'
  53. : 'bg-gray-100 text-gray-600'
  54. "
  55. class="chart-tab rounded-full px-3 py-1 text-sm"
  56. @click="state.statistic.type = 3"
  57. >
  58. 练习完成度
  59. </button>
  60. </div>
  61. <!-- 知识点掌握情况图表 -->
  62. <div class="mb-6 flex-1">
  63. <chart1 v-if="state.statistic.type === 1" />
  64. <chart2
  65. v-if="state.statistic.type === 2"
  66. v-loading="state.statistic.line.loading"
  67. :data="state.statistic.line.data"
  68. />
  69. <chart3 v-if="state.statistic.type === 3" />
  70. </div>
  71. <!-- 统计卡片 -->
  72. <div class="mt-4 grid grid-cols-2 gap-4 md:grid-cols-4">
  73. <div
  74. class="stat-card rounded-lg border-l-4 border-[var(--czr-main-color)] bg-gray-50 p-3"
  75. >
  76. <div class="text-xs text-gray-500">当前难度等级</div>
  77. <div class="mt-1 text-xl font-bold text-gray-800">S</div>
  78. </div>
  79. <div
  80. class="stat-card rounded-lg border-l-4 border-[var(--czr-main-color)] bg-gray-50 p-3"
  81. >
  82. <div class="text-xs text-gray-500">正确率</div>
  83. <div class="mt-1 text-xl font-bold text-gray-800">85%</div>
  84. </div>
  85. <div
  86. class="stat-card rounded-lg border-l-4 border-[var(--czr-main-color)] bg-gray-50 p-3"
  87. >
  88. <div class="text-xs text-gray-500">待提升板块</div>
  89. <div class="mt-1 text-xl font-bold text-gray-800">文言文</div>
  90. </div>
  91. <div
  92. class="stat-card rounded-lg border-l-4 border-[var(--czr-main-color)] bg-gray-50 p-3"
  93. >
  94. <div class="text-xs text-gray-500">优秀板块</div>
  95. <div class="mt-1 text-xl font-bold text-gray-800">作文</div>
  96. </div>
  97. </div>
  98. <div class="flex items-center justify-between py-3">
  99. <div class="font-semibold text-gray-800">最近刷题记录</div>
  100. <div class="ml-auto">
  101. <div class="__czr-quasar-el-date">
  102. <q-input
  103. class="w-[220px]"
  104. rounded
  105. standout="focus"
  106. :dense="true"
  107. v-model="dateStrMakeQuestion"
  108. readonly
  109. >
  110. <template v-slot:prepend>
  111. <q-icon
  112. name="event"
  113. class="cursor-pointer"
  114. @click="ref_dateMakeQuestion.handleOpen()"
  115. >
  116. </q-icon>
  117. </template>
  118. </q-input>
  119. <el-date-picker
  120. ref="ref_dateMakeQuestion"
  121. v-model="state.makeQuestion.date"
  122. value-format="YYYY-MM-DD"
  123. type="daterange"
  124. @change="initMakeQuestion"
  125. />
  126. </div>
  127. </div>
  128. </div>
  129. <div
  130. class="h-[120px] overflow-y-auto"
  131. v-loading="state.makeQuestion.loading"
  132. >
  133. <template v-if="state.makeQuestion.data?.length > 0">
  134. <template v-for="item in state.makeQuestion.data">
  135. <div class="record-item rounded-lg border border-gray-100 p-2">
  136. <div class="flex items-center justify-between">
  137. <span class="text-sm font-medium">
  138. {{ AppStore.subjectMap.get(item.subject) }}
  139. </span>
  140. <div class="flex justify-end">
  141. <button
  142. class="text-subject-color flex items-center text-xs hover:underline"
  143. @click="
  144. $router.push({
  145. name: $route.meta.subjectId + 'plan',
  146. query: {
  147. planId: item.planId,
  148. },
  149. })
  150. "
  151. >
  152. <i class="fas fa-file-alt mr-1"></i>
  153. 查看详情
  154. </button>
  155. </div>
  156. </div>
  157. <div class="mt-1 text-xs text-gray-500">
  158. {{ item.planDate }} | {{ item.questionCount }}题(<span
  159. class="text-red"
  160. >{{ item.wrongCount }}</span
  161. >/<span class="text-green">{{ item.correctCount }}</span
  162. >) | 正确率{{
  163. (
  164. (Number(item.correctCount) /
  165. Number(item.questionCount)) *
  166. 100
  167. ).toFixed(1)
  168. }}%
  169. </div>
  170. </div>
  171. </template>
  172. </template>
  173. <template v-else>
  174. <div
  175. class="flex size-full items-center justify-center text-xl font-semibold text-gray-700"
  176. >
  177. 暂无数据
  178. </div>
  179. </template>
  180. </div>
  181. </div>
  182. </div>
  183. <div class="col-span-1 flex flex-col gap-6 overflow-hidden px-0.5">
  184. <div
  185. class="flex flex-1 flex-col overflow-hidden rounded-xl bg-white shadow-md"
  186. >
  187. <div class="bg-[var(--czr-main-sub-color)] p-4 text-white">
  188. <div class="relative flex items-center text-xl font-bold">
  189. <i class="fas fa-calendar-alt mr-2"></i>
  190. 考形训练
  191. <div class="absolute right-2 ml-auto flex">
  192. <div class="__czr-quasar-el-date training-date">
  193. <q-input
  194. class="w-[220px]"
  195. rounded
  196. standout="focus"
  197. :dense="true"
  198. v-model="dateStrTraining"
  199. readonly
  200. >
  201. <template v-slot:prepend>
  202. <q-icon
  203. name="event"
  204. class="cursor-pointer"
  205. color="white"
  206. @click="ref_dateTraining.handleOpen()"
  207. >
  208. </q-icon>
  209. </template>
  210. </q-input>
  211. <el-date-picker
  212. ref="ref_dateTraining"
  213. v-model="state.training.date"
  214. value-format="YYYY-MM-DD"
  215. type="daterange"
  216. @change="initTraining"
  217. />
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. <div
  223. class="flex flex-1 flex-col overflow-hidden"
  224. v-loading="state.training.loading"
  225. >
  226. <template v-if="state.training.data?.length > 0">
  227. <div class="h-[200px]">
  228. <chart4 :data="state.training.data" />
  229. </div>
  230. <div class="flex-1 overflow-y-auto px-6 py-2">
  231. <template v-if="state.training.groupData?.length > 0">
  232. <template v-for="item in state.training.groupData">
  233. <div class="timeline-date">{{ YM(item.date, true) }}</div>
  234. <template v-for="son in item.list">
  235. <div
  236. class="exam-item mb-3 rounded-lg border border-gray-300 p-3"
  237. >
  238. <div class="mb-2 flex items-start justify-between">
  239. <div>
  240. <div class="mt-0.5 text-sm text-gray-500">
  241. {{ son.planDate }}
  242. </div>
  243. </div>
  244. <span class="text-sm font-medium text-gray-800">
  245. {{ son.score }}分
  246. </span>
  247. </div>
  248. <div
  249. class="mt-3 grid grid-cols-2 gap-3 text-sm md:grid-cols-4"
  250. >
  251. <div class="rounded bg-gray-50 p-2 text-center">
  252. <div class="text-xs text-gray-500">试卷难度</div>
  253. <div class="font-medium text-gray-800">
  254. {{
  255. DictionaryStore.difficultyLevelMap.get(
  256. son.difficultyLevel,
  257. )
  258. }}
  259. </div>
  260. </div>
  261. <div class="rounded bg-gray-50 p-2 text-center">
  262. <div class="text-xs text-gray-500">用时</div>
  263. <div class="font-medium text-gray-800">
  264. {{ Math.floor(son.duration / 60) }}分钟
  265. </div>
  266. </div>
  267. <div class="rounded bg-gray-50 p-2 text-center">
  268. <div class="text-xs text-gray-500">得分</div>
  269. <div class="font-medium text-gray-800">
  270. {{ son.score }}分
  271. </div>
  272. </div>
  273. <div class="rounded bg-gray-50 p-2 text-center">
  274. <div class="text-xs text-gray-500">错题</div>
  275. <div class="font-medium text-red-500">
  276. {{ son.wrongCount }}题
  277. </div>
  278. </div>
  279. </div>
  280. <div class="mt-3">
  281. <div class="mb-1 text-xs text-gray-500">
  282. 错题类型:
  283. </div>
  284. <div class="flex flex-wrap gap-2">
  285. <span
  286. class="mistake-tag rounded-full bg-red-100 px-2 py-0.5 text-xs text-red-800"
  287. >xxx(xxx)</span
  288. >
  289. <span
  290. class="mistake-tag rounded-full bg-red-100 px-2 py-0.5 text-xs text-red-800"
  291. >xxx(xxx)</span
  292. >
  293. </div>
  294. </div>
  295. <div class="mt-3 flex justify-end">
  296. <button
  297. class="text-subject-color flex items-center text-xs hover:underline"
  298. @click="
  299. $router.push({
  300. name: $route.meta.subjectId + 'plan',
  301. query: {
  302. planId: son.planId,
  303. },
  304. })
  305. "
  306. >
  307. <i class="fas fa-file-alt mr-1"></i>
  308. 查看详情
  309. </button>
  310. </div>
  311. </div>
  312. </template>
  313. </template>
  314. </template>
  315. </div>
  316. </template>
  317. <template v-else>
  318. <div
  319. class="flex size-full items-center justify-center text-xl font-semibold text-gray-700"
  320. >
  321. 暂无数据
  322. </div>
  323. </template>
  324. </div>
  325. </div>
  326. <div
  327. class="flex flex-col overflow-hidden rounded-xl bg-white shadow-md"
  328. >
  329. <div class="bg-[var(--czr-main-sub-color)] p-4 text-white">
  330. <div class="flex items-center text-xl font-bold">
  331. <i class="fas fa-clock mr-2"></i>
  332. 错题统计
  333. <div
  334. class="subject-questions-btn bg-subject-color ml-auto flex items-center rounded-full px-3 py-1 text-sm"
  335. @click="
  336. $router.push({
  337. name: $route.meta.subjectId + 'plan',
  338. query: {
  339. onlyError: true,
  340. },
  341. })
  342. "
  343. >
  344. <i class="fas fa-question-circle mr-1"></i>
  345. <span>查看详情</span>
  346. </div>
  347. </div>
  348. </div>
  349. <div class="flex flex-col p-4">
  350. <div class="grid grid-cols-3 gap-4">
  351. <div class="flex flex-1 items-center rounded-lg bg-gray-50 p-3">
  352. <div
  353. class="mr-3 flex h-10 w-10 items-center justify-center rounded-full bg-blue-100"
  354. >
  355. <i class="fas fa-clipboard-list text-blue-600"></i>
  356. </div>
  357. <div>
  358. <div class="text-sm text-gray-500">本学科总做题数</div>
  359. <div class="text-lg font-bold">246题</div>
  360. </div>
  361. </div>
  362. <div class="flex flex-1 items-center rounded-lg bg-gray-50 p-3">
  363. <div
  364. class="mr-3 flex h-10 w-10 items-center justify-center rounded-full bg-red-100"
  365. >
  366. <i class="fas fa-times-circle text-red-600"></i>
  367. </div>
  368. <div>
  369. <div class="text-sm text-gray-500">错题数</div>
  370. <div class="text-lg font-bold">59题</div>
  371. </div>
  372. </div>
  373. <div class="flex flex-1 items-center rounded-lg bg-gray-50 p-3">
  374. <div
  375. class="mr-3 flex h-10 w-10 items-center justify-center rounded-full bg-red-100"
  376. >
  377. <i class="fas fa-times-circle text-red-600"></i>
  378. </div>
  379. <div>
  380. <div class="text-sm text-gray-500">错误率</div>
  381. <div class="text-lg font-bold">23%</div>
  382. </div>
  383. </div>
  384. </div>
  385. <div class="mt-4 border-t border-gray-100 pt-4">
  386. <div class="mb-3 text-sm font-medium">错题最多的知识点</div>
  387. <div class="flex flex-wrap gap-2">
  388. <span
  389. class="mistake-tag rounded-full bg-red-100 px-2 py-1 text-xs text-red-800"
  390. >文言文虚词 (12题)</span
  391. >
  392. <span
  393. class="mistake-tag rounded-full bg-red-100 px-2 py-1 text-xs text-red-800"
  394. >现代文阅读理解 (9题)</span
  395. >
  396. <span
  397. class="mistake-tag rounded-full bg-red-100 px-2 py-1 text-xs text-red-800"
  398. >诗歌鉴赏 (7题)</span
  399. >
  400. </div>
  401. </div>
  402. </div>
  403. </div>
  404. </div>
  405. </div>
  406. </StudyLayout>
  407. </template>
  408. <script setup lang="ts">
  409. import { computed, onBeforeMount, onMounted, reactive, ref } from 'vue'
  410. import StudyLayout from '@/views/study/components/study-layout.vue'
  411. import chart1 from './chart-1.vue'
  412. import chart2 from './chart-2.vue'
  413. import chart3 from './chart-3.vue'
  414. import chart4 from './chart-4.vue'
  415. import { trainingCampLearningPlanList } from '@/api/modules/study'
  416. import { oneDayTime, YM, YMD } from '@/utils/czr-util'
  417. import { useAppStore, useDictionaryStore } from '@/stores'
  418. import { useRoute } from 'vue-router'
  419. const route = useRoute()
  420. const AppStore = useAppStore()
  421. const DictionaryStore = useDictionaryStore()
  422. const state: any = reactive({
  423. makeQuestion: {
  424. loading: false,
  425. data: [],
  426. date: [YMD(new Date().getTime() - oneDayTime * 7), YMD(new Date())],
  427. },
  428. training: {
  429. loading: false,
  430. data: [],
  431. groupData: [],
  432. date: [YMD(new Date().getTime() - oneDayTime * 90), YMD(new Date())],
  433. },
  434. statistic: {
  435. type: 1,
  436. line: {
  437. loading: false,
  438. data: [],
  439. },
  440. },
  441. })
  442. const ref_dateMakeQuestion = ref()
  443. const dateStrMakeQuestion = computed(() => {
  444. if (state.makeQuestion.date.length > 0) {
  445. return `${state.makeQuestion.date[0]} - ${state.makeQuestion.date[1]}`
  446. }
  447. return ''
  448. })
  449. const ref_dateTraining = ref()
  450. const dateStrTraining = computed(() => {
  451. if (state.training.date.length > 0) {
  452. return `${state.training.date[0]} - ${state.training.date[1]}`
  453. }
  454. return ''
  455. })
  456. const initMakeQuestion = () => {
  457. state.makeQuestion.loading = true
  458. trainingCampLearningPlanList({
  459. pageNum: 1,
  460. pageSize: 10000,
  461. studentId: AppStore.studentInfo?.studentId,
  462. subject: route.meta.subjectId,
  463. paperType: 1,
  464. params: {
  465. beginPlanDate: `${state.makeQuestion.date[0]} 00:00:00`,
  466. endPlanDate: `${state.makeQuestion.date[1]} 23:59:59`,
  467. },
  468. })
  469. .then(({ rows }: any) => {
  470. state.makeQuestion.data = rows
  471. })
  472. .finally(() => {
  473. state.makeQuestion.loading = false
  474. })
  475. }
  476. const initTraining = () => {
  477. state.training.loading = true
  478. state.training.data = []
  479. trainingCampLearningPlanList({
  480. pageNum: 1,
  481. pageSize: 10000,
  482. studentId: AppStore.studentInfo?.studentId,
  483. subject: route.meta.subjectId,
  484. paperType: 2,
  485. params: {
  486. beginPlanDate: `${state.training.date[0]} 00:00:00`,
  487. endPlanDate: `${state.training.date[1]} 23:59:59`,
  488. },
  489. })
  490. .then(({ rows }: any) => {
  491. state.training.data = rows
  492. // 按照年月分组
  493. state.training.groupData = rows.reduce((acc, item) => {
  494. // 提取年月部分(格式:YYYY-MM)
  495. const datePart = item.planDate.slice(0, 7)
  496. // 查找是否已有该年月分组
  497. const group = acc.find((g) => g.date === datePart)
  498. if (group) {
  499. // 如果已有该分组,添加到对应的list中
  500. group.list.push(item)
  501. } else {
  502. // 如果没有该分组,创建新分组
  503. acc.push({
  504. date: datePart,
  505. list: [item],
  506. })
  507. }
  508. return acc
  509. }, [])
  510. })
  511. .finally(() => {
  512. state.training.loading = false
  513. })
  514. }
  515. const initStatistic = () => {
  516. state.statistic.line.loading = true
  517. trainingCampLearningPlanList({
  518. pageNum: 1,
  519. pageSize: 10000,
  520. studentId: AppStore.studentInfo?.studentId,
  521. subject: route.meta.subjectId,
  522. paperType: 1,
  523. params: {
  524. beginPlanDate: `${YMD(new Date().getTime() - oneDayTime * 90)} 00:00:00`,
  525. endPlanDate: `${YMD(new Date())} 23:59:59`,
  526. },
  527. })
  528. .then(({ rows }: any) => {
  529. state.statistic.line.data = rows
  530. })
  531. .finally(() => {
  532. state.statistic.line.loading = false
  533. })
  534. }
  535. onMounted(() => {
  536. initMakeQuestion()
  537. initTraining()
  538. initStatistic()
  539. })
  540. onBeforeMount(() => {
  541. // document.documentElement.style.setProperty(
  542. // '--czr-quasar-color',
  543. // 'var(--czr-main-color)',
  544. // )
  545. })
  546. </script>
  547. <style lang="scss" scoped>
  548. .subject-questions-btn {
  549. transition: all 0.2s ease;
  550. }
  551. .stat-card {
  552. transition: all 0.3s ease;
  553. }
  554. .stat-card:hover {
  555. transform: translateY(-5px);
  556. box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
  557. }
  558. .exam-item {
  559. transition: all 0.2s ease;
  560. position: relative;
  561. }
  562. .exam-item:hover {
  563. background-color: rgba(67, 97, 238, 0.05);
  564. }
  565. .mistake-tag {
  566. cursor: pointer;
  567. transition: all 0.2s ease;
  568. }
  569. .mistake-tag:hover {
  570. background-color: rgba(239, 68, 68, 0.2);
  571. transform: scale(1.05);
  572. }
  573. .progress-ring {
  574. transform: rotate(-90deg);
  575. width: 40px;
  576. height: 40px;
  577. }
  578. .progress-ring-circle {
  579. stroke-dasharray: 100;
  580. stroke-dashoffset: 100;
  581. transition: stroke-dashoffset 0.5s ease;
  582. }
  583. /* 时间轴样式 */
  584. .timeline {
  585. position: relative;
  586. padding-left: 2rem;
  587. }
  588. .timeline::before {
  589. content: '';
  590. position: absolute;
  591. left: 0.5rem;
  592. top: 0;
  593. bottom: 0;
  594. width: 2px;
  595. background-color: #e5e7eb;
  596. }
  597. .timeline-date-group {
  598. margin-bottom: 1.5rem;
  599. }
  600. .timeline-date {
  601. font-weight: 600;
  602. color: #6b7280;
  603. margin-bottom: 0.5rem;
  604. display: flex;
  605. align-items: center;
  606. }
  607. .timeline-date::before {
  608. content: '';
  609. width: 1rem;
  610. height: 1rem;
  611. border-radius: 50%;
  612. background-color: white;
  613. border: 2px solid #4361ee;
  614. margin-right: 0.5rem;
  615. z-index: 10;
  616. position: relative;
  617. }
  618. :deep(.training-date) {
  619. .q-placeholder {
  620. color: #ffffff;
  621. }
  622. }
  623. </style>