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/willingness.vue |  464 ++++++++++++++++++++++++++++++---------------------------
 1 files changed, 242 insertions(+), 222 deletions(-)

diff --git a/src/views/statistics/willingness.vue b/src/views/statistics/willingness.vue
index 20e469b..f300ad7 100644
--- a/src/views/statistics/willingness.vue
+++ b/src/views/statistics/willingness.vue
@@ -79,7 +79,6 @@
             {{ formatMonth(row.month) }}
           </template>
         </el-table-column>
-        <el-table-column label="鍣ㄥ畼鍚嶇О" prop="organName" align="center"  />
         <el-table-column label="妗堜緥鏁伴噺" prop="caseCount" align="center"  />
         <el-table-column label="鎹愮尞鏁伴噺" prop="donationCount" align="center"  />
         <el-table-column label="鎹愮尞鎰忔効" prop="willingnessRate" align="center" >
@@ -98,6 +97,18 @@
           </template>
         </el-table-column>
       </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>
@@ -105,7 +116,12 @@
 <script>
 import FilterPanel from '@/components/charts/FilterPanel.vue';
 import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
+import { caseTrend, caseTrendArea, 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';
 
+// 鎹愮尞鎰忔効 = 瀹屾垚鎹愮尞 / 娼滃湪鎹愮尞锛岀粺涓�澶嶇敤 case 鐩稿叧鎺ュ彛鎺ㄥ
 export default {
   name: 'WillingnessStatistics',
   components: {
@@ -118,7 +134,6 @@
       query: {
         region: [],
         monthRange: [],
-        organType: null,
         page: 1,
         limit: 10
       },
@@ -131,13 +146,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'
         },
@@ -150,20 +165,6 @@
           endPlaceholder: '缁撴潫鏈堜唤',
           valueFormat: 'yyyy-MM',
           width: '300px'
-        },
-        {
-          label: '鍣ㄥ畼绫诲瀷',
-          prop: 'organType',
-          type: 'select',
-          options: [
-            { label: '鍏ㄨ倽', value: 'liver' },
-            { label: '鑲捐剰', value: 'kidney' },
-            { label: '蹇冭剰', value: 'heart' },
-            { label: '鑲�', value: 'lung' },
-            { label: '鐪艰鑶�', value: 'cornea' },
-            { label: '澶氬櫒瀹�', value: 'multi' }
-          ],
-          width: '200px'
         }
       ],
 
