From 3df460667d74868d0a54fce12d5e881bbd6c0c1a Mon Sep 17 00:00:00 2001
From: WXL <wl_5969728@163.com>
Date: 星期四, 17 九月 2026 17:11:18 +0800
Subject: [PATCH] 青岛维护统计

---
 src/views/statistics/rate.vue |  406 +++++++++++++++++++++++++++++++++------------------------
 1 files changed, 233 insertions(+), 173 deletions(-)

diff --git a/src/views/statistics/rate.vue b/src/views/statistics/rate.vue
index 58c3edc..9eeb6d3 100644
--- a/src/views/statistics/rate.vue
+++ b/src/views/statistics/rate.vue
@@ -53,7 +53,7 @@
       <el-col :span="12">
         <el-card>
           <div slot="header" class="clearfix">
-            <span>寮冪敤鐜囨瀯鎴愬垎鏋�</span>
+            <span>鍦板尯鑾峰彇/寮冪敤鐜囧姣�</span>
           </div>
           <EChartsWrapper
             :id="'abandon-chart'"
@@ -104,6 +104,18 @@
         <el-table-column label="鍣ㄥ畼鎬绘暟" prop="totalOrgans" align="center"  />
         <el-table-column label="绉绘鎬绘暟" prop="totalTransplants" align="center"  />
       </el-table>
+
+      <el-pagination
+        v-show="total > 0"
+        :total="total"
+        :page.sync="query.page"
+        :limit.sync="query.limit"
+        @current-change="handlePagination"
+        @size-change="handlePageSizeChange"
+        style="margin-top: 20px;"
+        layout="total, sizes, prev, pager, next, jumper"
+        :page-sizes="[10, 20, 50, 100]"
+      />
     </el-card>
   </div>
 </template>
@@ -111,6 +123,10 @@
 <script>
 import FilterPanel from '@/components/charts/FilterPanel.vue';
 import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
+import { organ, organMoon, caseTrendMoon } from '@/api/businessApi';
+import { toTotalsList, toMonth, toNum } from '@/utils/totals';
+import statisticsMode from '@/utils/statisticsMode';
+import { COLORS, axisTooltip, grid, xAxis, legend, barSeries, lineSeries } from '@/utils/chartStyle';
 
 export default {
   name: 'RateStatistics',
@@ -136,13 +152,13 @@
           type: 'select',
           multiple: true,
           options: [
-            { label: '闈掑矝鍦板尯', value: 'qingdao' },
-            { label: '鏃ョ収鍦板尯', value: 'rizhao' },
-            { label: '娴庡崡鍦板尯', value: 'jinan' },
-            { label: '鐑熷彴鍦板尯', value: 'yantai' },
-            { label: '濞佹捣鍦板尯', value: 'weihai' },
-            { label: '娼嶅潑鍦板尯', value: 'weifang' },
-            { label: '涓存矀鍦板尯', value: 'linyi' }
+            { label: '闈掑矝鍦板尯', value: '闈掑矝鍦板尯' },
+            { label: '鏃ョ収鍦板尯', value: '鏃ョ収鍦板尯' },
+            { label: '娴庡崡鍦板尯', value: '娴庡崡鍦板尯' },
+            { label: '鐑熷彴鍦板尯', value: '鐑熷彴鍦板尯' },
+            { label: '濞佹捣鍦板尯', value: '濞佹捣鍦板尯' },
+            { label: '娼嶅潑鍦板尯', value: '娼嶅潑鍦板尯' },
+            { label: '涓存矀鍦板尯', value: '涓存矀鍦板尯' }
           ],
           width: '300px'
         },
