From ec8ff4f6259404460a0b4437e29a40986c2c4b9d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 12 十月 2026 18:31:43 +0800
Subject: [PATCH] feat(workflow): 集成自动化审批功能并优化多个业务模块

---
 src/views/energy/analysis/index.vue |   58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 56 insertions(+), 2 deletions(-)

diff --git a/src/views/energy/analysis/index.vue b/src/views/energy/analysis/index.vue
index 5ddc53e..deb6e85 100644
--- a/src/views/energy/analysis/index.vue
+++ b/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 }">

--
Gitblit v1.9.3