@@ -172,92 +173,125 @@
 
       // 姒傝鏁版嵁
       overviewData: [
-        { title: '鎬绘渚嬫暟', value: 0, desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-s-flag', color: '#409EFF' },
-        { title: '鎬绘崘鐚暟閲�', value: 0, desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-check', color: '#67C23A' },
-        { title: '骞冲潎鎹愮尞鎰忔効', value: '0%', desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-s-data', color: '#E6A23C' },
-        { title: '瓒嬪娍鍙樺寲', value: '0%', desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-trend', color: '#F56C6C' }
+        { title: '鎬绘渚嬫暟', value: 0, desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-s-flag', color: '#5B8FF9' },
+        { title: '鎬绘崘鐚暟閲�', value: 0, desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-check', color: '#5AD8A6' },
+        { title: '骞冲潎鎹愮尞鎰忔効', value: '0%', desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-s-data', color: '#F6BD16' },
+        { title: '瓒嬪娍鍙樺寲', value: '0%', desc: '缁熻鍛ㄦ湡鍐�', icon: 'el-icon-trend', color: '#E8684A' }
       ],
 
       // 鍥捐〃閰嶇疆
       trendChartOptions: {},
       regionChartOptions: {},
 
+      // 鍥捐〃鏁版嵁
+      trendData: [],
+      regionData: [],
+
       // 琛ㄦ牸鏁版嵁
       tableData: [],
-      total: 0,
-
-      // 妯℃嫙鏁版嵁
-      mockData: {
-        table: [
-          { id: 1, region: '闈掑矝鍦板尯', month: '2024-01', organName: '鍏ㄨ倽', caseCount: 15, donationCount: 10, willingnessRate: 66.7, monthCompare: 2.5 },
-          { id: 2, region: '闈掑矝鍦板尯', month: '2024-01', organName: '鑲捐剰', caseCount: 20, donationCount: 16, willingnessRate: 80.0, monthCompare: 1.2 },
-          { id: 3, region: '鏃ョ収鍦板尯', month: '2024-01', organName: '鍏ㄨ倽', caseCount: 8, donationCount: 5, willingnessRate: 62.5, monthCompare: 0.8 },
-          { id: 4, region: '鏃ョ収鍦板尯', month: '2024-01', organName: '鐪艰鑶�', caseCount: 12, donationCount: 9, willingnessRate: 75.0, monthCompare: 3.1 },
-          { id: 5, region: '闈掑矝鍦板尯', month: '2024-02', organName: '鍏ㄨ倽', caseCount: 18, donationCount: 12, willingnessRate: 66.7, monthCompare: 0.0 },
-          { id: 6, region: '闈掑矝鍦板尯', month: '2024-02', organName: '蹇冭剰', caseCount: 5, donationCount: 4, willingnessRate: 80.0, monthCompare: 5.2 },
-          { id: 7, region: '娴庡崡鍦板尯', month: '2024-02', organName: '鍏ㄨ倽', caseCount: 25, donationCount: 20, willingnessRate: 80.0, monthCompare: 1.8 },
-          { id: 8, region: '娴庡崡鍦板尯', month: '2024-02', organName: '澶氬櫒瀹�', caseCount: 8, donationCount: 6, willingnessRate: 75.0, monthCompare: 2.3 }
-        ],
-        trend: [
-          { month: '2023-10', willingness: 72.5, cases: 80, donations: 58 },
-          { month: '2023-11', willingness: 73.8, cases: 85, donations: 63 },
-          { month: '2023-12', willingness: 75.2, cases: 90, donations: 68 },
-          { month: '2024-01', willingness: 76.8, cases: 95, donations: 73 },
-          { month: '2024-02', willingness: 78.3, cases: 100, donations: 78 },
-          { month: '2024-03', willingness: 79.1, cases: 105, donations: 83 }
-        ],
-        region: [
-          { region: '闈掑矝鍦板尯', willingness: 78.3, cases: 150, donations: 118 },
-          { region: '鏃ョ収鍦板尯', willingness: 72.5, cases: 80, donations: 58 },
-          { region: '娴庡崡鍦板尯', willingness: 82.6, cases: 200, donations: 165 },
-          { region: '鐑熷彴鍦板尯', willingness: 75.8, cases: 100, donations: 76 }
-        ]
-      }
+      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.monthRange && this.query.monthRange.length === 2) {
+        q.startTime = this.query.monthRange[0];
+        q.endTime = this.query.monthRange[1];
+      }
+      return q;
+    },
+
+    // 璁$畻鎰忔効鐜�
+    calcWillingness(potential, complete) {
+      const p = toNum(potential);
+      return p > 0 ? Number(((toNum(complete) / p) * 100).toFixed(1)) : 0;
+    },
+
     // 鍔犺浇鏁版嵁
     async loadData() {
       this.loading = true;
       try {
-        await new Promise(resolve => setTimeout(resolve, 500));
+        const q = this.buildQuery();
+        const [trendRes, areaRes, moonRes] = await Promise.all([
+          caseTrend(q),
+          caseTrendArea(q),
+          caseTrendMoon(q)
+        ]);
 
-        // 澶勭悊琛ㄦ牸鏁版嵁
-        let filteredData = [...this.mockData.table];
+        // 瓒嬪娍锛堟湀浠界淮搴︼級
+        this.trendData = toTotalsList(trendRes)
+          .map(item => {
+            const potential = toNum(item.potentialCount);
+            const complete = toNum(item.completeCount);
+            return {
+              month: toMonth(item.donateTime) || toMonth(item.month),
+              potential,
+              complete,
+              willingness: this.calcWillingness(potential, complete)
+            };
+          })
+          .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())
-          );
-        }
+        // 鍦板尯瀵规瘮
+        this.regionData = toTotalsList(areaRes).map(item => {
+          const potential = toNum(item.potentialCount);
+          const complete = toNum(item.completeCount);
+          return {
+            region: item.area || item.region || '',
+            potential,
+            complete,
+            willingness: this.calcWillingness(potential, complete)
+          };
+        });
 
-        if (this.query.monthRange && this.query.monthRange.length === 2) {
-          const [start, end] = this.query.monthRange;
-          filteredData = filteredData.filter(item =>
-            item.month >= start && item.month <= end
-          );
-        }
+        // 鏄庣粏琛紙鍦板尯 x 鏈堜唤锛�
+        const rows = toTotalsList(moonRes)
+          .map(item => {
+            const potential = toNum(item.potentialCount);
+            const complete = toNum(item.completeCount);
+            return {
+              region: item.area || item.region || '',
+              month: toMonth(item.donateTime) || toMonth(item.month),
+              potential,
+              complete
+            };
+          })
+          .sort((a, b) => String(a.month).localeCompare(String(b.month)));
 
-        if (this.query.organType) {
-          const organMap = { liver: '鑲�', kidney: '鑲�', heart: '蹇冭剰', lung: '鑲�', cornea: '鐪艰鑶�', multi: '澶氬櫒瀹�' };
-          const organName = organMap[this.query.organType];
-          if (organName) {
-            filteredData = filteredData.filter(item => item.organName.includes(organName));
+        // 璁$畻鐜瘮鍙樺寲锛堝悓涓�鍦板尯锛岀浉瀵逛笂涓�鏈堜唤鎰忔効鐜囧彉鍖栵紝鐧惧垎鐐癸級
+        rows.forEach(row => {
+          const willingness = this.calcWillingness(row.potential, row.complete);
+          // 璇ュ湴鍖烘寜鏈堜唤鎺掑簭鍚庣殑涓婁竴涓湀
+          const regionRows = rows.filter(r => r.region === row.region);
+          const idx = regionRows.indexOf(row);
+          let monthCompare = 0;
+          if (idx > 0) {
+            const prev = regionRows[idx - 1];
+            const prevWillingness = this.calcWillingness(prev.potential, prev.complete);
+            monthCompare = Number((willingness - prevWillingness).toFixed(1));
           }
-        }
+          row.willingnessRate = willingness;
+          row.caseCount = row.potential;
+          row.donationCount = row.complete;
+          row.monthCompare = monthCompare;
+        });
 
-        this.tableData = filteredData;
-        this.total = filteredData.length;
+        this.total = rows.length;
+        const start = (this.query.page - 1) * this.query.limit;
+        this.tableData = rows.slice(start, start + this.query.limit);
 
-        // 鏇存柊姒傝鏁版嵁
-        this.updateOverviewData(filteredData);
-
-        // 鏇存柊鍥捐〃
+        this.updateOverviewData(rows);
         this.updateCharts();
-
       } catch (error) {
         console.error('鍔犺浇鏁版嵁澶辫触:', error);
         this.$message.error('鏁版嵁鍔犺浇澶辫触');
@@ -267,20 +301,18 @@
     },
 
     // 鏇存柊姒傝鏁版嵁
-    updateOverviewData(data) {
-      if (data.length === 0) return;
+    updateOverviewData(rows) {
+      const totalCases = rows.reduce((sum, r) => sum + toNum(r.potential), 0);
+      const totalDonations = rows.reduce((sum, r) => sum + toNum(r.complete), 0);
+      const avgWillingness = this.calcWillingness(totalCases, totalDonations);
 
-      const totalCases = data.reduce((sum, item) => sum + item.caseCount, 0);
-      const totalDonations = data.reduce((sum, item) => sum + item.donationCount, 0);
-      const avgWillingness = data.length > 0
-        ? (data.reduce((sum, item) => sum + parseFloat(item.willingnessRate), 0) / data.length).toFixed(1)
-        : 0;
-
-      // 璁$畻瓒嬪娍鍙樺寲锛堝彇鏈�杩戜袱涓湀鐨勫钩鍧囧彉鍖栵級
-      const recentData = data.slice(-2);
-      const trend = recentData.length >= 2
-        ? ((recentData[1].willingnessRate - recentData[0].willingnessRate) / recentData[0].willingnessRate * 100).toFixed(1)
-        : 0;
+      // 瓒嬪娍鍙樺寲锛氭渶杩戜袱涓湀鎰忔効鐜囧樊鍊�
+      let trend = 0;
+      if (this.trendData.length >= 2) {
+        const last = this.trendData[this.trendData.length - 1].willingness;
+        const prev = this.trendData[this.trendData.length - 2].willingness;
+        trend = Number((last - prev).toFixed(1));
+      }
 
       this.overviewData[0].value = totalCases;
       this.overviewData[1].value = totalDonations;
@@ -290,139 +322,55 @@
 
     // 鏇存柊鍥捐〃
     updateCharts() {
-      // 瓒嬪娍鍥�
+      const dualAxis = [
+        {
+          type: 'value',
+          name: '鎰忔効鐜�(%)',
+          nameTextStyle: { color: '#b0b6c3', fontSize: 12 },
+          axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' },
+          splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } }
+        },
+        {
+          type: 'value',
+          name: '鏁伴噺(涓�)',
+          nameTextStyle: { color: '#b0b6c3', fontSize: 12 },
+          axisLabel: { color: '#8a919f', fontSize: 12 },
+          splitLine: { show: false }
+        }
+      ];
+
+      // 瓒嬪娍鍥撅紙鎰忔効鐜囨姌绾� + 鏁伴噺鏌辩姸锛屽弻杞达級
       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.month.replace('-', '骞�') + '鏈�')
+          boundaryGap: true,
+          data: this.trendData.map(item => item.month),
+          axisLine: { show: false },
+          axisTick: { show: false },
+          axisLabel: { color: '#8a919f', fontSize: 12 }
         },
-        yAxis: [
-          {
-            type: 'value',
-            name: '鎰忔効鐜�(%)',
-            min: 60,
-            max: 90,
-            axisLabel: {
-              formatter: '{value}%'
-            }
-          },
-          {
-            type: 'value',
-            name: '鏁伴噺(涓�)',
-            position: 'right'
-          }
-        ],
+        yAxis: dualAxis,
         series: [
-          {
-            name: '鎹愮尞鎰忔効',
-            type: 'line',
-            smooth: true,
-            data: this.mockData.trend.map(item => item.willingness),
-            itemStyle: { color: '#409EFF' },
-            markLine: {
-              data: [
-                { yAxis: 75, name: '鐩爣绾�' }
-              ],
-              lineStyle: {
-                type: 'dashed',
-                color: '#F56C6C'
-              }
-            }
-          },
-          {
-            name: '妗堜緥鏁伴噺',
-            type: 'bar',
-            yAxisIndex: 1,
-            data: this.mockData.trend.map(item => item.cases),
-            itemStyle: { color: '#67C23A' }
-          },
-          {
-            name: '鎹愮尞鏁伴噺',
-            type: 'bar',
-            yAxisIndex: 1,
-            data: this.mockData.trend.map(item => item.donations),
-            itemStyle: { color: '#E6A23C' }
-          }
+          lineSeries('鎹愮尞鎰忔効', this.trendData.map(item => item.willingness), COLORS.blue, { area: true }),
+          barSeries('妗堜緥鏁伴噺', this.trendData.map(item => item.potential), COLORS.green, { yAxisIndex: 1 }),
+          barSeries('鎹愮尞鏁伴噺', this.trendData.map(item => item.complete), COLORS.amber, { yAxisIndex: 1 })
         ]
       };
 
       // 鍦板尯瀵规瘮鍥�
       this.regionChartOptions = {
-        tooltip: {
-          trigger: 'axis',
-          axisPointer: {
-            type: 'shadow'
-          }
-        },
-        legend: {
-          data: ['鎹愮尞鎰忔効', '妗堜緥鏁伴噺', '鎹愮尞鏁伴噺']
-        },
-        grid: {
-          left: '3%',
-          right: '4%',
-          bottom: '3%',
-          containLabel: true
-        },
-        xAxis: {
-          type: 'category',
-          data: this.mockData.region.map(item => item.region)
-        },
-        yAxis: [
-          {
-            type: 'value',
-            name: '鎰忔効鐜�(%)',
-            min: 60,
-            max: 90,
-            axisLabel: {
-              formatter: '{value}%'
-            }
-          },
-          {
-            type: 'value',
-            name: '鏁伴噺(涓�)',
-            position: 'right'
-          }
-        ],
+        tooltip: axisTooltip(),
+        legend: legend(['鎹愮尞鎰忔効', '妗堜緥鏁伴噺', '鎹愮尞鏁伴噺']),
+        grid: grid(),
+        xAxis: xAxis(this.regionData.map(item => item.region)),
+        yAxis: dualAxis,
         series: [
-          {
-            name: '鎹愮尞鎰忔効',
-            type: 'bar',
-            data: this.mockData.region.map(item => item.willingness),
-            itemStyle: { color: '#409EFF' }
-          },
-          {
-            name: '妗堜緥鏁伴噺',
-            type: 'bar',
-            yAxisIndex: 1,
-            data: this.mockData.region.map(item => item.cases),
-            itemStyle: { color: '#67C23A' }
-          },
-          {
-            name: '鎹愮尞鏁伴噺',
-            type: 'bar',
-            yAxisIndex: 1,
-            data: this.mockData.region.map(item => item.donations),
-            itemStyle: { color: '#E6A23C' }
-          }
+          barSeries('鎹愮尞鎰忔効', this.regionData.map(item => item.willingness), COLORS.blue),
+          barSeries('妗堜緥鏁伴噺', this.regionData.map(item => item.potential), COLORS.green, { yAxisIndex: 1 }),
+          barSeries('鎹愮尞鏁伴噺', this.regionData.map(item => item.complete), COLORS.amber, { yAxisIndex: 1 })
         ]
       };
     },
@@ -435,9 +383,9 @@
 
     // 鑾峰彇杩涘害鏉¢鑹�
     getProgressColor(percentage) {
-      if (percentage >= 80) return '#67C23A';
-      if (percentage >= 70) return '#E6A23C';
-      return '#F56C6C';
+      if (percentage >= 80) return COLORS.green;
+      if (percentage >= 70) return COLORS.amber;
+      return COLORS.coral;
     },
 
     // 鑾峰彇姣旇緝鏍囩绫诲瀷
@@ -458,10 +406,22 @@
       this.query = {
         region: [],
         monthRange: [],
-        organType: null,
         page: 1,
         limit: 10
       };
+      this.loadData();
+    },
+
+    // 澶勭悊鍒嗛〉
+    handlePagination(page) {
+      this.query.page = page;
+      this.loadData();
+    },
+
+    // 澶勭悊鍒嗛〉澶у皬鍙樺寲
+    handlePageSizeChange(size) {
+      this.query.limit = size;
+      this.query.page = 1;
       this.loadData();
     },
 
@@ -472,3 +432,63 @@
   }
 };
 </script>
+
+<style scoped>
+.statistics-page {
+  padding: 20px;
+}
+
+.overview-row {
+  margin-bottom: 20px;
+}
+
+.overview-card {
+  height: 100%;
+}
+
+.overview-content {
+  display: flex;
+  align-items: center;
+}
+
+.overview-icon {
+  width: 50px;
+  height: 50px;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 15px;
+}
+
+.overview-icon i {
+  font-size: 24px;
+  color: white;
+}
+
+.overview-info {
+  flex: 1;
+}
+
+.overview-title {
+  font-size: 14px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.overview-value {
+  font-size: 24px;
+  font-weight: bold;
+  color: #303133;
+  margin-bottom: 4px;
+}
+
+.overview-desc {
+  font-size: 12px;
+  color: #C0C4CC;
+}
+
+.chart-row {
+  margin-bottom: 20px;
+}
+</style>

--
Gitblit v1.9.3