@@ -166,25 +182,25 @@
         {
           key: 'acquisition',
           title: '鎹愮尞鑾峰彇鐜�',
-          value: '85.5%',
+          value: '0%',
           trendIcon: 'el-icon-top',
-          trendText: '鈫�2.7%',
+          trendText: '鈥�',
           trendClass: 'up'
         },
         {
           key: 'avg',
           title: '骞冲潎鍣ㄥ畼鏁�',
-          value: '3.2',
+          value: '0',
           trendIcon: 'el-icon-top',
-          trendText: '鈫�0.2',
+          trendText: '鈥�',
           trendClass: 'up'
         },
         {
           key: 'abandon',
           title: '鍣ㄥ畼寮冪敤鐜�',
-          value: '12.3%',
+          value: '0%',
           trendIcon: 'el-icon-bottom',
-          trendText: '鈫�2.7%',
+          trendText: '鈥�',
           trendClass: 'down'
         }
       ],
@@ -193,69 +209,150 @@
       trendChartOptions: {},
       abandonChartOptions: {},
 
+      // 鍥捐〃鏁版嵁
+      trendData: [],
+      regionCompareData: [],
+
       // 琛ㄦ牸鏁版嵁
       tableData: [],
-      total: 0,
-
-      // 妯℃嫙鏁版嵁
-      mockData: {
-        table: [
-          { id: 1, year: '2024', region: '闈掑矝鍦板尯', acquisitionRate: 85.5, avgOrgans: 3.2, abandonRate: 12.3, totalDonation: 280, totalOrgans: 896, totalTransplants: 765 },
-          { id: 2, year: '2024', region: '鏃ョ収鍦板尯', acquisitionRate: 80.3, avgOrgans: 2.9, abandonRate: 15.8, totalDonation: 140, totalOrgans: 406, totalTransplants: 325 },
-          { id: 3, year: '2024', region: '娴庡崡鍦板尯', acquisitionRate: 89.1, avgOrgans: 3.4, abandonRate: 9.5, totalDonation: 320, totalOrgans: 1088, totalTransplants: 970 },
-          { id: 4, year: '2023', region: '闈掑矝鍦板尯', acquisitionRate: 82.8, avgOrgans: 3.0, abandonRate: 15.0, totalDonation: 250, totalOrgans: 750, totalTransplants: 620 },
-          { id: 5, year: '2023', region: '鏃ョ収鍦板尯', acquisitionRate: 78.5, avgOrgans: 2.7, abandonRate: 18.2, totalDonation: 120, totalOrgans: 324, totalTransplants: 252 },
-          { id: 6, year: '2023', region: '娴庡崡鍦板尯', acquisitionRate: 85.2, avgOrgans: 3.1, abandonRate: 12.5, totalDonation: 280, totalOrgans: 868, totalTransplants: 740 }
-        ],
-        trend: [
-          { year: '2020', acquisition: 78.5, avgOrgans: 2.8, abandon: 18.5 },
-          { year: '2021', acquisition: 81.2, avgOrgans: 3.0, abandon: 16.3 },
-          { year: '2022', acquisition: 84.6, avgOrgans: 3.2, abandon: 13.8 },
-          { year: '2023', acquisition: 87.3, avgOrgans: 3.4, abandon: 11.2 },
-          { year: '2024', acquisition: 89.1, avgOrgans: 3.5, abandon: 9.5 }
-        ],
-        abandon: [
-          { name: '鍣ㄥ畼璐ㄩ噺涓嶅悎鏍�', value: 45 },
-          { name: '渚涗綋鐤剧梾', value: 25 },
-          { name: '杩愯緭闂', value: 15 },
-          { name: '鍙椾綋鍖归厤闂', value: 10 },
-          { name: '鍏朵粬鍘熷洜', value: 5 }
-        ]
-      }
+      total: 0
     };
   },
   created() {
     this.loadData();
+    // 鏁版嵁婧�(mock/鐪熷疄鎺ュ彛)鍒囨崲鏃堕噸鏂板姞杞�
+    this.$watch(() => statisticsMode.useMock, () => this.loadData());
   },
   methods: {
+    // 缁勮鏌ヨ鏉′欢
+    buildQuery() {
+      const q = {};
+      if (this.query.region && this.query.region.length) {
+        q.area = this.query.region.join(',');
+      }
+      if (this.query.yearRange && this.query.yearRange.length === 2) {
+        q.startTime = this.query.yearRange[0];
+        q.endTime = this.query.yearRange[1];
+      }
+      return q;
+    },
+
     // 鍔犺浇鏁版嵁
     async loadData() {
       this.loading = true;
       try {
-        await new Promise(resolve => setTimeout(resolve, 500));
+        const q = this.buildQuery();
+        const [organRes, moonRes, caseMoonRes] = await Promise.all([
+          organ(q),
+          organMoon(q),
+          caseTrendMoon(q)
+        ]);
 
-        // 澶勭悊琛ㄦ牸鏁版嵁
-        let filteredData = [...this.mockData.table];
+        // 鍣ㄥ畼鎸夋湀瓒嬪娍锛氳幏鍙栫巼(绉绘/鑾峰彇)銆佸純鐢ㄧ巼
+        this.trendData = toTotalsList(moonRes)
+          .map(item => {
+            const get = toNum(item.getCount);
+            const use = toNum(item.useCount);
+            return {
+              month: toMonth(item.donateTime) || toMonth(item.month),
+              acquisition: get > 0 ? Number(((use / get) * 100).toFixed(1)) : 0,
+              abandon: get > 0 ? Number((((get - use) / get) * 100).toFixed(1)) : 0
+            };
+          })
+          .sort((a, b) => String(a.month).localeCompare(String(b.month)));
 
-        if (this.query.region.length > 0) {
-          filteredData = filteredData.filter(item =>
-            this.query.region.includes(item.region.replace('鍦板尯', '').toLowerCase())
-          );
-        }
+        // 鎸� 骞翠唤|鍦板尯 姹囨�诲櫒瀹樻暟鎹�
+        const organByKey = {};
+        toTotalsList(organRes).forEach(item => {
+          const year = String(item.donateTime || item.year || '').slice(0, 4);
+          const region = item.area || item.region || '';
+          const key = `${year}|${region}`;
+          if (!organByKey[key]) organByKey[key] = { year, region, organs: 0, transplants: 0 };
+          organByKey[key].organs += toNum(item.getCount);
+          organByKey[key].transplants += toNum(item.useCount);
+        });
 
-        if (this.query.yearRange && this.query.yearRange.length === 2) {
-          const [start, end] = this.query.yearRange;
-          filteredData = filteredData.filter(item =>
-            parseInt(item.year) >= parseInt(start) && parseInt(item.year) <= parseInt(end)
-          );
-        }
+        // 鎸� 骞翠唤|鍦板尯 姹囨�绘渚嬫暟鎹紙鎹愮尞鎬绘暟銆佹綔鍦ㄦ渚嬶級
+        const caseByKey = {};
+        toTotalsList(caseMoonRes).forEach(item => {
+          const month = toMonth(item.donateTime) || toMonth(item.month);
+          const year = String(month).slice(0, 4);
+          const region = item.area || item.region || '';
+          const key = `${year}|${region}`;
+          if (!caseByKey[key]) caseByKey[key] = { year, region, potential: 0, complete: 0 };
+          caseByKey[key].potential += toNum(item.potentialCount);
+          caseByKey[key].complete += toNum(item.completeCount);
+        });
 
-        this.tableData = filteredData;
-        this.total = filteredData.length;
+        // 鍚堝苟 骞翠唤|鍦板尯 鏄庣粏
+        const keys = new Set([...Object.keys(organByKey), ...Object.keys(caseByKey)]);
+        const rows = [];
+        keys.forEach(key => {
+          const o = organByKey[key] || {};
+          const c = caseByKey[key] || {};
+          const year = o.year || c.year || '';
+          const region = o.region || c.region || '';
+          const organs = o.organs || 0;
+          const transplants = o.transplants || 0;
+          const potential = c.potential || 0;
+          const complete = c.complete || 0;
 
-        // 鏇存柊鍥捐〃
+          rows.push({
+            year,
+            region,
+            acquisitionRate: potential > 0 ? Number(((complete / potential) * 100).toFixed(1)) : 0,
+            avgOrgans: complete > 0 ? Number((organs / complete).toFixed(1)) : 0,
+            abandonRate: organs > 0 ? Number((((organs - transplants) / organs) * 100).toFixed(1)) : 0,
+            totalDonation: complete,
+            totalOrgans: organs,
+            totalTransplants: transplants
+          });
+        });
+        rows.sort((a, b) => String(a.year + a.region).localeCompare(String(b.year + b.region)));
+
+        this.total = rows.length;
+        const start = (this.query.page - 1) * this.query.limit;
+        this.tableData = rows.slice(start, start + this.query.limit);
+
+        // 鍦板尯缁村害鐨勮幏鍙栫巼/寮冪敤鐜囷紝鐢ㄦ綔鍦�/瀹屾垚銆佸櫒瀹�/绉绘鍘熷鏁版嵁姹囨��
+        const regionCompare = {};
+        toTotalsList(caseMoonRes).forEach(item => {
+          const region = item.area || item.region || '';
+          if (!regionCompare[region]) regionCompare[region] = { region, potential: 0, complete: 0 };
+          regionCompare[region].potential += toNum(item.potentialCount);
+          regionCompare[region].complete += toNum(item.completeCount);
+        });
+        const organRegion = {};
+        toTotalsList(organRes).forEach(item => {
+          const region = item.area || item.region || '';
+          if (!organRegion[region]) organRegion[region] = { organs: 0, transplants: 0 };
+          organRegion[region].organs += toNum(item.getCount);
+          organRegion[region].transplants += toNum(item.useCount);
+        });
+        const regionNames = new Set([...Object.keys(regionCompare), ...Object.keys(organRegion)]);
+        this.regionCompareData = [...regionNames].map(region => {
+          const c = regionCompare[region] || {};
+          const o = organRegion[region] || {};
+          const potential = c.potential || 0;
+          const complete = c.complete || 0;
+          const organs = o.organs || 0;
+          const transplants = o.transplants || 0;
+          return {
+            region,
+            acquisition: potential > 0 ? Number(((complete / potential) * 100).toFixed(1)) : 0,
+            abandon: organs > 0 ? Number((((organs - transplants) / organs) * 100).toFixed(1)) : 0
+          };
+        });
+
+        const totals = {
+          totalPotential: Object.values(caseByKey).reduce((s, c) => s + c.potential, 0),
+          totalDonation: Object.values(caseByKey).reduce((s, c) => s + c.complete, 0),
+          totalOrgans: Object.values(organByKey).reduce((s, o) => s + o.organs, 0),
+          totalTransplants: Object.values(organByKey).reduce((s, o) => s + o.transplants, 0)
+        };
+
+        this.updateOverviewData(totals);
         this.updateCharts();
-
       } catch (error) {
         console.error('鍔犺浇鏁版嵁澶辫触:', error);
         this.$message.error('鏁版嵁鍔犺浇澶辫触');
@@ -264,157 +361,107 @@
       }
     },
 
+    // 鏇存柊浠〃鐩�
+    updateOverviewData(totals) {
+      const { totalPotential, totalDonation, totalOrgans, totalTransplants } = totals;
+
+      const acquisitionRate = totalPotential > 0 ? Number(((totalDonation / totalPotential) * 100).toFixed(1)) : 0;
+      const avgOrgans = totalDonation > 0 ? Number((totalOrgans / totalDonation).toFixed(1)) : 0;
+      const abandonRate = totalOrgans > 0 ? Number((((totalOrgans - totalTransplants) / totalOrgans) * 100).toFixed(1)) : 0;
+
+      this.dashboardData[0].value = `${acquisitionRate}%`;
+      this.dashboardData[1].value = `${avgOrgans}`;
+      this.dashboardData[2].value = `${abandonRate}%`;
+      this.dashboardData[0].trendText = acquisitionRate >= 80 ? '鈫戣揪鏍�' : '鍋忎綆';
+      this.dashboardData[2].trendText = abandonRate <= 15 ? '鈫撹揪鏍�' : '鍋忛珮';
+    },
+
     // 鏇存柊鍥捐〃
     updateCharts() {
-      // 瓒嬪娍鍥�
+      // 鑾峰彇鐜囪秼鍔垮浘
       this.trendChartOptions = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'cross',
-            label: {
-              backgroundColor: '#6a7985'
-            }
-          }
-        },
-        legend: {
-          data: ['鎹愮尞鑾峰彇鐜�', '骞冲潎鍣ㄥ畼鏁�', '鍣ㄥ畼寮冪敤鐜�']
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
+        tooltip: axisTooltip(),
+        legend: legend(['鍣ㄥ畼鍒╃敤鐜�', '鍣ㄥ畼寮冪敤鐜�']),
+        grid: grid(),
         xAxis: {
           type: 'category',
           boundaryGap: false,
-          data: this.mockData.trend.map(item => item.year + '骞�')
+          data: this.trendData.map(item => item.month),
+          axisLine: { show: false },
+          axisTick: { show: false },
+          axisLabel: { color: '#8a919f', fontSize: 12 }
         },
         yAxis: {
           type: 'value',
-          axisLabel: {
-            formatter: '{value}'
-          }
+          axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' },
+          splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } }
         },
         series: [
-          {
-            name: '鎹愮尞鑾峰彇鐜�',
-            type: 'line',
-            data: this.mockData.trend.map(item => item.acquisition),
-            itemStyle: { color: '#409EFF' },
-            smooth: true
-          },
-          {
-            name: '骞冲潎鍣ㄥ畼鏁�',
-            type: 'line',
-            data: this.mockData.trend.map(item => item.avgOrgans * 20), // 缂╂斁鏄剧ず
-            itemStyle: { color: '#67C23A' },
-            smooth: true
-          },
-          {
-            name: '鍣ㄥ畼寮冪敤鐜�',
-            type: 'line',
-            data: this.mockData.trend.map(item => item.abandon),
-            itemStyle: { color: '#F56C6C' },
-            smooth: true
-          }
+          lineSeries('鍣ㄥ畼鍒╃敤鐜�', this.trendData.map(item => item.acquisition), COLORS.blue, { area: true }),
+          lineSeries('鍣ㄥ畼寮冪敤鐜�', this.trendData.map(item => item.abandon), COLORS.coral)
         ]
       };
 
