kpiGrid.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <template>
  2. <view class="kpi-grid">
  3. <view class="kpi-card bg-white radius" v-for="(item, index) in list" :key="index">
  4. <view class="kpi-card__header flex">
  5. <view class="kpi-card__icon" :style="{ backgroundColor: `${item.color}18` }">
  6. <text class="kpi-card__icon-text" :style="{ color: item.color }">{{ item.icon }}</text>
  7. </view>
  8. <text class="kpi-card__title">{{ item.title }}</text>
  9. </view>
  10. <view class="kpi-card__value">
  11. <text class="kpi-card__num">{{ item.value }}</text>
  12. <text class="kpi-card__unit">{{ item.unit }}</text>
  13. </view>
  14. <view class="kpi-card__compare flex" v-if="item.showCompare">
  15. <view class="kpi-card__compare-item">
  16. <text class="kpi-card__compare-label">环比</text>
  17. <text :class="item.mom >= 0 ? 'kpi-card__up' : 'kpi-card__down'">{{ formatRate(item.mom) }}</text>
  18. </view>
  19. <view class="kpi-card__compare-item">
  20. <text class="kpi-card__compare-label">同比</text>
  21. <text :class="item.yoy >= 0 ? 'kpi-card__up' : 'kpi-card__down'">{{ formatRate(item.yoy) }}</text>
  22. </view>
  23. </view>
  24. </view>
  25. </view>
  26. </template>
  27. <script setup>
  28. import { reactive, computed, watch, toRefs } from "vue";
  29. import { getEmsClassificationEnergy, getEmsOverviewTop } from "@/api/business/ems/overview.js";
  30. const PIE_COLORS = ["#4fe3c3", "#6797ed", "#9978fa", "#4ecee2", "#ffbb62", "#ff7d2e", "#dc76ee", "#56d853"];
  31. const emit = defineEmits(["update:unitIndex", "update:categoryData"]);
  32. const props = defineProps({
  33. dateType: {
  34. type: String,
  35. default: "1",
  36. },
  37. energyType: {
  38. type: String,
  39. default: "1",
  40. },
  41. unitIndex: {
  42. type: Number,
  43. default: 0,
  44. },
  45. categoryData: {
  46. type: Array,
  47. default: () => [],
  48. },
  49. });
  50. const DEFAULT_KPI = [
  51. { title: "能耗用量", value: "0.0", unit: "kWh", icon: "⚡", color: "#4a90e2", showCompare: true, mom: 0, yoy: 0 },
  52. { title: "等值树", value: "0.0", unit: "棵", icon: "🌳", color: "#40b883", showCompare: true, mom: 0, yoy: 0 },
  53. { title: "标准煤", value: "0.0", unit: "kgce", icon: "◉", color: "#f5a623", showCompare: true, mom: 0, yoy: 0 },
  54. { title: "碳排放", value: "0.0", unit: "kgce", icon: "☁", color: "#7b61ff", showCompare: true, mom: 0, yoy: 0 },
  55. { title: "综合能耗统计", value: "0.0", unit: "kgce", icon: "◎", color: "#9b59b6", showCompare: false, mom: 0, yoy: 0 },
  56. { title: "折算碳减排量", value: "0.0", unit: "kg", icon: "♻", color: "#ef6b6b", showCompare: false, mom: 0, yoy: 0 },
  57. ];
  58. const state = reactive({
  59. list: DEFAULT_KPI.map((item) => ({ ...item })),
  60. });
  61. const { list } = toRefs(state);
  62. const usageUnit = computed(() => (String(props.energyType) === "1" ? "kWh" : "m³"));
  63. function formatRate(value) {
  64. const num = Number(value) || 0;
  65. return `${num >= 0 ? "+" : ""}${num.toFixed(1)}%`;
  66. }
  67. function toFixedNum(val, fallback = 0) {
  68. const num = Number(val);
  69. return Number.isFinite(num) ? Number(num.toFixed(1)) : fallback;
  70. }
  71. function getQueryParams() {
  72. return {
  73. dateType: props.dateType,
  74. energyType: props.energyType,
  75. };
  76. }
  77. function applyClassificationEnergy(data) {
  78. if (!data) return;
  79. state.list[0].value = toFixedNum(data.consume);
  80. state.list[0].unit = usageUnit.value;
  81. state.list[0].mom = toFixedNum(data.sequentialCon);
  82. state.list[0].yoy = toFixedNum(data.pariPassCon);
  83. state.list[1].value = toFixedNum(data.plantTree);
  84. state.list[1].mom = toFixedNum(data.sequentialPlantTree);
  85. state.list[1].yoy = toFixedNum(data.pariPassPlantTree);
  86. state.list[2].value = toFixedNum(data.coalAmount);
  87. state.list[2].mom = toFixedNum(data.sequentialCoal);
  88. state.list[2].yoy = toFixedNum(data.pariPassCoal);
  89. state.list[3].value = toFixedNum(data.co2Amount);
  90. state.list[3].mom = toFixedNum(data.sequentialCo2);
  91. state.list[3].yoy = toFixedNum(data.pariPassCo2);
  92. }
  93. function applyOverviewTopKpi(data) {
  94. if (!data) return;
  95. state.list[4].value = toFixedNum(data.coalTotal);
  96. state.list[5].value = toFixedNum(data.co2Total);
  97. emit("update:unitIndex", toFixedNum(data.unitCoal));
  98. if (Array.isArray(data.ratioList) && data.ratioList.length) {
  99. emit(
  100. "update:categoryData",
  101. data.ratioList.map((item, index) => ({
  102. name: item.name,
  103. value: toFixedNum(item.consume),
  104. color: PIE_COLORS[index % PIE_COLORS.length],
  105. }))
  106. );
  107. }
  108. }
  109. function fetchClassificationEnergy() {
  110. return getEmsClassificationEnergy(getQueryParams())
  111. .then((requset) => {
  112. if (requset.status === "SUCCESS") {
  113. applyClassificationEnergy(requset.data);
  114. }
  115. })
  116. .catch(() => {});
  117. }
  118. function fetchOverviewTop() {
  119. return getEmsOverviewTop({ dateType: props.dateType })
  120. .then((requset) => {
  121. if (requset.status === "SUCCESS" && requset.data) {
  122. applyOverviewTopKpi(requset.data);
  123. }
  124. })
  125. .catch(() => {});
  126. }
  127. function refresh() {
  128. return Promise.all([fetchClassificationEnergy(), fetchOverviewTop()]);
  129. }
  130. watch(
  131. () => [props.dateType, props.energyType],
  132. () => {
  133. refresh();
  134. },
  135. { immediate: true }
  136. );
  137. defineExpose({
  138. refresh,
  139. });
  140. </script>
  141. <style lang="scss" scoped>
  142. .kpi-grid {
  143. display: grid;
  144. grid-template-columns: repeat(2, 1fr);
  145. gap: 10px;
  146. margin-bottom: 10px;
  147. }
  148. .kpi-card {
  149. padding: 12px;
  150. &__header {
  151. align-items: center;
  152. margin-bottom: 8px;
  153. }
  154. &__icon {
  155. width: 28px;
  156. height: 28px;
  157. border-radius: 8px;
  158. display: flex;
  159. align-items: center;
  160. justify-content: center;
  161. margin-right: 6px;
  162. flex-shrink: 0;
  163. }
  164. &__icon-text {
  165. font-size: 14px;
  166. line-height: 1;
  167. }
  168. &__title {
  169. font-size: 12px;
  170. color: #666;
  171. line-height: 18px;
  172. }
  173. &__value {
  174. margin-bottom: 6px;
  175. }
  176. &__num {
  177. font-size: 20px;
  178. font-weight: 600;
  179. color: #333;
  180. }
  181. &__unit {
  182. margin-left: 4px;
  183. font-size: 11px;
  184. color: #999;
  185. }
  186. &__compare {
  187. gap: 12px;
  188. }
  189. &__compare-item {
  190. font-size: 10px;
  191. color: #999;
  192. }
  193. &__compare-label {
  194. margin-right: 4px;
  195. }
  196. &__up {
  197. color: #ef4444;
  198. }
  199. &__down {
  200. color: #16a34a;
  201. }
  202. }
  203. </style>