WXL
2026-09-17 3df460667d74868d0a54fce12d5e881bbd6c0c1a
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>