chartList.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <template>
  2. <div class="chart-list">
  3. <div class="chart-item">
  4. <chart-box :name="`平泉街道${selectedCategory.category}占比统计`">
  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="`平泉街道物候进程分布`">
  27. <template #title-right>
  28. <el-select
  29. v-model="selectedPieItem"
  30. size="small"
  31. style="width: 100px"
  32. @change="fetchStatPhenologyAreaRatioForGrain"
  33. >
  34. <el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
  35. </el-select>
  36. </template>
  37. <div class="box-content">
  38. <div class="chart-dom">
  39. <bar-chart :key="`region-${activeBaseTab}`" :chartData="regionChartData"></bar-chart>
  40. </div>
  41. <div class="box-bg">{{ regionSummaryText }}</div>
  42. </div>
  43. </chart-box>
  44. </div>
  45. <div class="chart-item">
  46. <chart-box :name="`平泉街道非农化非量化占比统计`">
  47. <div class="box-content">
  48. <div class="chart-dom">
  49. <bar-chart
  50. :key="`yield-${activeBaseTab}`"
  51. :chartData="yieldChartData"
  52. :yAxisFormatter="yAxisFormatter"
  53. ></bar-chart>
  54. </div>
  55. <div class="box-bg">{{ yieldSummaryText }}</div>
  56. </div>
  57. </chart-box>
  58. </div>
  59. </div>
  60. </template>
  61. <script setup>
  62. import chartBox from "@/components/chartBox.vue";
  63. import pieChart from "./pieChart.vue";
  64. import { computed, ref, onMounted, onUnmounted } from "vue";
  65. import barChart from "./barChart.vue";
  66. import eventBus from "@/api/eventBus";
  67. const props = defineProps({
  68. activeBaseTab: {
  69. type: String,
  70. default: "作物分布",
  71. },
  72. areaCode: {
  73. type: String,
  74. default: "156440000",
  75. },
  76. areaName: {
  77. type: String,
  78. default: "广东省",
  79. },
  80. });
  81. // 图表数据
  82. const pieChartData = ref([]);
  83. const totalArea = ref(0);
  84. // 饼图颜色数组(与 chartOption.js 保持一致)
  85. const pieColors = ["#2199F8", "#14C9C9", "#FFCC4B", "#8F46F4", "#FF7878", "#9FDB1D"];
  86. // 图例数据(基于饼图数据计算)
  87. const legendData = computed(() => {
  88. if (!pieChartData.value || pieChartData.value.length === 0 || totalArea.value === 0) {
  89. return [];
  90. }
  91. return pieChartData.value.map((item, index) => {
  92. const percent = ((item.value / totalArea.value) * 100).toFixed(2);
  93. // 从饼图数据中获取颜色(优先使用 itemStyle.color,如果没有则使用默认颜色)
  94. const color = item.itemStyle?.color || pieColors[index % pieColors.length];
  95. return {
  96. color: color,
  97. name: item.name,
  98. percent: parseFloat(percent),
  99. value: Math.round(item.value),
  100. };
  101. });
  102. });
  103. // 物候进程分布标题右侧下拉框选中项(来自饼图数据)
  104. const selectedPieItem = ref(null);
  105. // 区域占比图表数据
  106. const regionChartData = ref({
  107. categories: [], // 区域名称数组
  108. values: [], // 占比百分比数组
  109. });
  110. // 区域占比摘要文字
  111. const regionSummaryText = ref("暂无数据");
  112. // 用地面积统计图表数据
  113. const yieldChartData = ref({
  114. categories: [], // 类别名称数组
  115. values: [], // 占比百分比数组
  116. });
  117. // 用地面积统计摘要文字
  118. const yieldSummaryText = ref("暂无数据");
  119. onMounted(() => {
  120. // 监听图例组件的变化事件
  121. eventBus.on("landUseLegend:change", handleLandUseLegendChange);
  122. // 初始化时请求用地面积统计数据(statType=1)
  123. fetchYieldChartData();
  124. });
  125. onUnmounted(() => {
  126. // 移除事件监听
  127. eventBus.off("landUseLegend:change", handleLandUseLegendChange);
  128. });
  129. // 处理图例组件变化
  130. const handleLandUseLegendChange = (data) => {
  131. if (data.category === "作物类型" || data.nonGrain == null) {
  132. options.value = data.children;
  133. selectedPieItem.value = data.children[0].id;
  134. }
  135. // 更新选中的类别信息
  136. selectedCategory.value = {
  137. category: data.category,
  138. nonGrain: data.nonGrain,
  139. };
  140. // 使用新的 nonGrain 重新获取饼图数据
  141. fetchStatRegionAreaRatio(data.nonGrain);
  142. fetchStatPhenologyAreaRatioForGrain();
  143. };
  144. const options = ref([]);
  145. // 当前选中的类别信息
  146. const selectedCategory = ref({
  147. category: null,
  148. nonGrain: null,
  149. });
  150. //种植面积占比
  151. const fetchStatRegionAreaRatio = (nonGrain = null) => {
  152. const params = { statType: nonGrain || 0 };
  153. VE_API.warning
  154. .fetchStatAreaRatioByType(params)
  155. .then((res) => {
  156. if (res.code === 0 && res.data && res.data.length > 0) {
  157. // 转换接口数据为饼图格式
  158. // 饼图数据格式:{ value: 种植面积, name: 物种名称, itemStyle: { color: 颜色 } }
  159. const chartData = res.data.map((item, index) => ({
  160. value: item.plantArea,
  161. name: item.name,
  162. itemStyle: {
  163. color: item.color || pieColors[index % pieColors.length], // 使用接口返回的 color,如果没有则使用默认颜色
  164. },
  165. }));
  166. if (chartData.length > 0) {
  167. // 计算总种植面积
  168. const total = chartData.reduce((sum, item) => sum + (item.value || 0), 0);
  169. // 更新饼图数据
  170. pieChartData.value = chartData;
  171. totalArea.value = total;
  172. } else {
  173. // 数据过滤后为空
  174. pieChartData.value = [];
  175. totalArea.value = 0;
  176. }
  177. } else {
  178. // 接口返回空数据时,清空饼图数据
  179. pieChartData.value = [];
  180. totalArea.value = 0;
  181. }
  182. })
  183. .catch((error) => {
  184. console.error("获取种植面积占比数据失败:", error);
  185. // 错误时清空数据
  186. pieChartData.value = [];
  187. totalArea.value = 0;
  188. });
  189. };
  190. // 获取用地面积统计图表数据(statType=1)
  191. const fetchYieldChartData = () => {
  192. const params = { statType: 1 }; // 固定为 1
  193. VE_API.warning
  194. .fetchStatAreaRatioByType(params)
  195. .then((res) => {
  196. if (res.code === 0 && res.data && res.data.length > 0) {
  197. // 计算总种植面积
  198. const total = res.data.reduce((sum, item) => sum + (item.plantArea || 0), 0);
  199. // 转换接口数据为柱状图格式
  200. const categories = res.data.map((item) => item.name);
  201. const values = res.data.map((item) => {
  202. // 计算百分比
  203. const percent = total > 0 ? ((item.plantArea / total) * 100).toFixed(2) : 0;
  204. return parseFloat(percent);
  205. });
  206. // 更新图表数据
  207. yieldChartData.value = {
  208. categories,
  209. values,
  210. };
  211. // 找到最大和最小占比的项
  212. let maxValue = 0;
  213. let maxName = "";
  214. let minValue = Infinity;
  215. let minName = "";
  216. res.data.forEach((item) => {
  217. if (item.plantArea > maxValue) {
  218. maxValue = item.plantArea;
  219. maxName = item.name;
  220. }
  221. if (item.plantArea < minValue) {
  222. minValue = item.plantArea;
  223. minName = item.name;
  224. }
  225. });
  226. // 更新摘要文字
  227. if (maxName && minName && total > 0) {
  228. const maxPercent = ((maxValue / total) * 100).toFixed(1);
  229. const minPercent = ((minValue / total) * 100).toFixed(1);
  230. yieldSummaryText.value = `${maxName}的种植面积最大,占比${maxPercent}%;${minName}的种植面积最小,占比${minPercent}%`;
  231. } else if (maxName && total > 0) {
  232. const maxPercent = ((maxValue / total) * 100).toFixed(1);
  233. yieldSummaryText.value = `${maxName}的种植面积最大,占比${maxPercent}%`;
  234. } else {
  235. yieldSummaryText.value = "暂无数据";
  236. }
  237. } else {
  238. // 接口返回空数据时,清空图表数据
  239. yieldChartData.value = {
  240. categories: [],
  241. values: [],
  242. };
  243. yieldSummaryText.value = "暂无数据";
  244. }
  245. })
  246. .catch((error) => {
  247. console.error("获取用地面积统计数据失败:", error);
  248. // 错误时清空数据
  249. yieldChartData.value = {
  250. categories: [],
  251. values: [],
  252. };
  253. yieldSummaryText.value = "暂无数据";
  254. });
  255. };
  256. const fetchStatPhenologyAreaRatioForGrain = () => {
  257. VE_API.warning
  258. .fetchStatPhenologyAreaRatioForGrain({ speciesId: selectedPieItem.value })
  259. .then((res) => {
  260. if (res.code === 0 && res.data && res.data.length > 0) {
  261. // 转换接口数据为图表格式(物候进程分布)
  262. // 横轴:作物名称(有物候期则为“作物名(物候期)”),纵轴:面积占比(百分比)
  263. const categories = res.data.map((item) =>
  264. item.phenologyName ? `${item.speciesName}(${item.phenologyName})` : item.speciesName
  265. );
  266. const values = res.data.map((item) => parseFloat((item.areaRatio * 100).toFixed(2)));
  267. // 更新区域图表数据
  268. regionChartData.value = {
  269. categories,
  270. values,
  271. };
  272. // 找到最大占比的物候期
  273. let maxRatio = 0;
  274. let maxPhenology = "";
  275. let maxPhenologyName = "";
  276. res.data.forEach((item) => {
  277. if (item.areaRatio > maxRatio) {
  278. maxRatio = item.areaRatio;
  279. maxPhenology = item.speciesName;
  280. maxPhenologyName = item.phenologyName;
  281. }
  282. });
  283. // 更新摘要文字
  284. if (maxPhenology) {
  285. const maxPercent = (maxRatio * 100).toFixed(1);
  286. regionSummaryText.value = `${maxPhenology}的种植面积最大,占比${maxPercent}%,进入${maxPhenologyName}`;
  287. } else {
  288. regionSummaryText.value = "暂无数据";
  289. }
  290. } else {
  291. // 接口返回空数据时,清空区域图表数据
  292. regionChartData.value = {
  293. categories: [],
  294. values: [],
  295. };
  296. regionSummaryText.value = "暂无数据";
  297. }
  298. })
  299. .catch(() => {
  300. // 错误时也清空数据
  301. regionChartData.value = {
  302. categories: [],
  303. values: [],
  304. };
  305. regionSummaryText.value = "暂无数据";
  306. });
  307. };
  308. </script>
  309. <style lang="scss" scoped>
  310. .chart-list {
  311. width: 100%;
  312. height: 100%;
  313. .chart-item {
  314. width: 100%;
  315. height: 285px;
  316. box-sizing: border-box;
  317. margin-bottom: 10px;
  318. background: rgba(35, 35, 35, 1);
  319. border: 1px solid #444444;
  320. border-radius: 4px;
  321. .box-content {
  322. width: 100%;
  323. height: 100%;
  324. }
  325. .chart-dom {
  326. height: calc(100% - 61px);
  327. width: 100%;
  328. }
  329. .box-bg {
  330. border-radius: 2px 2px 0 0;
  331. font-size: 12px;
  332. padding: 6px 8px;
  333. box-sizing: border-box;
  334. height: 61px;
  335. overflow-y: auto;
  336. background: linear-gradient(180deg, rgb(85, 85, 85, 0.4) 0%, rgb(35, 35, 35, 1) 100%);
  337. .legend-list {
  338. display: grid;
  339. grid-template-columns: 1fr 1fr;
  340. gap: 8px 12px;
  341. height: 100%;
  342. font-size: 12px;
  343. .legend-item {
  344. display: flex;
  345. align-items: center;
  346. gap: 6px;
  347. line-height: 12px;
  348. .dot {
  349. width: 5px;
  350. height: 5px;
  351. border-radius: 50%;
  352. flex-shrink: 0;
  353. }
  354. .text {
  355. color: rgba(255, 255, 255, 1);
  356. font-size: 12px;
  357. white-space: nowrap;
  358. .percent {
  359. padding: 0 10px;
  360. }
  361. .line {
  362. color: rgba(255, 255, 255, 0.2);
  363. padding-right: 10px;
  364. }
  365. }
  366. }
  367. }
  368. }
  369. }
  370. }
  371. </style>