云
4 天以前 ec8ff4f6259404460a0b4437e29a40986c2c4b9d
src/views/energy/analysis/index.vue
@@ -4,9 +4,10 @@
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' });
@@ -16,6 +17,7 @@
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>();
@@ -23,6 +25,18 @@
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' },
@@ -80,6 +94,7 @@
  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 },
@@ -87,21 +102,56 @@
    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 },
@@ -127,7 +177,7 @@
        <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" />
@@ -157,7 +207,11 @@
          <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 }">