Echart3.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. <template>
  2. <div class="content-box1">
  3. <div class="chart-wrap">
  4. <!-- 右上角表头 -->
  5. <div class="chart-header">
  6. <span class="h-rate">入住率</span>
  7. <span class="h-checked">已入住数</span>
  8. <span class="h-slash">/</span>
  9. <span class="h-total">床位开放总数</span>
  10. </div>
  11. <!-- ECharts 挂载点 -->
  12. <div ref="stayChartRef" class="chart-inner"></div>
  13. </div>
  14. </div>
  15. </template>
  16. <script setup>
  17. import { ref, onMounted, watch, onUnmounted, nextTick, computed } from "vue";
  18. import * as echarts from "echarts";
  19. import { https } from "@/utils/request";
  20. import { ElMessage } from "element-plus";
  21. const stayChartRef = ref(null);
  22. let stayChartInstance = null;
  23. const props = defineProps({
  24. yearId: {
  25. type: [String, Number],
  26. default: "",
  27. },
  28. });
  29. const stayData = ref([]);
  30. // 安全计算Y轴标签,空数据不会报错
  31. const yData = computed(() => {
  32. if (!Array.isArray(stayData.value) || stayData.value.length === 0) return [];
  33. return stayData.value.map((item) => `${item.college}|${item.gender}`);
  34. });
  35. // 学院分隔虚线
  36. const drawSeparators = () => {
  37. if (!stayChartInstance || !stayData.value.length) return;
  38. const lines = [];
  39. for (let i = 1; i < stayData.value.length; i += 2) {
  40. if (i + 1 >= stayData.value.length) continue;
  41. const y1 = stayChartInstance.convertToPixel({ yAxisIndex: 0 }, i);
  42. const y2 = stayChartInstance.convertToPixel({ yAxisIndex: 0 }, i + 1);
  43. const x1 = stayChartInstance.convertToPixel({ xAxisIndex: 0 }, 0);
  44. const x2 = stayChartInstance.convertToPixel({ xAxisIndex: 0 }, 100);
  45. if (y1 && y2 && x1 && x2) {
  46. lines.push({
  47. type: "line",
  48. shape: { x1, y1: (y1 + y2) / 2, x2, y2: (y1 + y2) / 2 },
  49. style: { stroke: "#ddd", lineWidth: 1, lineDash: [4, 4] },
  50. z: 100,
  51. silent: true,
  52. });
  53. }
  54. }
  55. stayChartInstance.setOption({ graphic: lines }, false);
  56. };
  57. // 图表初始化函数,增加DOM判空兜底
  58. const stayInitChart = async (yearId) => {
  59. // 核心修复:先判断DOM是否存在,不存在直接终止,防止invalid dom
  60. if (!stayChartRef.value) return;
  61. // 实例存在则销毁重建,避免重复初始化报错
  62. if (stayChartInstance) {
  63. stayChartInstance.dispose();
  64. stayChartInstance = null;
  65. }
  66. stayChartInstance = echarts.init(stayChartRef.value);
  67. let params = { yearId };
  68. let res = await https.get("/welcome/api/welcomeStudent/collegeStudentStay", "params", params);
  69. console.log(res, "各学院住宿情况统计");
  70. if (res.code == 200) {
  71. const rawList = res.data;
  72. // 空数据兜底模板
  73. const emptyTemplate = [
  74. { college: "交通运输学院", gender: "男", checked: 0, total: 0, rate: 0 },
  75. { college: "交通运输学院", gender: "女", checked: 0, total: 0, rate: 0 },
  76. { college: "人工智能学院", gender: "男", checked: 0, total: 0, rate: 0 },
  77. { college: "人工智能学院", gender: "女", checked: 0, total: 0, rate: 0 },
  78. { college: "智能制造学院", gender: "男", checked: 0, total: 0, rate: 0 },
  79. { college: "智能制造学院", gender: "女", checked: 0, total: 0, rate: 0 },
  80. { college: "商学院", gender: "男", checked: 0, total: 0, rate: 0 },
  81. { college: "商学院", gender: "女", checked: 0, total: 0, rate: 0 },
  82. { college: "交通与土木工程学院", gender: "男", checked: 0, total: 0, rate: 0 },
  83. { college: "交通与土木工程学院", gender: "女", checked: 0, total: 0, rate: 0 },
  84. { college: "艺术与体育", gender: "男", checked: 0, total: 0, rate: 0 },
  85. { college: "艺术与体育", gender: "女", checked: 0, total: 0, rate: 0 },
  86. { college: "文法学院", gender: "男", checked: 0, total: 0, rate: 0 },
  87. { college: "文法学院", gender: "女", checked: 0, total: 0, rate: 0 },
  88. { college: "数智五金产业学院", gender: "男", checked: 0, total: 0, rate: 0 },
  89. { college: "数智五金产业学院", gender: "女", checked: 0, total: 0, rate: 0 },
  90. ];
  91. if (!rawList || rawList.length === 0) {
  92. stayData.value = emptyTemplate;
  93. } else {
  94. const tempArr = [];
  95. const sortOrder = [
  96. "交通运输学院",
  97. "人工智能学院",
  98. "智能制造学院",
  99. "商学院",
  100. "交通与土木工程学院",
  101. "艺术与体育",
  102. "文法学院",
  103. "数智五金产业学院",
  104. ];
  105. rawList.forEach((item) => {
  106. let name = item.collegeName === "艺术与体育学院" ? "艺术与体育" : item.collegeName;
  107. tempArr.push({
  108. college: name,
  109. gender: "男",
  110. checked: item.maleCheck,
  111. total: item.maleTotal,
  112. rate: item.maleRate,
  113. });
  114. tempArr.push({
  115. college: name,
  116. gender: "女",
  117. checked: item.femaleCheck,
  118. total: item.femaleTotal,
  119. rate: item.femaleRate,
  120. });
  121. });
  122. tempArr.sort((a) => sortOrder.indexOf(a.college));
  123. stayData.value = tempArr;
  124. }
  125. const rateList = stayData.value.map((item) => item.rate);
  126. const bgGrayList = stayData.value.map(() => 100);
  127. const option = {
  128. tooltip: {
  129. trigger: "axis",
  130. axisPointer: { type: "shadow" },
  131. formatter: (params) => {
  132. const row = stayData.value[params[0].dataIndex];
  133. return `${row.college} - ${row.gender}<br/>入住率:${row.rate}%<br/>已入住:${row.checked}<br/>床位总数:${row.total}`;
  134. },
  135. },
  136. grid: {
  137. left: 10,
  138. right: 230, // 加宽右侧预留空间,文字不会挤压
  139. top: 40,
  140. bottom: 10,
  141. containLabel: true,
  142. },
  143. xAxis: {
  144. type: "value",
  145. max: 100,
  146. axisLine: { show: false },
  147. axisTick: { show: false },
  148. splitLine: { show: false },
  149. axisLabel: { show: false },
  150. },
  151. yAxis: {
  152. type: "category",
  153. inverse: true,
  154. data: yData.value,
  155. axisLine: { show: false },
  156. axisTick: { show: false },
  157. splitLine: { show: false },
  158. axisLabel: {
  159. margin: 12,
  160. formatter: (value) => {
  161. const [college, gender] = value.split("|");
  162. return gender === "男"
  163. ? `{college|${college}}{gap|}{genderMale|男}`
  164. : `{spacer|}{gap|}{genderFemale|女}`;
  165. },
  166. rich: {
  167. college: { color: "#333", fontSize: 14, width: 140 },
  168. spacer: { width: 140 },
  169. gap: { width: 10 },
  170. genderMale: { color: "#2F7FFF", fontSize: 12, width: 20 },
  171. genderFemale: { color: "#FF6B6B", fontSize: 12, width: 20 },
  172. },
  173. },
  174. },
  175. series: [
  176. {
  177. // 灰色 100% 背景条 —— label 挂在这里,不管数据有没有都在右侧
  178. name: "总床位基底",
  179. type: "bar",
  180. data: bgGrayList,
  181. barWidth: 8,
  182. itemStyle: { color: "#E8E8E8", borderRadius: [0, 4, 4, 0] },
  183. silent: true,
  184. animation: false,
  185. label: {
  186. show: true,
  187. position: "right",
  188. distance: 20,
  189. formatter: (params) => {
  190. const row = stayData.value[params.dataIndex];
  191. const colorKey = row.gender === "男" ? "blueNum" : "redNum";
  192. return `{rateTxt|${row.rate}%} {${colorKey}|${row.checked}}{slash| / }{totalTxt|${row.total}}`;
  193. },
  194. rich: {
  195. rateTxt: {
  196. color: "#d93025",
  197. fontSize: 13,
  198. fontWeight: "bold",
  199. width: 40,
  200. },
  201. blueNum: {
  202. color: "#2F7FFF",
  203. fontSize: 13,
  204. fontWeight: "bold",
  205. width: 60,
  206. align: "right",
  207. },
  208. redNum: {
  209. color: "#FF6B6B",
  210. fontSize: 13,
  211. fontWeight: "bold",
  212. width: 60,
  213. align: "right",
  214. },
  215. slash: {
  216. color: "#333",
  217. fontSize: 13,
  218. width: 15,
  219. align: "center",
  220. },
  221. totalTxt: {
  222. color: "#333",
  223. fontSize: 13,
  224. fontWeight: "bold",
  225. width: 80,
  226. },
  227. },
  228. },
  229. },
  230. {
  231. // 已入住占比:男蓝女橙
  232. name: "已入住占比",
  233. type: "bar",
  234. data: rateList,
  235. barWidth: 8,
  236. barGap: "-100%",
  237. itemStyle: {
  238. color: (params) => stayData.value[params.dataIndex].gender === "男" ? "#2F7FFF" : "#FF6B6B",
  239. borderRadius: [0, 4, 4, 0],
  240. },
  241. label: { show: false },
  242. },
  243. ],
  244. };
  245. stayChartInstance.setOption(option);
  246. stayChartInstance.on("finished", function drawOnce() {
  247. setTimeout(drawSeparators, 0);
  248. stayChartInstance.off("finished", drawOnce);
  249. });
  250. } else {
  251. ElMessage({ type: "error", showClose: true, message: res.message, center: true });
  252. }
  253. };
  254. const handleResize = () => {
  255. if (stayChartRef.value && stayChartInstance) {
  256. stayChartInstance.resize();
  257. stayChartInstance.on("finished", function drawOnce() {
  258. setTimeout(drawSeparators, 0);
  259. stayChartInstance.off("finished", drawOnce);
  260. });
  261. }
  262. };
  263. // 监听年份,去掉immediate,挂载完成后再执行
  264. watch(
  265. () => props.yearId,
  266. (newVal) => {
  267. nextTick(() => stayInitChart(newVal));
  268. }
  269. );
  270. onMounted(() => {
  271. // 仅DOM挂载完成后执行初始化,彻底解决DOM不存在问题
  272. nextTick(() => stayInitChart(props.yearId));
  273. window.addEventListener("resize", handleResize);
  274. });
  275. onUnmounted(() => {
  276. if (stayChartInstance) stayChartInstance.dispose();
  277. window.removeEventListener("resize", handleResize);
  278. });
  279. </script>
  280. <style scoped lang="scss">
  281. .content-box1 {
  282. width: calc(100% - 40px);
  283. height: calc(100% - 105px);
  284. margin: 20px auto 0;
  285. background-color: #ffffff;
  286. color: #000;
  287. display: flex;
  288. flex-direction: column;
  289. overflow: auto;
  290. .system_title {
  291. height: 45px;
  292. border-radius: 0 12px 0 0;
  293. background: rgb(243, 249, 255);
  294. margin-bottom: 15px;
  295. display: flex;
  296. align-items: center;
  297. h4 {
  298. font-size: 15px;
  299. padding: 0 5px 0 20px;
  300. margin: 0;
  301. }
  302. }
  303. .chart-wrap {
  304. position: relative;
  305. width: 100%;
  306. height: 480px;
  307. }
  308. .chart-inner {
  309. width: 100%;
  310. height: 100%;
  311. }
  312. .chart-header {
  313. position: absolute;
  314. top: 10px;
  315. right: 20px;
  316. width: 260px; /* 和grid.right保持一致,表头对齐下方文字 */
  317. display: flex;
  318. align-items: center;
  319. justify-content: flex-end;
  320. white-space: nowrap;
  321. font-size: 13px;
  322. color: #666;
  323. font-weight: bold;
  324. z-index: 10;
  325. user-select: none;
  326. pointer-events: none;
  327. span {
  328. display: inline-block;
  329. flex-shrink: 0;
  330. }
  331. .h-rate { width: 70px; text-align: right; }
  332. .h-checked { width: 70px; text-align: right; }
  333. .h-slash { width: 25px; text-align: center; }
  334. .h-total { width: 60px; text-align: left; }
  335. }
  336. }
  337. </style>