-      // 寮冪敤鐜囨瀯鎴愬浘
+      // 鍦板尯鑾峰彇/寮冪敤鐜囧姣斿浘
       this.abandonChartOptions = {
-        tooltip: {
-          trigger: 'item',
-          formatter: '{a} <br/>{b} : {c} ({d}%)'
-        },
-        legend: {
-          orient: 'vertical',
-          left: 'left',
-          data: this.mockData.abandon.map(item => item.name)
+        tooltip: axisTooltip(),
+        legend: legend(['鎹愮尞鑾峰彇鐜�', '鍣ㄥ畼寮冪敤鐜�']),
+        grid: grid(),
+        xAxis: xAxis(this.regionCompareData.map(item => item.region)),
+        yAxis: {
+          type: 'value',
+          axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' },
+          splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } }
         },
         series: [
-          {
-            name: '寮冪敤鍘熷洜',
-            type: 'pie',
-            radius: ['40%', '70%'],
-            avoidLabelOverlap: false,
-            label: {
-              show: false,
-              position: 'center'
-            },
-            emphasis: {
-              label: {
-                show: true,
-                fontSize: '20',
-                fontWeight: 'bold'
-              }
-            },
-            labelLine: {
-              show: false
-            },
-            data: this.mockData.abandon
-          }
+          barSeries('鎹愮尞鑾峰彇鐜�', this.regionCompareData.map(item => item.acquisition), COLORS.blue),
+          barSeries('鍣ㄥ畼寮冪敤鐜�', this.regionCompareData.map(item => item.abandon), COLORS.coral)
         ]
       };
     },
 
