index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <script setup lang="ts">
  2. import { DashboardData } from "@/api/dashboard/types";
  3. import {
  4. AddressId,
  5. Area,
  6. AreaCard,
  7. AreaLineData,
  8. AreaParams,
  9. } from "@/api/areaboard/types";
  10. import AreaDataCard from "@/views/areaboard/components/AreaDataCard.vue";
  11. import LiquidChart from "@/components/Charts/LiquidChart.vue";
  12. import PercentBarChart from "@/components/Charts/PercentBarChart.vue";
  13. import CircleChart from "@/components/Charts/CircleChart.vue";
  14. import LineChart from "@/components/Charts/LineChart.vue";
  15. import AverageBarChart from "@/components/Charts/AverageBarChart.vue";
  16. import { GradeList } from "@/api/grade/types";
  17. import { getGradeSelect } from "@/api/grade";
  18. import {
  19. getAreaAddress,
  20. getAreaBoardLines,
  21. getAreaBoardPies,
  22. getAreaCard,
  23. getAreaSchool,
  24. } from "@/api/areaboard";
  25. import { SchoolList } from "@/api/school/types";
  26. defineOptions({
  27. name: "DashboardArea",
  28. inheritAttrs: false,
  29. });
  30. /**
  31. * 筛选条件
  32. */
  33. const address: AddressId = reactive({
  34. province_id: 0,
  35. city_id: 0,
  36. });
  37. const dataParams: AreaParams = reactive({
  38. province_id: 0,
  39. city_id: 0,
  40. school_id: 0,
  41. grade_id: 0,
  42. start_time: Math.ceil(Date.parse("2023/1/1 00:00") / 1000),
  43. end_time: Math.ceil(Date.now() / 1000),
  44. });
  45. const datePicker = ref<number[]>([Date.parse("2023/1/1 00:00"), Date.now()]);
  46. const provinceData = ref<Area[]>();
  47. const cityData = ref<Area[]>();
  48. async function getAddressData(id: number) {
  49. getAreaAddress(id)
  50. .then(({ data }) => {
  51. if (id == 0) {
  52. provinceData.value = data;
  53. provinceData.value?.unshift({ area_id: 0, area_name: "全部省" });
  54. } else {
  55. cityData.value = data;
  56. cityData.value?.unshift({ area_id: 0, area_name: "全部市" });
  57. }
  58. // 重新获取学校
  59. getSchoolData(dataParams);
  60. })
  61. .catch((error) => {
  62. console.log(error);
  63. if (id == 0) {
  64. provinceData.value = [{ area_id: 0, area_name: "全部省" }];
  65. } else {
  66. cityData.value = [{ area_id: 0, area_name: "全部市" }];
  67. }
  68. });
  69. }
  70. //getAreaSchool
  71. /**
  72. * 获取学校
  73. */
  74. const schoolData = ref<SchoolList[]>();
  75. async function getSchoolData(params: AreaParams) {
  76. getAreaSchool(params.province_id, params.city_id)
  77. .then(({ data }) => {
  78. schoolData.value = data;
  79. schoolData.value?.unshift({ num: "", school_id: 0, name: "全部学校" });
  80. })
  81. .catch((error) => {
  82. schoolData.value = [];
  83. schoolData.value?.unshift({ num: "", school_id: 0, name: "全部学校" });
  84. console.log(error);
  85. });
  86. }
  87. /**
  88. * 班级数据
  89. */
  90. const gradeData = ref<GradeList[]>();
  91. async function getGradeData(schoolId: number) {
  92. getGradeSelect(schoolId)
  93. .then(({ data }) => {
  94. gradeData.value = data;
  95. gradeData.value?.unshift({ id: 0, name: "全部班级" });
  96. })
  97. .catch((error) => {
  98. gradeData.value = [];
  99. gradeData.value?.unshift({ id: 0, name: "全部班级" });
  100. console.log(error);
  101. });
  102. }
  103. // 改变时间
  104. function changeDate() {
  105. dataParams.start_time = Math.ceil(datePicker.value[0] / 1000);
  106. dataParams.end_time = Math.ceil(datePicker.value[1] / 1000);
  107. }
  108. /**
  109. * 数据卡片
  110. */
  111. const cardStatus = ref(false);
  112. const cards = ref<AreaCard>();
  113. async function getDataCard(params: AreaParams) {
  114. getAreaCard(params)
  115. .then(({ data }) => {
  116. cards.value = data;
  117. cardStatus.value = true;
  118. })
  119. .catch((error) => {
  120. console.log(error);
  121. });
  122. }
  123. /**
  124. * 饼图数据
  125. */
  126. const pieStatus = ref(false);
  127. const pieMessage = ref("加载中...");
  128. const pieChartData = ref<DashboardData>();
  129. async function getPieChartData(params: AreaParams) {
  130. pieStatus.value = false;
  131. getAreaBoardPies(params)
  132. .then(({ data }) => {
  133. pieChartData.value = data;
  134. pieStatus.value = true;
  135. })
  136. .catch((error) => {
  137. pieStatus.value = false;
  138. pieMessage.value = error.message;
  139. console.log(error.message);
  140. });
  141. }
  142. /**
  143. * 折线图数据
  144. */
  145. const lineStatus = ref(false);
  146. const lineMessage = ref("加载中...");
  147. const lineChartData = ref<AreaLineData>();
  148. const averageData = ref<number[][]>();
  149. async function getLineChartData(params: AreaParams) {
  150. lineStatus.value = false;
  151. getAreaBoardLines(params)
  152. .then(({ data }) => {
  153. lineChartData.value = data;
  154. // 柱状图
  155. averageData.value = [];
  156. averageData.value?.push(lineChartData.value?.frontNum || []);
  157. averageData.value?.push(lineChartData.value?.afterNum || []);
  158. lineStatus.value = true;
  159. })
  160. .catch((error) => {
  161. lineStatus.value = false;
  162. lineMessage.value = error.message;
  163. console.log(error.message);
  164. });
  165. }
  166. /**
  167. * 获取页面数据
  168. */
  169. function getPageData() {
  170. // 数据卡片
  171. getDataCard(dataParams);
  172. // 饼图数据
  173. getPieChartData(dataParams);
  174. // 折线图数据
  175. getLineChartData(dataParams);
  176. }
  177. onMounted(() => {
  178. // 获取省份
  179. getAddressData(0);
  180. cityData.value = [{ area_id: 0, area_name: "全部市" }];
  181. // 获取学校
  182. getSchoolData(dataParams);
  183. gradeData.value = [{ id: 0, name: "全部班级" }];
  184. // 获取页面数据
  185. getPageData();
  186. });
  187. </script>
  188. <template>
  189. <div class="area-container">
  190. <div class="area-top">
  191. <div class="search-box">
  192. <el-select
  193. v-model="dataParams.province_id"
  194. placeholder="全部省"
  195. size="large"
  196. @change="getAddressData(dataParams.province_id)"
  197. >
  198. <el-option
  199. v-for="item in provinceData"
  200. :key="item.area_id"
  201. :label="item.area_name"
  202. :value="item.area_id"
  203. />
  204. </el-select>
  205. <el-select
  206. v-model="dataParams.city_id"
  207. placeholder="全部市"
  208. size="large"
  209. @change="getSchoolData(dataParams)"
  210. >
  211. <el-option
  212. v-for="item in cityData"
  213. :key="item.area_id"
  214. :label="item.area_name"
  215. :value="item.area_id"
  216. />
  217. </el-select>
  218. <div>
  219. <el-date-picker
  220. v-model="datePicker"
  221. type="daterange"
  222. size="large"
  223. start-placeholder="开始日期"
  224. end-placeholder="结束日期"
  225. format="YYYY-MM-DD"
  226. value-format="x"
  227. @change="changeDate()"
  228. />
  229. </div>
  230. <el-button size="large" color="#4284f2" @click="getPageData()"
  231. >查找</el-button
  232. >
  233. </div>
  234. <div class="card-box">
  235. <!-- 数据卡片 -->
  236. <template v-if="cardStatus">
  237. <AreaDataCard
  238. :key="cards.toString()"
  239. :schools="cards?.school || 0"
  240. :games="cards?.game || 0"
  241. :students="cards?.student || 0"
  242. />
  243. </template>
  244. </div>
  245. </div>
  246. <div class="search-box s2">
  247. <el-select
  248. v-model="dataParams.school_id"
  249. placeholder="全部学校"
  250. size="large"
  251. @change="getGradeData(dataParams.school_id)"
  252. >
  253. <el-option
  254. v-for="item in schoolData"
  255. :key="item.school_id"
  256. :label="item.name"
  257. :value="item.school_id"
  258. />
  259. </el-select>
  260. <el-select
  261. v-model="dataParams.grade_id"
  262. placeholder="全部班级"
  263. size="large"
  264. >
  265. <el-option
  266. v-for="item in gradeData"
  267. :key="item.id"
  268. :label="item.name"
  269. :value="item.id"
  270. />
  271. </el-select>
  272. <el-button size="large" color="#4284f2" @click="getPageData()"
  273. >查找</el-button
  274. >
  275. </div>
  276. <!-- Echarts 图表 -->
  277. <div class="charts-container">
  278. <el-row :gutter="20">
  279. <el-col :xs="24" :span="8">
  280. <div class="charts-item">
  281. <p class="title">学员专注力平均值整体对比分析</p>
  282. <template v-if="pieStatus">
  283. <el-row justify="space-between">
  284. <el-col :span="12">
  285. <div class="item">
  286. <LiquidChart
  287. id="liquidChart1"
  288. :key="pieChartData?.frontAverage"
  289. :data="pieChartData?.frontAverage || 0"
  290. height="200px"
  291. width="200px"
  292. color="#3a7fc2"
  293. bg-color="#accded"
  294. class="chart"
  295. />
  296. <p>全体学员初期</p>
  297. <p>专注力评估均值</p>
  298. </div>
  299. </el-col>
  300. <el-col :span="12">
  301. <div class="item">
  302. <LiquidChart
  303. id="liquidChart2"
  304. :key="pieChartData?.afterAverage"
  305. :data="pieChartData?.afterAverage || 0"
  306. height="200px"
  307. width="200px"
  308. color="#5563ac"
  309. bg-color="#cacce6"
  310. class="chart"
  311. />
  312. <p>全体学员训练近期</p>
  313. <p>专注力评估均值</p>
  314. </div>
  315. </el-col>
  316. </el-row>
  317. <el-row justify="space-between">
  318. <el-col :span="12">
  319. <div class="item">
  320. <CircleChart
  321. id="circleChart1"
  322. :key="pieChartData?.front"
  323. :data="pieChartData?.front || 0"
  324. height="200px"
  325. width="200px"
  326. color="#3a7fc2"
  327. bg-color="#e4e7f4"
  328. font-color="#3a7fc2"
  329. font-size="30px"
  330. :round-cap="Boolean(true)"
  331. />
  332. <p>初期训练</p>
  333. <p>专注力50以上人数比例</p>
  334. </div>
  335. </el-col>
  336. <el-col :span="12">
  337. <div class="item">
  338. <CircleChart
  339. id="circleChart2"
  340. :key="pieChartData?.after"
  341. :data="pieChartData?.after || 0"
  342. height="200px"
  343. width="200px"
  344. color="#5563ac"
  345. bg-color="#e4e7f4"
  346. font-color="#5563ac"
  347. font-size="30px"
  348. :round-cap="Boolean(true)"
  349. />
  350. <p>近期训练</p>
  351. <p>专注力50以上人数比例</p>
  352. </div>
  353. </el-col>
  354. </el-row>
  355. </template>
  356. <div v-else class="empty">
  357. <img src="../../assets/empty.png" alt="数据为空" />
  358. <p>{{ pieMessage }}</p>
  359. </div>
  360. </div>
  361. </el-col>
  362. <!-- 学员专注力评分分级占比分析 -->
  363. <el-col :xs="24" :span="16">
  364. <div class="charts-item">
  365. <p class="title">样本每次训练专注力评分均值整体变化曲线</p>
  366. <template v-if="lineStatus">
  367. <line-chart
  368. id="lineChart1"
  369. :data="lineChartData?.curve"
  370. width="100%"
  371. height="558px"
  372. />
  373. </template>
  374. <div v-else class="empty">
  375. <img src="../../assets/empty.png" alt="数据为空" />
  376. <p>{{ lineMessage }}</p>
  377. </div>
  378. </div>
  379. </el-col>
  380. </el-row>
  381. </div>
  382. <div class="charts-container">
  383. <el-row :gutter="20">
  384. <el-col :xs="24" :span="8">
  385. <div class="charts-item">
  386. <p class="title pos">学员专注力训练高专注占比分析</p>
  387. <template v-if="lineStatus">
  388. <AverageBarChart
  389. id="averageBarChart1"
  390. :key="averageData.toString()"
  391. :data-sets="averageData"
  392. width="520px"
  393. height="520px"
  394. class="chart"
  395. />
  396. </template>
  397. <div v-else class="empty">
  398. <img src="../../assets/empty.png" alt="数据为空" />
  399. <p>{{ lineMessage }}</p>
  400. </div>
  401. <el-row :gutter="15" class="bottom">
  402. <el-col :span="12">
  403. <p class="l">
  404. <span>训练前期全体学员</span>
  405. <span>高专注占比平均值</span>
  406. <b>{{ lineChartData?.frontHeight }}</b>
  407. </p>
  408. </el-col>
  409. <el-col :span="12">
  410. <p class="r">
  411. <span>训练后期全体学员</span>
  412. <span>高专注占比平均值</span>
  413. <b>{{ lineChartData?.afterHeight }}</b>
  414. </p>
  415. </el-col>
  416. </el-row>
  417. </div>
  418. </el-col>
  419. <!-- 学员专注力评分分级占比分析 -->
  420. <el-col :xs="24" :span="16">
  421. <div class="charts-item">
  422. <p class="title">学员专注力评分分级占比分析</p>
  423. <el-row v-if="pieStatus" justify="space-between">
  424. <el-col :xs="24" :span="12">
  425. <div class="bar">
  426. <PercentBarChart
  427. id="barChart1"
  428. :key="pieChartData?.frontProportion.toString()"
  429. width="400px"
  430. height="500px"
  431. title="全体学员初期训练专注力评分占比"
  432. :percent="pieChartData?.frontProportion.percentage"
  433. :data="pieChartData?.frontProportion.num"
  434. class="chart"
  435. />
  436. </div>
  437. </el-col>
  438. <el-col :xs="24" :span="12">
  439. <div class="bar">
  440. <PercentBarChart
  441. id="barChart2"
  442. :key="pieChartData?.afterProportion.toString()"
  443. width="400px"
  444. height="500px"
  445. title="全体学员训练近期专注力评分平均占比"
  446. :percent="pieChartData?.afterProportion.percentage"
  447. :data="pieChartData?.afterProportion.num"
  448. class="chart"
  449. />
  450. </div>
  451. </el-col>
  452. </el-row>
  453. <div v-else class="empty">
  454. <img src="../../assets/empty.png" alt="数据为空" />
  455. <p>{{ pieMessage }}</p>
  456. </div>
  457. </div>
  458. </el-col>
  459. </el-row>
  460. </div>
  461. </div>
  462. </template>
  463. <style lang="scss" scoped>
  464. .area-top {
  465. background: #fff;
  466. }
  467. .card-box {
  468. padding: 0 30px 20px;
  469. }
  470. .search-box {
  471. display: flex;
  472. padding: 20px 55px;
  473. line-height: 40px;
  474. .el-select {
  475. width: 140px;
  476. margin-right: 10px;
  477. }
  478. .el-button {
  479. padding: 0 26px;
  480. margin: 0 20px;
  481. font-size: 16px;
  482. border-radius: 10px;
  483. }
  484. }
  485. :deep(.el-select),
  486. :deep(.el-date-editor) {
  487. --el-select-input-focus-border-color: none !important;
  488. width: 300px;
  489. margin: 0;
  490. overflow: hidden;
  491. border: 1px solid #ddd;
  492. border-radius: 10px;
  493. }
  494. :deep(.search-box.s2 .el-select) {
  495. border: none;
  496. }
  497. :deep(.el-date-editor) {
  498. --el-select-input-focus-border-color: none !important;
  499. }
  500. :deep(.el-input__wrapper) {
  501. box-shadow: none !important;
  502. }
  503. :deep(.el-input__wrapper.is-focus) {
  504. box-shadow: none !important;
  505. }
  506. :deep(.el-select:hover:not(.el-select--disabled) .el-input__wrapper) {
  507. box-shadow: none !important;
  508. }
  509. .charts-container {
  510. position: relative;
  511. padding: 0 30px 20px;
  512. }
  513. .charts-item {
  514. position: relative;
  515. text-align: center;
  516. background: #fff;
  517. border: 1px solid #e8eaec;
  518. border-radius: 24px;
  519. .title {
  520. height: 78px;
  521. margin: 0;
  522. font-size: 18px;
  523. line-height: 78px;
  524. text-align: left;
  525. text-indent: 2em;
  526. &.pos {
  527. margin-bottom: -50px;
  528. }
  529. }
  530. .item {
  531. padding-bottom: 30px;
  532. }
  533. .chart {
  534. margin: 0 auto;
  535. }
  536. p {
  537. margin: 0;
  538. font-size: 16px;
  539. line-height: 24px;
  540. }
  541. .bar {
  542. margin-top: 60px;
  543. }
  544. .bottom {
  545. padding: 0 20px 20px;
  546. .el-col p {
  547. position: relative;
  548. box-sizing: border-box;
  549. padding: 10px 20px;
  550. color: #fff;
  551. text-align: left;
  552. white-space: nowrap;
  553. border-radius: 10px;
  554. &.l {
  555. background: #f8b865;
  556. }
  557. &.r {
  558. background: #8877ef;
  559. }
  560. span {
  561. display: block;
  562. }
  563. b {
  564. position: absolute;
  565. top: 22px;
  566. right: 20px;
  567. font-size: 26px;
  568. font-style: normal;
  569. }
  570. }
  571. }
  572. .empty {
  573. box-sizing: border-box;
  574. height: 400px;
  575. padding-top: 200px;
  576. text-align: center;
  577. background-image: url("../../assets/empty.png");
  578. background-repeat: no-repeat;
  579. background-position: center 30%;
  580. p {
  581. font-size: 14px;
  582. color: #a1a1a1;
  583. &.red {
  584. color: #e04962;
  585. }
  586. }
  587. }
  588. }
  589. .mobile .el-col {
  590. margin-bottom: 10px;
  591. }
  592. .empty {
  593. padding: 200px 0;
  594. }
  595. </style>