WXL
2026-09-17 3df460667d74868d0a54fce12d5e881bbd6c0c1a
src/views/statistics/case.vue
@@ -35,16 +35,25 @@
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
          <el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
          <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
          <el-button type="primary" icon="el-icon-search" @click="handleSearch"
            >查询</el-button
          >
          <el-button icon="el-icon-refresh" @click="handleReset"
            >重置</el-button
          >
          <el-button
            type="success"
            icon="el-icon-download"
            @click="handleExport"
            >导出</el-button
          >
        </el-form-item>
      </el-form>
    </el-card>
    <!-- 数据概览 -->
    <el-row :gutter="20" class="overview-row">
      <el-col :span="6" v-for="item in overviewData" :key="item.title">
      <el-col :span="8" v-for="item in overviewData" :key="item.title">
        <el-card class="overview-card">
          <div class="overview-content">
            <div class="overview-icon" :style="{ backgroundColor: item.color }">
@@ -71,7 +80,7 @@
              type="text"
              @click="toggleChartType('trend')"
            >
              {{ chartTypes.trend === 'line' ? '柱状图' : '折线图' }}
              {{ chartTypes.trend === "line" ? "柱状图" : "折线图" }}
            </el-button>
          </div>
          <EChartsWrapper
@@ -117,15 +126,23 @@
        border
        stripe
      >
        <el-table-column label="地区" prop="region" align="center"  />
        <el-table-column label="月份" prop="month" align="center" >
        <el-table-column label="地区" prop="region" align="center" />
        <el-table-column label="月份" prop="month" align="center">
          <template slot-scope="{ row }">
            {{ formatMonth(row.month) }}
          </template>
        </el-table-column>
        <el-table-column label="潜在捐献案例数" prop="potentialCount" align="center"  />
        <el-table-column label="完成捐献数量" prop="completedCount" align="center"  />
        <el-table-column label="完成率" prop="completionRate" align="center" >
        <el-table-column
          label="潜在捐献案例数"
          prop="potentialCount"
          align="center"
        />
        <el-table-column
          label="完成捐献数量"
          prop="completedCount"
          align="center"
        />
        <el-table-column label="完成率" prop="completionRate" align="center">
          <template slot-scope="{ row }">
            <el-progress
              :percentage="row.completionRate"
@@ -135,13 +152,9 @@
            <span>{{ row.completionRate }}%</span>
          </template>
        </el-table-column>
        <el-table-column label="案例明细" align="center" >
        <el-table-column label="案例明细" align="center">
          <template slot-scope="{ row }">
            <el-button
              type="text"
              size="mini"
              @click="viewCaseDetail(row)"
            >
            <el-button type="text" size="mini" @click="viewCaseDetail(row)">
              查看({{ row.caseCount || 0 }})
            </el-button>
          </template>
@@ -161,26 +174,21 @@
    </el-card>
    <!-- 案例明细弹窗 -->
    <el-dialog
      title="案例明细"
      :visible.sync="showDetail"
      append-to-body
    >
      <case-detail
        :query="detailQuery"
        @close="showDetail = false"
      />
    <el-dialog title="案例明细" :visible.sync="showDetail" append-to-body>
      <case-detail :query="detailQuery" @close="showDetail = false" />
    </el-dialog>
  </div>