-    // 鑾峰彇浠〃鐩樺浘琛ㄩ厤缃�
+    // 鑾峰彇浠〃鐩樺浘琛ㄩ厤缃紙琛ㄧ洏 + 鎸囬拡锛屾暟鍊肩敱涓婃柟澶ф暟瀛楀睍绀猴紝鍒诲害澶栫疆锛�
     getDashboardOptions(item) {
       const value = parseFloat(item.value);
       const max = item.key === 'avg' ? 5 : 100;
-      const data = [{ value, name: item.title }];
+      const color = item.key === 'abandon' ? COLORS.coral : item.key === 'acquisition' ? COLORS.blue : COLORS.green;
+      // 骞冲潎鍣ㄥ畼鏁拌寖鍥村皬鐢ㄦ暣鏁板埢搴︼紝鐧惧垎姣旂敤 0/25/50/75/100 浜斾釜鍒诲害锛岄伩鍏嶆暟瀛楁嫢鎸�
+      const splitNumber = item.key === 'avg' ? 5 : 4;
 
       return {
-        tooltip: {
-          formatter: '{b}: {c}' + (item.key === 'abandon' ? '%' : item.key === 'acquisition' ? '%' : '')
-        },
         series: [
           {
-            name: item.title,
             type: 'gauge',
-            progress: {
-              show: true,
-              width: 18
-            },
+            startAngle: 210,
+            endAngle: -30,
+            min: 0,
+            max,
+            splitNumber,
+            radius: '60%',
+            center: ['50%', '52%'],
             axisLine: {
               lineStyle: {
-                width: 18
+                width: 10,
+                color: [[value / max, color], [1, '#eef0f5']]
               }
             },
-            axisTick: {
-              show: false
+            pointer: {
+              length: '60%',
+              width: 4,
+              itemStyle: { color }
             },
-            splitLine: {
-              length: 15,
-              lineStyle: {
-                width: 2,
-                color: '#999'
-              }
-            },
+            anchor: { show: false },
+            axisTick: { show: false },
+            // length 褰掗浂 + 璐熻窛绂伙紝浣垮埢搴︽暟瀛楄惤鍦ㄨ〃鐩樺渾寮у渚э紙澶栧洿锛�
+            splitLine: { show: false, length: 0 },
             axisLabel: {
-              distance: 25,
-              color: '#999',
+              distance: -18,
+              color: '#8a919f',
               fontSize: 12
             },
-            anchor: {
-              show: true,
-              showAbove: true,
-              size: 25,
-              itemStyle: {
-                borderWidth: 10
-              }
-            },
-            title: {
-              show: false
-            },
-            detail: {
-              valueAnimation: true,
-              fontSize: 20,
-              offsetCenter: [0, '70%']
-            },
-            data: data,
-            max: max
+            title: { show: false },
+            detail: { show: false },
+            data: [{ value, name: item.title }]
           }
         ]
       };
@@ -455,6 +502,19 @@
       this.loadData();
     },
 
+    // 澶勭悊鍒嗛〉
+    handlePagination(page) {
+      this.query.page = page;
+      this.loadData();
+    },
+
+    // 澶勭悊鍒嗛〉澶у皬鍙樺寲
+    handlePageSizeChange(size) {
+      this.query.limit = size;
+      this.query.page = 1;
+      this.loadData();
+    },
+
     // 澶勭悊瀵煎嚭
     handleExport() {
       this.$message.info('瀵煎嚭鍔熻兘寮�鍙戜腑...');

--
Gitblit v1.9.3