| | |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">总能耗成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.totalCost) }} |
| | | ¥{{ formatMoney(animatedOverview.totalEnergyCost) }} |
| | | </div> |
| | | <div class="kpi-meta"> |
| | | <span |
| | |
| | | <button |
| | | class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('totalCost')" |
| | | @click="copyKpi('totalEnergyCost')" |
| | | > |
| | | 复制 |
| | | </button> |
| | |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">生产能耗成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.productionCost) }} |
| | | ¥{{ formatMoney(animatedOverview.productEnergyCost) }} |
| | | </div> |
| | | <div class="kpi-meta"> |
| | | <span |
| | |
| | | <button |
| | | class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('productionCost')" |
| | | @click="copyKpi('productEnergyCost')" |
| | | > |
| | | 复制 |
| | | </button> |
| | |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">办公能耗成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.officeCost) }} |
| | | ¥{{ formatMoney(animatedOverview.officeEnergyCost) }} |
| | | </div> |
| | | <div class="kpi-meta"> |
| | | <span |
| | |
| | | <button |
| | | class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('officeCost')" |
| | | @click="copyKpi('officeEnergyCost')" |
| | | > |
| | | 复制 |
| | | </button> |
| | |
| | | <div class="kpi-left"> |
| | | <div class="kpi-label">平均成本</div> |
| | | <div class="kpi-value"> |
| | | ¥{{ formatMoney(animatedOverview.avgCost) }} |
| | | ¥{{ formatMoney(animatedOverview.averageEnergyCost) }} |
| | | <span class="kpi-unit" |
| | | >/{{ statisticsType === "day" ? "日" : "月" }}</span |
| | | > |
| | |
| | | <button |
| | | class="kpi-action" |
| | | type="button" |
| | | @click="copyKpi('avgCost')" |
| | | @click="copyKpi('averageEnergyCost')" |
| | | > |
| | | 复制 |
| | | </button> |
| | |
| | | <button |
| | | class="chart-tool" |
| | | type="button" |
| | | @click="downloadChart('consumption', '能耗用量对比')" |
| | | @click="downloadChart('unit', '能耗用量对比')" |
| | | > |
| | | 下载 |
| | | </button> |
| | | <button |
| | | class="chart-tool" |
| | | type="button" |
| | | @click="openBigChart('consumption', '能耗用量对比')" |
| | | @click="openBigChart('unit', '能耗用量对比')" |
| | | > |
| | | 大图 |
| | | </button> |
| | |
| | | </div> |
| | | </template> |
| | | <div |
| | | ref="consumptionChartWrap" |
| | | ref="unitChartWrap" |
| | | class="chart-wrap" |
| | | v-loading="tableLoading" |
| | | > |
| | | <div |
| | | ref="consumptionChart" |
| | | class="chart-content" |
| | | ref="unitChart" |
| | | class="chart-content" |
| | | v-show="hasTableData" |
| | | ></div> |
| | | <div class="chart-empty" v-show="!hasTableData"> |
| | |
| | | sortable="custom" |
| | | /> |
| | | <el-table-column |
| | | prop="energyType" |
| | | prop="energyTyep" |
| | | label="能耗类型" |
| | | width="100" |
| | | align="center" |
| | |
| | | filter-placement="bottom-end" |
| | | > |
| | | <template #default="scope"> |
| | | <el-tag :type="getEnergyTypeType(scope.row.energyType)"> |
| | | {{ scope.row.energyType }} |
| | | <el-tag :type="getEnergyTypeType(scope.row.energyTyep)"> |
| | | {{ scope.row.energyTyep }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | filter-placement="bottom-end" |
| | | > |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.type === '生产' ? 'primary' : 'info'"> |
| | | <el-tag :type="scope.row.type === '生产' ? 'primary' : 'warning'"> |
| | | {{ scope.row.type }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="consumption" label="用量" align="right"> |
| | | <el-table-column prop="dosage" label="用量" align="right"> |
| | | <template #default="scope"> |
| | | <span class="consumption-value">{{ |
| | | formatNumber(scope.row.consumption, 2) |
| | | <span class="unit-value">{{ |
| | | formatNumber(scope.row.dosage, 2) |
| | | }}</span> |
| | | <span class="consumption-unit">{{ scope.row.unit }}</span> |
| | | <span class="unit-unit">{{ scope.row.unit }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | prop="price" |
| | | prop="unitPrice" |
| | | label="单价(元)" |
| | | align="right" |
| | | sortable="custom" |
| | | > |
| | | <template #default="scope"> |
| | | <span class="price-value">{{ |
| | | formatNumber(scope.row.price, 2) |
| | | formatNumber(scope.row.unitPrice, 2) |
| | | }}</span> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | } from "@element-plus/icons-vue"; |
| | | import * as echarts from "echarts"; |
| | | // import { energyCostStatistics } from "@/api/costAccounting/energyCosts"; |
| | | import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType"; |
| | | import { energyConsumptionDetailAccount } from "@/api/energyManagement/energyType"; |
| | | // 统计维度:day-按日,month-按月 |
| | | const statisticsType = ref("day"); |
| | | |
| | |
| | | |
| | | // 统计概览 |
| | | const overview = reactive({ |
| | | totalCost: "0.00", |
| | | productionCost: "0.00", |
| | | officeCost: "0.00", |
| | | avgCost: "0.00", |
| | | totalEnergyCost: "0.00", |
| | | productEnergyCost: "0.00", |
| | | officeEnergyCost: "0.00", |
| | | averageEnergyCost: "0.00", |
| | | }); |
| | | |
| | | const selectedKpi = ref("all"); // all | production | office |
| | | const animatedOverview = reactive({ |
| | | totalCost: 0, |
| | | productionCost: 0, |
| | | officeCost: 0, |
| | | avgCost: 0, |
| | | totalEnergyCost: 0, |
| | | productEnergyCost: 0, |
| | | officeEnergyCost: 0, |
| | | averageEnergyCost: 0, |
| | | }); |
| | | |
| | | const formatMoney = (v) => { |
| | |
| | | watch( |
| | | () => ({ ...overview }), |
| | | (val) => { |
| | | animateNumber("totalCost", Number.parseFloat(val.totalCost)); |
| | | animateNumber("productionCost", Number.parseFloat(val.productionCost)); |
| | | animateNumber("officeCost", Number.parseFloat(val.officeCost)); |
| | | animateNumber("avgCost", Number.parseFloat(val.avgCost)); |
| | | animateNumber("totalEnergyCost", Number.parseFloat(val.totalEnergyCost)); |
| | | animateNumber("productEnergyCost", Number.parseFloat(val.productEnergyCost)); |
| | | animateNumber("officeEnergyCost", Number.parseFloat(val.officeEnergyCost)); |
| | | animateNumber("averageEnergyCost", Number.parseFloat(val.averageEnergyCost)); |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | |
| | | |
| | | const copyKpi = async (field) => { |
| | | const map = { |
| | | totalCost: animatedOverview.totalCost, |
| | | productionCost: animatedOverview.productionCost, |
| | | officeCost: animatedOverview.officeCost, |
| | | avgCost: animatedOverview.avgCost, |
| | | totalEnergyCost: animatedOverview.totalEnergyCost, |
| | | productEnergyCost: animatedOverview.productEnergyCost, |
| | | officeEnergyCost: animatedOverview.officeEnergyCost, |
| | | averageEnergyCost: animatedOverview.averageEnergyCost, |
| | | }; |
| | | const raw = map[field]; |
| | | const text = `¥${formatMoney(raw)}`; |
| | |
| | | if (key === "cost") return costChartInstance; |
| | | if (key === "type") return typeChartInstance; |
| | | if (key === "purpose") return purposeChartInstance; |
| | | if (key === "consumption") return consumptionChartInstance; |
| | | if (key === "unit") return unitChartInstance; |
| | | return null; |
| | | }; |
| | | |
| | | const ensurePanelForChart = (key) => { |
| | | if (key === "cost" || key === "type") chartPanel.value = "core"; |
| | | if (key === "purpose" || key === "consumption") chartPanel.value = "advanced"; |
| | | if (key === "purpose" || key === "unit") chartPanel.value = "advanced"; |
| | | }; |
| | | |
| | | const downloadChart = (key, title) => { |
| | |
| | | |
| | | const prop = sortState.prop; |
| | | const direction = sortState.order === "ascending" ? 1 : -1; |
| | | const numFields = new Set(["price", "cost", "consumption"]); |
| | | const numFields = new Set(["price", "cost", "unit"]); |
| | | |
| | | return data.sort((a, b) => { |
| | | const av = a?.[prop]; |
| | |
| | | { text: "办公", value: "办公" }, |
| | | ]; |
| | | |
| | | const filterEnergyType = (value, row) => row.energyType === value; |
| | | const filterEnergyType = (value, row) => row.energyTyep === value; |
| | | const filterEnergyPurpose = (value, row) => row.type === value; |
| | | |
| | | // 分页 |
| | |
| | | const costChart = ref(null); |
| | | const typeChart = ref(null); |
| | | const purposeChart = ref(null); |
| | | const consumptionChart = ref(null); |
| | | const unitChart = ref(null); |
| | | |
| | | const costChartWrap = ref(null); |
| | | const typeChartWrap = ref(null); |
| | | const purposeChartWrap = ref(null); |
| | | const consumptionChartWrap = ref(null); |
| | | const unitChartWrap = ref(null); |
| | | |
| | | const tableAnchor = ref(null); |
| | | |
| | |
| | | let costChartInstance = null; |
| | | let typeChartInstance = null; |
| | | let purposeChartInstance = null; |
| | | let consumptionChartInstance = null; |
| | | let unitChartInstance = null; |
| | | |
| | | // 图表区切换:core | advanced | none(点击当前选中可收起) |
| | | const chartPanel = ref("core"); |
| | | |
| | | const ensureChartsReady = (panel) => { |
| | | if (panel === "core") { |
| | | if (costChart.value && !costChartInstance) |
| | | if (costChart.value && !costChartInstance) { |
| | | costChartInstance = echarts.init(costChart.value); |
| | | if (typeChart.value && !typeChartInstance) |
| | | setTimeout(() => costChartInstance?.resize(), 50); |
| | | } |
| | | if (typeChart.value && !typeChartInstance) { |
| | | typeChartInstance = echarts.init(typeChart.value); |
| | | if (costChartInstance) updateCostChart(); |
| | | if (typeChartInstance) updateTypeChart(); |
| | | setTimeout(() => typeChartInstance?.resize(), 50); |
| | | } |
| | | return; |
| | | } |
| | | if (panel === "advanced") { |
| | | if (purposeChart.value && !purposeChartInstance) |
| | | if (purposeChart.value && !purposeChartInstance) { |
| | | purposeChartInstance = echarts.init(purposeChart.value); |
| | | if (consumptionChart.value && !consumptionChartInstance) |
| | | consumptionChartInstance = echarts.init(consumptionChart.value); |
| | | if (purposeChartInstance) updatePurposeChart(); |
| | | if (consumptionChartInstance) updateConsumptionChart(); |
| | | setTimeout(() => purposeChartInstance?.resize(), 50); |
| | | } |
| | | if (unitChart.value && !unitChartInstance) { |
| | | unitChartInstance = echarts.init(unitChart.value); |
| | | setTimeout(() => unitChartInstance?.resize(), 50); |
| | | } |
| | | } |
| | | }; |
| | | |
| | |
| | | watch(chartPanel, (val) => { |
| | | if (val !== "none") resizeChartsAfterExpand(); |
| | | }); |
| | | |
| | | // 监听表格数据变化,确保数据加载后图表正确渲染 |
| | | watch(tableData, () => { |
| | | nextTick(() => { |
| | | updateCharts(); |
| | | nextTick(() => { |
| | | handleResize(); |
| | | }); |
| | | }); |
| | | }, { deep: true }); |
| | | |
| | | // 获取能耗类型标签类型 |
| | | const getEnergyTypeType = (type) => { |
| | |
| | | const typeCosts = {}; |
| | | |
| | | data.forEach((item) => { |
| | | if (!typeCosts[item.energyType]) { |
| | | typeCosts[item.energyType] = 0; |
| | | if (!typeCosts[item.energyTyep]) { |
| | | typeCosts[item.energyTyep] = 0; |
| | | } |
| | | typeCosts[item.energyType] += parseFloat(item.cost); |
| | | typeCosts[item.energyTyep] += parseFloat(item.cost); |
| | | }); |
| | | |
| | | const chartData = Object.entries(typeCosts).map(([name, value]) => ({ |
| | |
| | | // 更新能耗用量对比图 |
| | | const updateConsumptionChart = () => { |
| | | const data = tableData.value; |
| | | const consumptionData = {}; |
| | | const unitData = {}; |
| | | |
| | | data.forEach((item) => { |
| | | if (!consumptionData[item.energyType]) { |
| | | consumptionData[item.energyType] = { |
| | | if (!unitData[item.energyTyep]) { |
| | | unitData[item.energyTyep] = { |
| | | 生产: 0, |
| | | 办公: 0, |
| | | }; |
| | | } |
| | | if (consumptionData[item.energyType].hasOwnProperty(item.type)) { |
| | | consumptionData[item.energyType][item.type] = parseFloat( |
| | | item.consumption |
| | | if (unitData[item.energyTyep].hasOwnProperty(item.type)) { |
| | | unitData[item.energyTyep][item.type] += parseFloat( |
| | | item.dosage || 0 |
| | | ); |
| | | } |
| | | }); |
| | | |
| | | const energyTypes = Object.keys(consumptionData); |
| | | const energyTypes = Object.keys(unitData); |
| | | const productionConsumptions = energyTypes.map( |
| | | (type) => consumptionData[type].生产 |
| | | (type) => unitData[type].生产 |
| | | ); |
| | | const officeConsumptions = energyTypes.map( |
| | | (type) => consumptionData[type].办公 |
| | | (type) => unitData[type].办公 |
| | | ); |
| | | |
| | | const option = { |
| | |
| | | }, |
| | | ], |
| | | }; |
| | | consumptionChartInstance.setOption(option); |
| | | unitChartInstance.setOption(option); |
| | | }; |
| | | |
| | | // 统计维度切换 |
| | |
| | | |
| | | // 查询 |
| | | const handleQuery = () => { |
| | | |
| | | queryPulse.value = true; |
| | | window.setTimeout(() => { |
| | | queryPulse.value = false; |
| | |
| | | } |
| | | |
| | | // 调用接口获取数据 |
| | | energyConsumptionDetailStatistics(params) |
| | | energyConsumptionDetailAccount(params) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | const data = res.data; |
| | | overview.totalCost = data.totalEnergyConsumption || "0"; |
| | | overview.productionCost = data.totalEnergyCost || "0"; |
| | | overview.avgCost = data.averageConsumption || "0"; |
| | | overview.officeCost = data.changeVite || 0; |
| | | overview.totalEnergyCost = data.totalEnergyCost || "0"; |
| | | overview.productEnergyCost = data.productEnergyCost || "0"; |
| | | overview.officeEnergyCost = data.officeEnergyCost || "0"; |
| | | overview.averageEnergyCost = data.averageEnergyCost || "0"; |
| | | |
| | | // 处理表格数据 |
| | | tableData.value = data.energyCostDtos || []; |
| | | tableData.value = data.energyConsumptionDetailDtoList || []; |
| | | page.total = tableData.value.length || 0; |
| | | } else { |
| | | ElMessage.error(res.message || "获取数据失败"); |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | overview.totalCost = "0.00"; |
| | | overview.productionCost = "0.00"; |
| | | overview.officeCost = "0.00"; |
| | | overview.avgCost = "0.00"; |
| | | overview.totalEnergyCost = "0.00"; |
| | | overview.productEnergyCost = "0.00"; |
| | | overview.officeEnergyCost = "0.00"; |
| | | overview.averageEnergyCost = "0.00"; |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | ElMessage.error("获取数据异常"); |
| | | tableData.value = []; |
| | | page.total = 0; |
| | | overview.totalCost = "0.00"; |
| | | overview.productionCost = "0.00"; |
| | | overview.officeCost = "0.00"; |
| | | overview.avgCost = "0.00"; |
| | | overview.totalEnergyCost = "0.00"; |
| | | overview.productEnergyCost = "0.00"; |
| | | overview.officeEnergyCost = "0.00"; |
| | | overview.averageEnergyCost = "0.00"; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | |
| | | // 更新所有图表 |
| | | const updateCharts = () => { |
| | | nextTick(() => { |
| | | // 确保 core 面板的图表始终初始化(因为默认显示的是 core) |
| | | ensureChartsReady("core"); |
| | | |
| | | // 同时也初始化当前可见面板的图表 |
| | | if (chartPanel.value === "advanced") { |
| | | ensureChartsReady("advanced"); |
| | | } |
| | | |
| | | // 更新所有已初始化的图表 |
| | | if (costChartInstance) updateCostChart(); |
| | | if (typeChartInstance) updateTypeChart(); |
| | | if (purposeChartInstance) updatePurposeChart(); |
| | | if (consumptionChartInstance) updateConsumptionChart(); |
| | | if (unitChartInstance) updateConsumptionChart(); |
| | | }); |
| | | }; |
| | | |
| | |
| | | costChartInstance && costChartInstance.resize(); |
| | | typeChartInstance && typeChartInstance.resize(); |
| | | purposeChartInstance && purposeChartInstance.resize(); |
| | | consumptionChartInstance && consumptionChartInstance.resize(); |
| | | unitChartInstance && unitChartInstance.resize(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | |
| | | width: 100%; |
| | | } |
| | | |
| | | .consumption-value { |
| | | .unit-value { |
| | | font-weight: bold; |
| | | color: var(--lux-primary); |
| | | } |
| | | |
| | | .consumption-unit { |
| | | .unit-unit { |
| | | font-size: 12px; |
| | | color: var(--lux-muted); |
| | | margin-left: 2px; |
| | |
| | | max-height: 600px; |
| | | opacity: 1; |
| | | } |
| | | </style> |
| | | </style> |