| | |
| | | import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { Alert, Button, Card, DatePicker, Empty, Form, FormItem, Input, RadioGroup, Select, Space, Spin, Table, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { getEnergyAnalysis } from '#/api/energy'; |
| | | import { exportEnergyAnalysis, getEnergyAnalysis } from '#/api/energy'; |
| | | import { energyInfo, energyOptions, numberText, productOptions } from '../shared'; |
| | | |
| | | defineOptions({ name: 'EnergyAnalysis' }); |
| | |
| | | const data = ref<EnergyAnalysis>(); |
| | | const loading = ref(false); |
| | | const failed = ref(false); |
| | | const exporting = ref(false); |
| | | const selectedEnergy = ref<EnergyType>('WATER'); |
| | | const trendRef = ref<EchartsUIType>(); |
| | | const shareRef = ref<EchartsUIType>(); |
| | |
| | | const trendChart = useEcharts(trendRef); |
| | | const shareChart = useEcharts(shareRef); |
| | | const areaChart = useEcharts(areaRef); |
| | | const costRef = ref<EchartsUIType>(); |
| | | const areaShareRef = ref<EchartsUIType>(); |
| | | const usageRef = ref<EchartsUIType>(); |
| | | const costChart = useEcharts(costRef); |
| | | const areaShareChart = useEcharts(areaShareRef); |
| | | const usageChart = useEcharts(usageRef); |
| | | const toolbox = (name: string) => ({ right: 8, feature: { saveAsImage: { title: '下载 PNG 图片', name, pixelRatio: 2 } } }); |
| | | async function exportReport() { |
| | | exporting.value = true; |
| | | try { downloadFileFromBlobPart({ fileName: '能耗分析.xlsx', source: await exportEnergyAnalysis({ ...applied.value }) }); } |
| | | finally { exporting.value = false; } |
| | | } |
| | | const hasRecords = computed(() => (data.value?.recordCount ?? 0) > 0); |
| | | const summaryColumns = [ |
| | | { title: '能源', dataIndex: 'energyType' }, |
| | |
| | | const values = new Map(data.value.trend.filter(row => row.energyType === selectedEnergy.value).map(row => [row.label, Number(row.quantity)])); |
| | | await trendChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox(`${info.name}用量趋势`), |
| | | tooltip: { trigger: 'axis', confine: true }, |
| | | grid: { left: 20, right: 24, top: 40, bottom: 70, containLabel: true }, |
| | | xAxis: { type: 'category', data: periods }, |
| | |
| | | dataZoom: [{ type: 'inside' }, { type: 'slider', height: 18, bottom: 12 }], |
| | | series: [{ name: `${info.name}(${info.unit})`, type: 'line', showSymbol: periods.length < 40, connectNulls: false, itemStyle: { color: info.color }, data: periods.map(period => values.get(period) ?? null) }], |
| | | }); |
| | | const usage = data.value.areas.filter(row => row.energyType === selectedEnergy.value).sort((a, b) => Number(b.quantity) - Number(a.quantity)).slice(0, 10).reverse(); |
| | | await usageChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox(`${info.name}区域用量`), |
| | | tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' } }, |
| | | grid: { left: 16, right: 36, top: 40, bottom: 24, containLabel: true }, |
| | | xAxis: { type: 'value', name: info.unit }, |
| | | yAxis: { type: 'category', data: usage.map(row => row.label), axisLabel: { width: 100, overflow: 'truncate' } }, |
| | | series: [{ name: `${info.name}(${info.unit})`, type: 'bar', itemStyle: { color: info.color }, data: usage.map(row => Number(row.quantity)) }], |
| | | }); |
| | | await costChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox('能源费用趋势'), |
| | | tooltip: { trigger: 'axis', confine: true }, legend: { bottom: 0 }, |
| | | grid: { left: 16, right: 24, top: 40, bottom: 70, containLabel: true }, |
| | | xAxis: { type: 'category', data: periods }, yAxis: { type: 'value', name: '费用(元)' }, |
| | | dataZoom: [{ type: 'inside' }], |
| | | series: energyOptions.map(option => { |
| | | const costs = new Map(data.value!.trend.filter(row => row.energyType === option.value).map(row => [row.label, Number(row.amount)])); |
| | | return { name: option.label, type: 'line' as const, showSymbol: periods.length < 40, connectNulls: false, |
| | | itemStyle: { color: energyInfo[option.value as EnergyType].color }, data: periods.map(period => costs.get(period) ?? null) }; |
| | | }), |
| | | }); |
| | | } |
| | | async function renderCharts() { |
| | | if (!hasRecords.value || !data.value) return; |
| | | await renderTrend(); |
| | | if (Number(data.value.totalAmount) > 0) await shareChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox('能源费用占比'), |
| | | tooltip: { trigger: 'item', confine: true, formatter: '{b}:{c} 元({d}%)' }, |
| | | legend: { bottom: 0 }, |
| | | series: [{ type: 'pie', radius: '65%', center: ['50%', '45%'], label: { formatter: '{b}\n{d}%' }, data: data.value.summary.filter(row => Number(row.amount) > 0).map(row => ({ name: energyInfo[row.energyType].name, value: Number(row.amount), itemStyle: { color: energyInfo[row.energyType].color } })) }], |
| | | }); |
| | | const totals = new Map<string, number>(); |
| | | for (const row of data.value.areas) totals.set(row.label, (totals.get(row.label) ?? 0) + Number(row.amount)); |
| | | if (Number(data.value.totalAmount) > 0) { |
| | | const sorted = [...totals].filter(([, amount]) => amount > 0).sort((a, b) => b[1] - a[1]); |
| | | const shares = sorted.slice(0, 10).map(([name, value]) => ({ name, value })); |
| | | const others = sorted.slice(10).reduce((sum, [, amount]) => sum + amount, 0); |
| | | if (others > 0) shares.push({ name: '其他区域合计', value: others }); |
| | | await areaShareChart.renderEcharts({ |
| | | animation: false, toolbox: toolbox('区域费用占比'), |
| | | tooltip: { trigger: 'item', confine: true, formatter: '{b}:{c} 元({d}%)' }, |
| | | legend: { type: 'scroll', bottom: 0 }, |
| | | series: [{ type: 'pie', radius: ['35%', '60%'], center: ['50%', '44%'], label: { formatter: '{b}\n{d}%', width: 85, overflow: 'truncate' }, data: shares }], |
| | | }); |
| | | } |
| | | const areaLabels = [...totals].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([label]) => label).reverse(); |
| | | await areaChart.renderEcharts({ |
| | | animation: false, |
| | | toolbox: toolbox('区域费用分布'), |
| | | tooltip: { trigger: 'axis', confine: true, axisPointer: { type: 'shadow' } }, |
| | | legend: { bottom: 0 }, |
| | | grid: { left: 16, right: 24, top: 30, bottom: 45, containLabel: true }, |
| | |
| | | <FormItem label="能源"><Select v-model:value="filters.energyType" :options="energyOptions" allow-clear placeholder="全部能源" style="width: 130px" /></FormItem> |
| | | <FormItem label="区域"><Input v-model:value="filters.usageArea" placeholder="区域关键词" allow-clear style="width: 150px" /></FormItem> |
| | | <FormItem label="产品"><Select v-model:value="filters.productCategory" :options="productOptions" allow-clear placeholder="全部类别" style="width: 150px" /></FormItem> |
| | | <FormItem><Space><Button type="primary" :loading="loading" @click="load">分析</Button><Button @click="reset">重置</Button></Space></FormItem> |
| | | <FormItem><Space wrap><Button type="primary" :loading="loading" @click="load">分析</Button><Button @click="reset">重置</Button><Button v-access:code="['energy:analysis:export']" :disabled="!data || loading" :loading="exporting" @click="exportReport">导出分析报表</Button></Space></FormItem> |
| | | </Form> |
| | | </Card> |
| | | <Alert v-if="failed" type="error" show-icon message="能耗分析加载失败,请点击分析重试。" class="mb-4" /> |
| | |
| | | <div class="mb-4 grid grid-cols-1 gap-4 lg:grid-cols-2"> |
| | | <Card title="能源费用占比"><EchartsUI v-if="Number(data.totalAmount) > 0" ref="shareRef" class="h-[330px] w-full" /><Empty v-else description="尚未录入费用,填写单价后显示费用占比。" /></Card> |
| | | <Card title="区域费用分布(前 10)"><EchartsUI ref="areaRef" class="h-[330px] w-full" /></Card> |
| | | <Card title="能源费用趋势"><EchartsUI ref="costRef" class="h-[330px] w-full" /></Card> |
| | | <Card title="区域费用占比"><EchartsUI v-if="Number(data.totalAmount) > 0" ref="areaShareRef" class="h-[330px] w-full" /><Empty v-else description="尚无费用数据" /></Card> |
| | | <Card class="lg:col-span-2" :title="`${energyInfo[selectedEnergy].name} · 区域用量对比(前 10)`"><EchartsUI ref="usageRef" class="h-[330px] w-full" /></Card> |
| | | </div> |
| | | <p class="text-sm text-muted-foreground">图表右上角可下载 PNG;分析报表包含统计口径、能源汇总、日期趋势、区域汇总四个工作表,使用最近一次分析的筛选条件。</p> |
| | | <Card title="趋势明细"> |
| | | <Table :columns="detailColumns" :data-source="data.trend" :row-key="row => `${row.label}-${row.energyType}`" :scroll="{ x: 600 }" :pagination="{ pageSize: 10, showSizeChanger: true }" size="small"> |
| | | <template #bodyCell="{ column, record }"> |