</template>
<script>
import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
import CaseDetail from './components/CaseDetail.vue';
import EChartsWrapper from "@/components/charts/EChartsWrapper.vue";
import CaseDetail from "./components/CaseDetail.vue";
import { caseTrendArea, caseTrend, caseTrendMoon, totals,trendList } from "@/api/businessApi";
import { toTotalsList, toMonth, toNum } from "@/utils/totals";
import statisticsMode from "@/utils/statisticsMode";
import { COLORS, axisTooltip, grid, xAxis, yAxis, legend, barSeries, lineSeries } from "@/utils/chartStyle";
export default {
  name: 'CaseStatistics',
  name: "CaseStatistics",
  components: {
    EChartsWrapper,
    CaseDetail
@@ -195,15 +203,15 @@
        limit: 10
      },
      // 地区选项
      // 地区选项(value 直接传地区名称给接口 area 字段)
      regionOptions: [
        { 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: "临沂地区" }
      ],
      // 加载状态
@@ -215,15 +223,32 @@
      // 概览数据
      overviewData: [
        { title: '总潜在捐献案例', value: 0, desc: '统计周期内', icon: 'el-icon-s-flag', color: '#409EFF' },
        { title: '总完成捐献数量', value: 0, desc: '统计周期内', icon: 'el-icon-circle-check', color: '#67C23A' },
        { title: '平均完成率', value: '0%', desc: '统计周期内', icon: 'el-icon-s-data', color: '#E6A23C' },
        { title: '涉及案例数', value: 0, desc: '统计周期内', icon: 'el-icon-document', color: '#F56C6C' }
        {
          title: "总潜在捐献案例",
          value: 0,
          desc: "统计周期内",
          icon: "el-icon-s-flag",
          color: "#5B8FF9"
        },
        {
          title: "总完成捐献数量",
          value: 0,
          desc: "统计周期内",
          icon: "el-icon-circle-check",
          color: "#5AD8A6"
        },
        {
          title: "平均完成率",
          value: "0%",
          desc: "统计周期内",
          icon: "el-icon-s-data",
          color: "#F6BD16"
        }
      ],
      // 图表类型
      chartTypes: {
        trend: 'line'
        trend: "line"
      },
      // 图表数据
@@ -234,200 +259,157 @@
      // 弹窗控制
      showDetail: false,
      detailQuery: {},
      // 模拟数据
      mockData: {
        table: [
          { id: 1, region: '青岛地区', month: '2024-01', potentialCount: 15, completedCount: 8, completionRate: 53.3, caseCount: 5 },
          { id: 2, region: '青岛地区', month: '2024-02', potentialCount: 18, completedCount: 10, completionRate: 55.6, caseCount: 6 },
          { id: 3, region: '日照地区', month: '2024-01', potentialCount: 8, completedCount: 4, completionRate: 50.0, caseCount: 3 },
          { id: 4, region: '日照地区', month: '2024-02', potentialCount: 10, completedCount: 6, completionRate: 60.0, caseCount: 4 },
          { id: 5, region: '济南地区', month: '2024-01', potentialCount: 20, completedCount: 12, completionRate: 60.0, caseCount: 7 },
          { id: 6, region: '济南地区', month: '2024-02', potentialCount: 22, completedCount: 14, completionRate: 63.6, caseCount: 8 }
        ],
        trend: [
          { month: '2024-01', potential: 55, completed: 31 },
          { month: '2024-02', potential: 64, completed: 39 },
          { month: '2024-03', potential: 58, completed: 35 },
          { month: '2024-04', potential: 62, completed: 40 },
          { month: '2024-05', potential: 68, completed: 45 },
          { month: '2024-06', potential: 72, completed: 50 }
        ],
        region: [
          { region: '青岛地区', potential: 150, completed: 85 },
          { region: '日照地区', potential: 80, completed: 45 },
          { region: '济南地区', potential: 200, completed: 120 },
          { region: '烟台地区', potential: 100, completed: 60 }
        ]
      }
      detailQuery: {}
    };
  },
  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;
    },
    // 加载数据
    async loadData() {
      this.loading = true;
      try {
        // 模拟接口调用
        await new Promise(resolve => setTimeout(resolve, 500));
        const q = this.buildQuery();
        const [areaRes, trendRes, moonRes, totalsRes] = await Promise.all([
          caseTrendArea(q),
          caseTrend(q),
          caseTrendMoon(q),
          totals(q)
        ]);
        // 处理表格数据
        let filteredData = [...this.mockData.table];
        // 地区分布对比
        this.chartData.region = toTotalsList(areaRes).map(item => ({
          region: item.area || item.region || "",
          potential: toNum(item.potentialCount),
          completed: toNum(item.completeCount)
        }));
        if (this.query.region.length > 0) {
          filteredData = filteredData.filter(item =>
            this.query.region.includes(item.region.replace('地区', '').toLowerCase())
          );
        }
        // 案例趋势
        this.chartData.trend = toTotalsList(trendRes).map(item => ({
          month: toMonth(item.donateTime) || toMonth(item.month),
          potential: toNum(item.potentialCount),
          completed: toNum(item.completeCount)
        }));
        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 all = toTotalsList(moonRes).map(item => {
          const potential = toNum(item.potentialCount);
          const completed = toNum(item.completeCount);
          const rate =
            potential > 0
              ? Number(((completed / potential) * 100).toFixed(1))
              : 0;
          return {
            region: item.area || item.region || "",
            month: toMonth(item.donateTime) || toMonth(item.month),
            potentialCount: potential,
            completedCount: completed,
            completionRate: rate,
            caseCount: toNum(item.totalCount) || potential
          };
        });
        this.total = all.length;
        const start = (this.query.page - 1) * this.query.limit;
        this.tableData = all.slice(start, start + this.query.limit);
        this.tableData = filteredData;
        this.total = filteredData.length;
        // 更新概览数据
        this.updateOverviewData(filteredData);
        // 更新图表数据
        this.chartData.trend = [...this.mockData.trend];
        this.chartData.region = [...this.mockData.region];
        this.updateOverviewData(toTotalsList(totalsRes));
      } catch (error) {
        console.error('加载数据失败:', error);
        this.$message.error('数据加载失败');
        console.error("加载数据失败:", error);
        this.$message.error("数据加载失败");
      } finally {
        this.loading = false;
      }
    },
    // 更新概览数据
    updateOverviewData(data) {
      if (data.length === 0) return;
      const totalPotential = data.reduce((sum, item) => sum + item.potentialCount, 0);
      const totalCompleted = data.reduce((sum, item) => sum + item.completedCount, 0);
      const avgRate = data.length > 0
        ? (data.reduce((sum, item) => sum + item.completionRate, 0) / data.length).toFixed(1)
        : 0;
      const totalCases = data.reduce((sum, item) => sum + (item.caseCount || 0), 0);
    // 更新概览数据(总潜在/总完成捐献数来自 /opo/totals/totals 接口)
    updateOverviewData(totalsList) {
      const totalPotential = totalsList.reduce(
        (sum, item) => sum + toNum(item.totalPotential),
        0
      );
      const totalCompleted = totalsList.reduce(
        (sum, item) => sum + toNum(item.totalCompletion),
        0
      );
      const avgRate =
        totalPotential > 0
          ? ((totalCompleted / totalPotential) * 100).toFixed(1)
          : 0;
      this.overviewData[0].value = totalPotential;
      this.overviewData[1].value = totalCompleted;
      this.overviewData[2].value = `${avgRate}%`;
      this.overviewData[3].value = totalCases;
    },
    // 获取趋势图表配置
    getTrendChartOptions() {
      const isLine = this.chartTypes.trend === 'line';
      const isLine = this.chartTypes.trend === "line";
      const xData = this.chartData.trend.map(item => item.month);
      const potential = this.chartData.trend.map(item => item.potential);
      const completed = this.chartData.trend.map(item => item.completed);
      const series = isLine
        ? [
            lineSeries("潜在捐献数", potential, COLORS.blue, { area: true }),
            lineSeries("完成捐献数", completed, COLORS.green, { area: true })
          ]
        : [
            barSeries("潜在捐献数", potential, COLORS.blue),
            barSeries("完成捐献数", completed, COLORS.green)
          ];
      return {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        legend: {
          data: ['潜在捐献数', '完成捐献数']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: this.chartData.trend.map(item => this.formatMonth(item.month))
        },
        yAxis: {
          type: 'value',
          name: '数量(个)'
        },
        series: [
          {
            name: '潜在捐献数',
            type: isLine ? 'line' : 'bar',
            data: this.chartData.trend.map(item => item.potential),
            itemStyle: { color: '#409EFF' },
            smooth: isLine,
            lineStyle: isLine ? { width: 3 } : {}
          },
          {
            name: '完成捐献数',
            type: isLine ? 'line' : 'bar',
            data: this.chartData.trend.map(item => item.completed),
            itemStyle: { color: '#67C23A' },
            smooth: isLine,
            lineStyle: isLine ? { width: 3 } : {}
          }
        ]
        tooltip: axisTooltip(),
        legend: legend(["潜在捐献数", "完成捐献数"]),
        grid: grid(),
        xAxis: xAxis(xData),
        yAxis: yAxis(),
        series
      };
    },
    // 获取地区图表配置
    getRegionChartOptions() {
      return {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        legend: {
          data: ['潜在捐献数', '完成捐献数']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: this.chartData.region.map(item => item.region)
        },
        yAxis: {
          type: 'value',
          name: '数量(个)'
        },
        tooltip: axisTooltip(),
        legend: legend(["潜在捐献数", "完成捐献数"]),
        grid: grid(),
        xAxis: xAxis(this.chartData.region.map(item => item.region)),
        yAxis: yAxis(),
        series: [
          {
            name: '潜在捐献数',
            type: 'bar',
            data: this.chartData.region.map(item => item.potential),
            itemStyle: { color: '#409EFF' }
          },
          {
            name: '完成捐献数',
            type: 'bar',
            data: this.chartData.region.map(item => item.completed),
            itemStyle: { color: '#67C23A' }
          }
          barSeries("潜在捐献数", this.chartData.region.map(item => item.potential), COLORS.blue),
          barSeries("完成捐献数", this.chartData.region.map(item => item.completed), COLORS.green)
        ]
      };
    },
    // 获取进度条颜色
    getProgressColor(percentage) {
      if (percentage >= 60) return '#67C23A';
      if (percentage >= 50) return '#E6A23C';
      return '#F56C6C';
      if (percentage >= 60) return COLORS.green;
      if (percentage >= 50) return COLORS.amber;
      return COLORS.coral;
    },
    // 格式化月份显示
    formatMonth(month) {
      if (!month) return '';
      return month.replace('-', '年') + '月';
      if (!month) return "";
      return month.replace("-", "年") + "月";
    },
    // 处理查询
@@ -456,13 +438,12 @@
    // 处理导出
    handleExport() {
      this.$message.info('导出功能开发中...');
      // 这里可以调用导出接口
      this.$message.info("导出功能开发中...");
    },
    // 切换图表类型
    toggleChartType(type) {
      this.chartTypes[type] = this.chartTypes[type] === 'line' ? 'bar' : 'line';
      this.chartTypes[type] = this.chartTypes[type] === "line" ? "bar" : "line";
    },
    // 查看案例详情
@@ -537,7 +518,7 @@
.overview-desc {
  font-size: 12px;
  color: #C0C4CC;
  color: #c0c4cc;
}
.chart-row {