chartList.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. <template>
  2. <div class="chart-list">
  3. <div class="chart-item">
  4. <chart-box :name="`${props.areaName}作物种植面积占比`">
  5. <div class="box-content">
  6. <div class="chart-dom">
  7. <pie-chart :chartData="pieChartData" :totalArea="totalArea"></pie-chart>
  8. </div>
  9. <div class="box-bg">
  10. <div class="legend-list">
  11. <div class="legend-item" v-for="(item, index) in legendData" :key="index">
  12. <span class="dot" :style="{ background: item.color }"></span>
  13. <span class="text">
  14. {{ item.name }}
  15. <span class="percent">{{ item.percent }}%</span>
  16. <span class="line">|</span>
  17. <span class="value">{{ item.value }}亩</span>
  18. </span>
  19. </div>
  20. </div>
  21. </div>
  22. </div>
  23. </chart-box>
  24. </div>
  25. <div class="chart-item">
  26. <chart-box :name="twoTitle">
  27. <div class="box-content">
  28. <div class="chart-dom">
  29. <bar-chart :key="0" :chartData="regionChartData"></bar-chart>
  30. </div>
  31. <div class="box-bg">{{ regionSummaryText }}</div>
  32. </div>
  33. </chart-box>
  34. </div>
  35. <div class="chart-item" v-if="activeBaseTab === '作物分布'">
  36. <chart-box name="近三年主要作物种植面积变化趋势">
  37. <div class="box-content">
  38. <div class="chart-dom">
  39. <line-chart :chartData="areaTrendChartData"></line-chart>
  40. </div>
  41. <div class="box-bg">暂无数据</div>
  42. </div>
  43. </chart-box>
  44. </div>
  45. <div class="chart-item">
  46. <chart-box :name="threeTitle">
  47. <div class="box-content">
  48. <div class="chart-dom">
  49. <bar-chart :key="1" :chartData="yieldChartData" :yAxisFormatter="yAxisFormatter"></bar-chart>
  50. </div>
  51. <div class="box-bg">{{ yieldSummaryText }}</div>
  52. </div>
  53. </chart-box>
  54. </div>
  55. </div>
  56. </template>
  57. <script setup>
  58. import chartBox from "@/components/chartBox.vue";
  59. import pieChart from "./pieChart.vue";
  60. import lineChart from "./lineChart.vue";
  61. import { computed, onMounted, ref, watch } from "vue";
  62. import { pieOption } from "./chartOption.js";
  63. import barChart from "./barChart.vue";
  64. const props = defineProps({
  65. activeBaseTab: {
  66. type: String,
  67. default: "作物分布",
  68. },
  69. areaCode: {
  70. type: String,
  71. default: "156440000",
  72. },
  73. areaName: {
  74. type: String,
  75. default: "广东省",
  76. },
  77. });
  78. // 图表数据
  79. const pieChartData = ref([]);
  80. const totalArea = ref(0);
  81. // 区域占比图表数据
  82. const regionChartData = ref({
  83. categories: [], // 区域名称数组
  84. values: [], // 占比百分比数组
  85. });
  86. // 区域占比摘要文字
  87. const regionSummaryText = ref("暂无数据");
  88. // 预估产量图表数据
  89. const yieldChartData = ref({
  90. categories: [], // 作物名称数组
  91. values: [], // 预估产量数组(单位:吨)
  92. });
  93. // 预估产量摘要文字
  94. const yieldSummaryText = ref("暂无数据");
  95. // 根据 activeBaseTab 动态设置 y 轴单位
  96. const yAxisFormatter = computed(() => {
  97. return props.activeBaseTab === "物候期分布" ? "{value}亩" : "{value}吨";
  98. });
  99. // 种植面积趋势图表数据
  100. const areaTrendChartData = ref({
  101. xAxisData: [], // 时间轴数据
  102. series: [], // 系列数据
  103. });
  104. // 计算图例数据
  105. const legendData = computed(() => {
  106. if (!pieChartData.value.length || totalArea.value === 0) {
  107. return [];
  108. }
  109. return pieChartData.value.map((item, index) => {
  110. const percent = ((item.value / totalArea.value) * 100).toFixed(1);
  111. return {
  112. name: item.name,
  113. value: Math.round(item.value), // 显示整数
  114. percent: percent,
  115. color: pieOption.color[index % pieOption.color.length],
  116. };
  117. });
  118. });
  119. const twoTitle = ref(`${props.areaName}作物区域占比`);
  120. const threeTitle = ref(`${props.areaName}作物预估产量对比`);
  121. // 监听 activeBaseTab 变化
  122. watch(
  123. () => props.activeBaseTab,
  124. () => {
  125. initData();
  126. }
  127. );
  128. // 监听 areaCode 变化,当切换到物候期分布时重新获取数据
  129. watch(
  130. () => props.areaCode,
  131. (newVal) => {
  132. if (newVal) {
  133. initData();
  134. }
  135. }
  136. );
  137. const initData = () => {
  138. if (props.activeBaseTab === "物候期分布") {
  139. twoTitle.value = `${props.areaName}小麦物候进程分布`;
  140. threeTitle.value = `${props.areaName}小麦预告产量统计`;
  141. fetchStatPhenologyRatio();
  142. fetchStatRegionYieldRatio();
  143. } else {
  144. twoTitle.value = `${props.areaName}作物区域占比`;
  145. threeTitle.value = `${props.areaName}作物预估产量对比`;
  146. fetchStatSpeciesAreaYield();
  147. fetchStatRegionAreaRatio();
  148. fetchAreaTrend();
  149. }
  150. };
  151. //统计指定物种在下级区划中的预估产量占比
  152. const fetchStatRegionYieldRatio = () => {
  153. const params = {
  154. speciesId: 1,
  155. adminCode: props.areaCode,
  156. adminLevel: "province",
  157. };
  158. VE_API.warning
  159. .fetchStatRegionYieldRatio(params)
  160. .then((res) => {
  161. if (res.code === 0 && res.data && res.data.length > 0) {
  162. // 转换接口数据为图表格式
  163. const categories = res.data.map((item) => item.adminName);
  164. // 如果是物候期分布,使用 expectYield(但单位显示为亩),否则使用 expectYield(单位显示为吨)
  165. const values = res.data.map((item) => parseFloat(item.expectYield.toFixed(2)));
  166. // 更新图表数据
  167. yieldChartData.value = {
  168. categories,
  169. values,
  170. };
  171. // 找到最大值的区域
  172. let maxValue = 0;
  173. let maxRegion = "";
  174. res.data.forEach((item) => {
  175. if (item.expectYield > maxValue) {
  176. maxValue = item.expectYield;
  177. maxRegion = item.adminName;
  178. }
  179. });
  180. // 更新摘要文字
  181. if (maxRegion) {
  182. const maxValueFormatted = maxValue.toFixed(1);
  183. const unit = props.activeBaseTab === "物候期分布" ? "亩" : "吨";
  184. yieldSummaryText.value = `${maxRegion}的${
  185. props.activeBaseTab === "物候期分布" ? "种植面积" : "预估产量"
  186. }最大,为${maxValueFormatted}${unit}`;
  187. } else {
  188. yieldSummaryText.value = "暂无数据";
  189. }
  190. } else {
  191. yieldChartData.value = {
  192. categories: [],
  193. values: [],
  194. };
  195. yieldSummaryText.value = "暂无数据";
  196. }
  197. })
  198. .catch((error) => {
  199. console.error("获取区域产量占比数据失败:", error);
  200. yieldChartData.value = {
  201. categories: [],
  202. values: [],
  203. };
  204. yieldSummaryText.value = "暂无数据";
  205. });
  206. };
  207. //统计指定物种在不同物候期下的面积占比
  208. const fetchStatPhenologyRatio = () => {
  209. const params = {
  210. speciesId: 1,
  211. adminCode: props.areaCode,
  212. adminLevel: "province",
  213. };
  214. VE_API.warning
  215. .fetchStatPhenologyRatio(params)
  216. .then((res) => {
  217. if (res.code === 0 && res.data && res.data.length > 0) {
  218. console.log(res.data);
  219. // 转换接口数据为图表格式
  220. const categories = res.data.map((item) => item.phenologyName);
  221. const values = res.data.map((item) => parseFloat((item.areaRatio * 100).toFixed(2))); // 转换为百分比,保留两位小数
  222. // 更新图表数据
  223. regionChartData.value = {
  224. categories,
  225. values,
  226. };
  227. // 找到最大占比的物候期
  228. let maxRatio = 0;
  229. let maxPhenology = "";
  230. res.data.forEach((item) => {
  231. if (item.areaRatio > maxRatio) {
  232. maxRatio = item.areaRatio;
  233. maxPhenology = item.phenologyName;
  234. }
  235. });
  236. // 更新摘要文字
  237. if (maxPhenology) {
  238. const maxPercent = (maxRatio * 100).toFixed(1);
  239. regionSummaryText.value = `${maxPhenology}的种植面积最大,占比${maxPercent}%`;
  240. } else {
  241. regionSummaryText.value = "暂无数据";
  242. }
  243. } else {
  244. regionChartData.value = {
  245. categories: [],
  246. values: [],
  247. };
  248. regionSummaryText.value = "暂无数据";
  249. }
  250. })
  251. .catch((error) => {
  252. console.error("获取物候期占比数据失败:", error);
  253. regionChartData.value = {
  254. categories: [],
  255. values: [],
  256. };
  257. regionSummaryText.value = "暂无数据";
  258. });
  259. };
  260. const fetchAreaTrend = () => {
  261. const params = {
  262. speciesIds: [1, 222, 60876],
  263. adminCode: props.areaCode,
  264. adminLevel: "province",
  265. };
  266. VE_API.warning.fetchAreaTrend(params).then((res) => {
  267. if (res.code === 0 && res.data && res.data.length > 0) {
  268. // 收集所有唯一的时间点(year-quarter组合)
  269. const timePointSet = new Set();
  270. res.data.forEach((species) => {
  271. species.timeSeries.forEach((item) => {
  272. const timeKey = `${item.year}Q${item.quarter}`;
  273. timePointSet.add(timeKey);
  274. });
  275. });
  276. // 转换为数组并排序
  277. const xAxisData = Array.from(timePointSet).sort((a, b) => {
  278. const [yearA, quarterA] = a.split("Q").map(Number);
  279. const [yearB, quarterB] = b.split("Q").map(Number);
  280. if (yearA !== yearB) return yearA - yearB;
  281. return quarterA - quarterB;
  282. });
  283. // 定义颜色映射(可以根据需要调整)
  284. const colorMap = {
  285. 荔枝: "#2199F8",
  286. 籼稻: "#178B00",
  287. 小麦: "#FAA53D",
  288. };
  289. // 为每个作物创建系列数据
  290. const series = res.data.map((species) => {
  291. // 创建时间点到种植面积的映射
  292. const dataMap = new Map();
  293. species.timeSeries.forEach((item) => {
  294. const timeKey = `${item.year}Q${item.quarter}`;
  295. dataMap.set(timeKey, item.plantArea);
  296. });
  297. // 根据 xAxisData 顺序生成数据数组
  298. const data = xAxisData.map((timeKey) => {
  299. return dataMap.has(timeKey) ? parseFloat(dataMap.get(timeKey).toFixed(2)) : null;
  300. });
  301. return {
  302. name: species.speciesName,
  303. type: "line",
  304. smooth: true,
  305. showSymbol: false,
  306. data: data,
  307. itemStyle: {
  308. color: colorMap[species.speciesName] || "#2199F8",
  309. },
  310. lineStyle: {
  311. color: colorMap[species.speciesName] || "#2199F8",
  312. },
  313. };
  314. });
  315. // 更新图表数据
  316. areaTrendChartData.value = {
  317. xAxisData,
  318. series,
  319. };
  320. } else {
  321. console.log("空数据");
  322. // 接口返回空数据时,清空图表数据
  323. areaTrendChartData.value = {
  324. xAxisData: [],
  325. series: [],
  326. };
  327. }
  328. }).catch(() => {
  329. // 错误时也清空数据
  330. areaTrendChartData.value = {
  331. xAxisData: [],
  332. series: [],
  333. };
  334. });
  335. };
  336. const fetchStatRegionAreaRatio = () => {
  337. const params = {
  338. speciesId: "1",
  339. adminCode: props.areaCode,
  340. adminLevel: "",
  341. };
  342. VE_API.warning.fetchStatRegionAreaRatio(params).then((res) => {
  343. if (res.code === 0 && res.data && res.data.length > 0) {
  344. // 转换接口数据为图表格式
  345. const categories = res.data.map((item) => item.adminName);
  346. const values = res.data.map((item) => (item.areaRatio * 100).toFixed(2)); // 转换为百分比,保留两位小数
  347. // 更新图表数据
  348. regionChartData.value = {
  349. categories,
  350. values: values.map((v) => parseFloat(v)), // 转换为数字
  351. };
  352. // 找到最大占比的区域
  353. let maxRatio = 0;
  354. let maxRegion = "";
  355. res.data.forEach((item) => {
  356. if (item.areaRatio > maxRatio) {
  357. maxRatio = item.areaRatio;
  358. maxRegion = item.adminName;
  359. }
  360. });
  361. // 更新摘要文字
  362. if (maxRegion) {
  363. const maxPercent = (maxRatio * 100).toFixed(1);
  364. regionSummaryText.value = `${maxRegion}的作物种植面积最大,占比${maxPercent}%`;
  365. } else {
  366. regionSummaryText.value = "暂无数据";
  367. }
  368. } else {
  369. // 接口返回空数据时,清空图表数据
  370. regionChartData.value = {
  371. categories: [],
  372. values: [],
  373. };
  374. regionSummaryText.value = "暂无数据";
  375. }
  376. }).catch((error) => {
  377. console.error("获取区域面积占比数据失败:", error);
  378. // 错误时也清空数据
  379. regionChartData.value = {
  380. categories: [],
  381. values: [],
  382. };
  383. regionSummaryText.value = "暂无数据";
  384. });
  385. };
  386. const fetchStatSpeciesAreaYield = () => {
  387. const params = {
  388. year: 2025,
  389. adminCode: props.areaCode,
  390. adminLevel: null,
  391. };
  392. VE_API.warning.fetchStatSpeciesAreaYield(params).then((res) => {
  393. if (res.code === 0 && res.data && res.data.length > 0) {
  394. // 转换接口数据为图表格式(用于饼图)
  395. const chartData = res.data.map((item) => ({
  396. value: item.plantArea, // 种植面积
  397. name: item.speciesName, // 作物名称
  398. expectYield: item.expectYield, // 预估产量
  399. speciesId: item.speciesId, // 作物ID
  400. }));
  401. // 计算总种植面积
  402. const total = chartData.reduce((sum, item) => sum + item.value, 0);
  403. // 更新饼图数据
  404. pieChartData.value = chartData;
  405. totalArea.value = total;
  406. // 处理预估产量数据(用于柱状图)
  407. const categories = res.data.map((item) => item.speciesName);
  408. const values = res.data.map((item) => parseFloat(item.expectYield.toFixed(2))); // 保留两位小数
  409. // 更新预估产量图表数据
  410. yieldChartData.value = {
  411. categories,
  412. values,
  413. };
  414. // 找到最大预估产量的作物
  415. let maxYield = 0;
  416. let maxSpecies = "";
  417. res.data.forEach((item) => {
  418. if (item.expectYield > maxYield) {
  419. maxYield = item.expectYield;
  420. maxSpecies = item.speciesName;
  421. }
  422. });
  423. // 更新摘要文字
  424. if (maxSpecies) {
  425. const maxYieldFormatted = maxYield.toFixed(1);
  426. yieldSummaryText.value = `${maxSpecies}的预估产量最高,为${maxYieldFormatted}吨`;
  427. } else {
  428. yieldSummaryText.value = "暂无数据";
  429. }
  430. } else {
  431. // 接口返回空数据时,清空图表数据
  432. pieChartData.value = [];
  433. totalArea.value = 0;
  434. yieldChartData.value = {
  435. categories: [],
  436. values: [],
  437. };
  438. yieldSummaryText.value = "暂无数据";
  439. }
  440. }).catch((error) => {
  441. console.error("获取作物面积产量数据失败:", error);
  442. // 错误时也清空数据
  443. pieChartData.value = [];
  444. totalArea.value = 0;
  445. yieldChartData.value = {
  446. categories: [],
  447. values: [],
  448. };
  449. yieldSummaryText.value = "暂无数据";
  450. });
  451. };
  452. </script>
  453. <style lang="scss" scoped>
  454. .chart-list {
  455. width: 100%;
  456. height: 100%;
  457. .chart-item {
  458. width: 100%;
  459. height: 285px;
  460. box-sizing: border-box;
  461. margin-bottom: 10px;
  462. background: rgba(35, 35, 35, 1);
  463. border: 1px solid #444444;
  464. border-radius: 4px;
  465. .box-content {
  466. width: 100%;
  467. height: 100%;
  468. }
  469. .chart-dom {
  470. height: calc(100% - 61px);
  471. width: 100%;
  472. }
  473. .box-bg {
  474. border-radius: 2px 2px 0 0;
  475. font-size: 12px;
  476. padding: 6px 8px;
  477. box-sizing: border-box;
  478. height: 61px;
  479. overflow-y: auto;
  480. background: linear-gradient(180deg, rgb(85, 85, 85, 0.4) 0%, rgb(35, 35, 35, 1) 100%);
  481. .legend-list {
  482. display: grid;
  483. grid-template-columns: 1fr 1fr;
  484. gap: 8px 12px;
  485. height: 100%;
  486. font-size: 12px;
  487. .legend-item {
  488. display: flex;
  489. align-items: center;
  490. gap: 6px;
  491. line-height: 12px;
  492. .dot {
  493. width: 5px;
  494. height: 5px;
  495. border-radius: 50%;
  496. flex-shrink: 0;
  497. }
  498. .text {
  499. color: rgba(255, 255, 255, 1);
  500. font-size: 12px;
  501. white-space: nowrap;
  502. .percent {
  503. padding: 0 10px;
  504. }
  505. .line {
  506. color: rgba(255, 255, 255, 0.2);
  507. padding-right: 10px;
  508. }
  509. }
  510. }
  511. }
  512. }
  513. }
  514. }
  515. </style>