WXL
2026-05-17 b22b937bf902dcfbbf6d2cc6dc95ca47d160e199
统计维护
已修改1个文件
已添加15个文件
4166 ■■■■■ 文件已修改
package.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/statistics/index.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/charts/EChartsWrapper.vue 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/charts/FilterPanel.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/statistics.css 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/excel.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/statistics.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/case.vue 546 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/components/CaseDetail.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/components/OrganDetail.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/donor.vue 702 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/index.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/organ.vue 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/rate.vue 506 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/utilization.vue 688 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/statistics/willingness.vue 474 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -44,10 +44,10 @@
    "core-js": "3.8.1",
    "dayjs": "^1.11.1",
    "dingtalk-jsapi": "^3.1.1",
    "echarts": "4.9.0",
    "echarts": "^4.9.0",
    "element-china-area-data": "^5.0.2",
    "element-ui": "2.15.6",
    "file-saver": "2.0.5",
    "file-saver": "^2.0.5",
    "fuse.js": "6.4.3",
    "highlight.js": "9.18.5",
    "js-beautify": "1.13.0",
@@ -68,7 +68,8 @@
    "vue-router": "3.4.9",
    "vue-year-picker": "^1.1.0",
    "vuedraggable": "2.24.3",
    "vuex": "3.6.0"
    "vuex": "3.6.0",
    "xlsx": "^0.18.5"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "4.4.6",
src/api/statistics/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
// api/statistics/index.js
import request from '@/utils/request';
// æ¡ˆä¾‹ç»Ÿè®¡
export function getCaseStatistics(params) {
  return request({
    url: '/statistics/case/list',
    method: 'get',
    params
  });
}
// æçŒ®å™¨å®˜ç»Ÿè®¡
export function getOrganStatistics(params) {
  return request({
    url: '/statistics/organ/list',
    method: 'get',
    params
  });
}
// èŽ·å–çŽ‡ç»Ÿè®¡
export function getAcquisitionRate(params) {
  return request({
    url: '/statistics/acquisition/rate',
    method: 'get',
    params
  });
}
// æçŒ®æ„æ„¿ç»Ÿè®¡
export function getWillingness(params) {
  return request({
    url: '/statistics/willingness',
    method: 'get',
    params
  });
}
// æçŒ®è€…分析
export function getDonorAnalysis(params) {
  return request({
    url: '/statistics/donor/analysis',
    method: 'get',
    params
  });
}
// å™¨å®˜èŽ·å–åˆ©ç”¨
export function getOrganUtilization(params) {
  return request({
    url: '/statistics/organ/utilization',
    method: 'get',
    params
  });
}
src/components/charts/EChartsWrapper.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
<!-- src/components/charts/EChartsWrapper.vue -->
<template>
  <div :id="id" :style="chartStyle"></div>
</template>
<script>
import echarts from 'echarts';
export default {
  name: 'EChartsWrapper',
  props: {
    id: {
      type: String,
      default: `chart_${Date.now()}`
    },
    width: {
      type: [String, Number],
      default: '100%'
    },
    height: {
      type: [String, Number],
      default: '400px'
    },
    options: {
      type: Object,
      required: true
    },
    theme: {
      type: String,
      default: null
    }
  },
  computed: {
    chartStyle() {
      return {
        width: typeof this.width === 'number' ? `${this.width}px` : this.width,
        height: typeof this.height === 'number' ? `${this.height}px` : this.height
      };
    }
  },
  data() {
    return {
      chart: null
    };
  },
  watch: {
    options: {
      deep: true,
      handler(newOptions) {
        if (this.chart && newOptions) {
          this.chart.setOption(newOptions, true);
        }
      }
    }
  },
  mounted() {
    this.initChart();
    window.addEventListener('resize', this.handleResize);
  },
  beforeDestroy() {
    if (this.chart) {
      this.chart.dispose();
      this.chart = null;
    }
    window.removeEventListener('resize', this.handleResize);
  },
  methods: {
    initChart() {
      this.chart = echarts.init(document.getElementById(this.id), this.theme);
      this.chart.setOption(this.options);
    },
    handleResize() {
      if (this.chart) {
        this.chart.resize();
      }
    }
  }
};
</script>
src/components/charts/FilterPanel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
<!-- src/components/FilterPanel.vue -->
<template>
  <el-card class="filter-panel">
    <el-form :inline="true" :model="formData" class="filter-form" label-width="80px">
      <!-- åŠ¨æ€æ¸²æŸ“è¡¨å•å­—æ®µ -->
      <template v-for="field in fields">
        <!-- ä¸‹æ‹‰é€‰æ‹©ï¼ˆå•选) -->
        <el-form-item
          v-if="field.type === 'select' && !field.multiple"
          :key="field.prop"
          :label="field.label"
        >
          <el-select
            v-model="formData[field.prop]"
            :placeholder="field.placeholder || `请选择${field.label}`"
            clearable
            :style="{ width: field.width || '200px' }"
          >
            <el-option
              v-for="item in field.options"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- ä¸‹æ‹‰é€‰æ‹©ï¼ˆå¤šé€‰ï¼‰ -->
        <el-form-item
          v-if="field.type === 'select' && field.multiple"
          :key="field.prop"
          :label="field.label"
        >
          <el-select
            v-model="formData[field.prop]"
            :placeholder="field.placeholder || `请选择${field.label}`"
            clearable
            multiple
            collapse-tags
            :style="{ width: field.width || '300px' }"
          >
            <el-option
              v-for="item in field.options"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <!-- æ—¥æœŸèŒƒå›´é€‰æ‹© -->
        <el-form-item
          v-if="field.type === 'daterange'"
          :key="field.prop"
          :label="field.label"
        >
          <el-date-picker
            v-model="formData[field.prop]"
            :type="field.dateType || 'daterange'"
            :range-separator="field.rangeSeparator || '至'"
            :start-placeholder="field.startPlaceholder || '开始日期'"
            :end-placeholder="field.endPlaceholder || '结束日期'"
            :value-format="field.valueFormat || 'yyyy-MM-dd'"
            :style="{ width: field.width || '300px' }"
          />
        </el-form-item>
        <!-- è¾“入框 -->
        <el-form-item
          v-if="field.type === 'input'"
          :key="field.prop"
          :label="field.label"
        >
          <el-input
            v-model="formData[field.prop]"
            :placeholder="field.placeholder || `请输入${field.label}`"
            clearable
            :style="{ width: field.width || '200px' }"
            @keyup.enter.native="handleSearch"
          />
        </el-form-item>
        <!-- è‡ªå®šä¹‰æ’æ§½ -->
        <el-form-item
          v-if="field.type === 'slot'"
          :key="field.prop"
          :label="field.label"
        >
          <slot :name="field.slotName"></slot>
        </el-form-item>
      </template>
      <!-- æ“ä½œæŒ‰é’® -->
      <el-form-item class="filter-actions">
        <el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
        <el-button icon="el-icon-refresh" @click="handleReset">重置</el-button>
        <slot name="extra-actions"></slot>
      </el-form-item>
    </el-form>
  </el-card>
</template>
<script>
export default {
  name: 'FilterPanel',
  props: {
    // è¡¨å•字段配置
    fields: {
      type: Array,
      default: () => []
    },
    // è¡¨å•默认值
    defaultValue: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      formData: { ...this.defaultValue }
    };
  },
  watch: {
    defaultValue: {
      deep: true,
      handler(newVal) {
        this.formData = { ...newVal };
      }
    }
  },
  methods: {
    handleSearch() {
      this.$emit('search', this.formData);
    },
    handleReset() {
      this.formData = { ...this.defaultValue };
      this.$emit('reset', this.formData);
    },
    // æš´éœ²ç»™çˆ¶ç»„件的方法
    getFormData() {
      return { ...this.formData };
    },
    setFormData(data) {
      this.formData = { ...data };
    }
  }
};
</script>
<style scoped>
.filter-panel {
  margin-bottom: 20px;
}
.filter-form {
  margin-bottom: 0;
}
.filter-actions {
  float: right;
  margin-right: 0;
  margin-bottom: 0;
}
</style>
src/styles/statistics.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
/* src/styles/statistics.css */
/* ç»Ÿè®¡é¡µé¢å…¬å…±æ ·å¼ */
.statistics-container {
  padding: 20px;
  background-color: #f5f7fa;
}
.filter-panel {
  margin-bottom: 20px;
  background: white;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.chart-container {
  background: white;
  border-radius: 4px;
  margin-bottom: 20px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.chart-container .chart-header {
  padding: 20px 20px 0;
  border-bottom: 1px solid #ebeef5;
}
.chart-container .chart-body {
  padding: 20px;
  height: 400px;
}
.data-table {
  background: white;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .chart-container {
    margin-bottom: 10px;
  }
  .chart-container .chart-body {
    height: 300px;
  }
}
src/utils/excel.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
// src/utils/excel.js
import * as XLSX from 'xlsx';
import FileSaver from 'file-saver';
export function exportExcel(data, fileName = '导出数据') {
  // åˆ›å»ºå·¥ä½œç°¿
  const wb = XLSX.utils.book_new();
  // åˆ›å»ºå·¥ä½œè¡¨
  const ws = XLSX.utils.aoa_to_sheet(data);
  // æ·»åŠ åˆ°å·¥ä½œç°¿
  XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
  // ç”ŸæˆExcel文件
  const wbout = XLSX.write(wb, {
    bookType: 'xlsx',
    type: 'array'
  });
  // åˆ›å»ºBlob对象
  const blob = new Blob([wbout], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  });
  // ä¿å­˜æ–‡ä»¶
  FileSaver.saveAs(blob, `${fileName}_${new Date().getTime()}.xlsx`);
}
export function exportJsonToExcel(jsonData, headers, fileName = '导出数据') {
  const data = [headers];
  jsonData.forEach(item => {
    const row = headers.map(header => {
      if (typeof header === 'object') {
        return item[header.key] || '';
      }
      return item[header] || '';
    });
    data.push(row);
  });
  exportExcel(data, fileName);
}
src/utils/statistics.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
// src/utils/statistics.js
export const regionOptions = [
  { label: '青岛地区', value: 'qingdao' },
  { label: '日照地区', value: 'rizhao' },
  { label: '济南地区', value: 'jinan' },
  { label: '烟台地区', value: 'yantai' },
  { label: '威海地区', value: 'weihai' },
  { label: '潍坊地区', value: 'weifang' },
  { label: '临沂地区', value: 'linyi' }
];
export const organOptions = [
  { label: '全肝', value: 'liver' },
  { label: '左肾', value: 'leftKidney' },
  { label: '右肾', value: 'rightKidney' },
  { label: '心脏', value: 'heart' },
  { label: '全肺', value: 'lung' },
  { label: '胰腺', value: 'pancreas' },
  { label: '眼角膜', value: 'cornea' }
];
export const ageRangeOptions = [
  { label: '<17岁', value: '0-17' },
  { label: '18-49岁', value: '18-49' },
  { label: '50-69岁', value: '50-69' },
  { label: '>69岁', value: '70-100' }
];
// å¯¼å‡ºExcel功能
export function exportToExcel(data, filename = '导出数据') {
  import('xlsx').then(XLSX => {
    const ws = XLSX.utils.json_to_sheet(data);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
    XLSX.writeFile(wb, `${filename}.xlsx`);
  });
}
src/views/statistics/case.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,546 @@
<!-- src/views/statistics/case.vue -->
<template>
  <div class="statistics-page">
    <!-- ç­›é€‰é¢æ¿ -->
    <el-card class="filter-card">
      <el-form :inline="true" :model="query" class="filter-form">
        <el-form-item label="地区">
          <el-select
            v-model="query.region"
            placeholder="请选择地区"
            clearable
            multiple
            collapse-tags
            style="width: 200px"
          >
            <el-option
              v-for="item in regionOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="月份范围">
          <el-date-picker
            v-model="query.monthRange"
            type="monthrange"
            range-separator="至"
            start-placeholder="开始月份"
            end-placeholder="结束月份"
            value-format="yyyy-MM"
            style="width: 300px"
          />
        </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-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-card class="overview-card">
          <div class="overview-content">
            <div class="overview-icon" :style="{ backgroundColor: item.color }">
              <i :class="item.icon"></i>
            </div>
            <div class="overview-info">
              <div class="overview-title">{{ item.title }}</div>
              <div class="overview-value">{{ item.value }}</div>
              <div class="overview-desc">{{ item.desc }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>案例趋势分析</span>
            <el-button
              style="float: right; padding: 3px 0"
              type="text"
              @click="toggleChartType('trend')"
            >
              {{ chartTypes.trend === 'line' ? '柱状图' : '折线图' }}
            </el-button>
          </div>
          <EChartsWrapper
            :id="'trend-chart'"
            :options="getTrendChartOptions()"
            height="400px"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>地区分布对比</span>
          </div>
          <EChartsWrapper
            :id="'region-chart'"
            :options="getRegionChartOptions()"
            height="400px"
          />
        </el-card>
      </el-col>
    </el-row>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <div slot="header" class="clearfix">
        <span>案例统计明细</span>
        <el-button
          style="float: right;"
          type="primary"
          icon="el-icon-zoom-in"
          @click="showDetail = true"
        >
          æŸ¥çœ‹æ¡ˆä¾‹æ˜Žç»†
        </el-button>
      </div>
      <el-table
        v-loading="loading"
        :data="tableData"
        style="width: 100%"
        border
        stripe
      >
        <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" >
          <template slot-scope="{ row }">
            <el-progress
              :percentage="row.completionRate"
              :color="getProgressColor(row.completionRate)"
              :show-text="false"
            />
            <span>{{ row.completionRate }}%</span>
          </template>
        </el-table-column>
        <el-table-column label="案例明细" align="center" >
          <template slot-scope="{ row }">
            <el-button
              type="text"
              size="mini"
              @click="viewCaseDetail(row)"
            >
              æŸ¥çœ‹({{ row.caseCount || 0 }})
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-pagination
        v-show="total > 0"
        :total="total"
        :page.sync="query.page"
        :limit.sync="query.limit"
        @pagination="handlePagination"
        style="margin-top: 20px;"
        layout="total, sizes, prev, pager, next, jumper"
        :page-sizes="[10, 20, 50, 100]"
      />
    </el-card>
    <!-- æ¡ˆä¾‹æ˜Žç»†å¼¹çª— -->
    <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';
export default {
  name: 'CaseStatistics',
  components: {
    EChartsWrapper,
    CaseDetail
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      query: {
        region: [],
        monthRange: [],
        page: 1,
        limit: 10
      },
      // åœ°åŒºé€‰é¡¹
      regionOptions: [
        { label: '青岛地区', value: 'qingdao' },
        { label: '日照地区', value: 'rizhao' },
        { label: '济南地区', value: 'jinan' },
        { label: '烟台地区', value: 'yantai' },
        { label: '威海地区', value: 'weihai' },
        { label: '潍坊地区', value: 'weifang' },
        { label: '临沂地区', value: 'linyi' }
      ],
      // åŠ è½½çŠ¶æ€
      loading: false,
      // è¡¨æ ¼æ•°æ®
      tableData: [],
      total: 0,
      // æ¦‚览数据
      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' }
      ],
      // å›¾è¡¨ç±»åž‹
      chartTypes: {
        trend: 'line'
      },
      // å›¾è¡¨æ•°æ®
      chartData: {
        trend: [],
        region: []
      },
      // å¼¹çª—控制
      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 }
        ]
      }
    };
  },
  created() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        // æ¨¡æ‹ŸæŽ¥å£è°ƒç”¨
        await new Promise(resolve => setTimeout(resolve, 500));
        // å¤„理表格数据
        let filteredData = [...this.mockData.table];
        if (this.query.region.length > 0) {
          filteredData = filteredData.filter(item =>
            this.query.region.includes(item.region.replace('地区', '').toLowerCase())
          );
        }
        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
          );
        }
        this.tableData = filteredData;
        this.total = filteredData.length;
        // æ›´æ–°æ¦‚览数据
        this.updateOverviewData(filteredData);
        // æ›´æ–°å›¾è¡¨æ•°æ®
        this.chartData.trend = [...this.mockData.trend];
        this.chartData.region = [...this.mockData.region];
      } catch (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);
      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';
      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 } : {}
          }
        ]
      };
    },
    // èŽ·å–åœ°åŒºå›¾è¡¨é…ç½®
    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: '数量(个)'
        },
        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' }
          }
        ]
      };
    },
    // èŽ·å–è¿›åº¦æ¡é¢œè‰²
    getProgressColor(percentage) {
      if (percentage >= 60) return '#67C23A';
      if (percentage >= 50) return '#E6A23C';
      return '#F56C6C';
    },
    // æ ¼å¼åŒ–月份显示
    formatMonth(month) {
      if (!month) return '';
      return month.replace('-', 'å¹´') + '月';
    },
    // å¤„理查询
    handleSearch() {
      this.query.page = 1;
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.query = {
        region: [],
        monthRange: [],
        page: 1,
        limit: 10
      };
      this.loadData();
    },
    // å¤„理分页
    handlePagination({ page, limit }) {
      this.query.page = page;
      this.query.limit = limit;
      this.loadData();
    },
    // å¤„理导出
    handleExport() {
      this.$message.info('导出功能开发中...');
      // è¿™é‡Œå¯ä»¥è°ƒç”¨å¯¼å‡ºæŽ¥å£
    },
    // åˆ‡æ¢å›¾è¡¨ç±»åž‹
    toggleChartType(type) {
      this.chartTypes[type] = this.chartTypes[type] === 'line' ? 'bar' : 'line';
    },
    // æŸ¥çœ‹æ¡ˆä¾‹è¯¦æƒ…
    viewCaseDetail(row) {
      this.detailQuery = {
        region: row.region,
        month: row.month
      };
      this.showDetail = true;
    }
  }
};
</script>
<style scoped>
.statistics-page {
  padding: 20px;
}
.filter-card {
  margin-bottom: 20px;
}
.filter-form {
  margin-bottom: 0;
}
.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>
src/views/statistics/components/CaseDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
<!-- src/views/statistics/components/CaseDetail.vue -->
<template>
  <div class="case-detail">
    <el-table
      v-loading="loading"
      :data="caseList"
      style="width: 100%"
      border
      stripe
    >
      <el-table-column label="案例编号" prop="caseNo" align="center"  />
      <el-table-column label="捐献者姓名" prop="donorName" align="center"  />
      <el-table-column label="住院号" prop="inpatientNo" align="center"  />
      <el-table-column label="案例状态" prop="status" align="center" >
        <template slot-scope="{ row }">
          <el-tag :type="getStatusTag(row.status)" size="small">
            {{ getStatusText(row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="捐献器官" prop="organs" align="center" >
        <template slot-scope="{ row }">
          <span v-if="row.organs && row.organs.length > 0">
            {{ row.organs.join('、') }}
          </span>
          <span v-else>-</span>
        </template>
      </el-table-column>
      <el-table-column label="捐献时间" prop="donationTime" align="center"  />
      <el-table-column label="死亡原因" prop="deathCause" align="center"  />
      <el-table-column label="医院" prop="hospital" align="center" width="200" />
    </el-table>
    <div style="text-align: center; margin-top: 20px;">
      <el-button @click="$emit('close')">关闭</el-button>
    </div>
  </div>
</template>
<script>
export default {
  name: 'CaseDetail',
  props: {
    query: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      loading: false,
      caseList: [
        { caseNo: 'C202401001', donorName: '张三', inpatientNo: 'ZY202401001', status: 'completed', organs: ['肝脏', '肾脏'], donationTime: '2024-01-15 10:30', deathCause: '脑外伤', hospital: '青岛大学附属医院' },
        { caseNo: 'C202401002', donorName: '李四', inpatientNo: 'ZY202401002', status: 'completed', organs: ['心脏', '眼角膜'], donationTime: '2024-01-20 14:20', deathCause: '心搏骤停', hospital: '青岛市立医院' },
        { caseNo: 'C202401003', donorName: '王五', inpatientNo: 'ZY202401003', status: 'potential', organs: [], donationTime: null, deathCause: '脑血管意外', hospital: '青岛市中心医院' }
      ]
    };
  },
  methods: {
    getStatusTag(status) {
      const map = { completed: 'success', potential: 'warning', abandoned: 'danger' };
      return map[status] || 'info';
    },
    getStatusText(status) {
      const map = { completed: '已完成', potential: '潜在捐献', abandoned: '放弃捐献' };
      return map[status] || '未知';
    }
  }
};
</script>
<style scoped>
.case-detail {
  padding: 0;
}
</style>
src/views/statistics/components/OrganDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
<!-- src/views/statistics/components/OrganDetail.vue -->
<template>
  <div class="organ-detail">
    <el-table
      v-loading="loading"
      :data="organList"
      style="width: 100%"
      border
      stripe
    >
      <el-table-column label="器官编号" prop="organNo" align="center"  />
      <el-table-column label="器官名称" prop="organName" align="center"  />
      <el-table-column label="捐献者" prop="donorName" align="center"  />
      <el-table-column label="地区" prop="region" align="center"  />
      <el-table-column label="年份" prop="year" align="center"  />
      <el-table-column label="器官状态" prop="status" align="center" >
        <template slot-scope="{ row }">
          <el-tag :type="getStatusTag(row.status)" size="small">
            {{ getStatusText(row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="获取时间" prop="acquisitionTime" align="center"  />
      <el-table-column label="移植时间" prop="transplantTime" align="center" >
        <template slot-scope="{ row }">
          {{ row.transplantTime || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="弃用原因" prop="abandonReason" align="center" >
        <template slot-scope="{ row }">
          {{ row.abandonReason || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="受体姓名" prop="recipientName" align="center" >
        <template slot-scope="{ row }">
          {{ row.recipientName || '-' }}
        </template>
      </el-table-column>
      <el-table-column label="移植医院" prop="hospital" align="center" />
      <el-table-column label="操作医生" prop="doctor" align="center"  />
    </el-table>
    <div style="text-align: center; margin-top: 20px;">
      <el-button @click="$emit('close')">关闭</el-button>
    </div>
  </div>
</template>
<script>
export default {
  name: 'OrganDetail',
  props: {
    query: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      loading: false,
      organList: [
        { organNo: 'O202401001', organName: '全肝', donorName: '张三', region: '青岛地区', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-15 10:30', transplantTime: '2024-01-15 12:45', abandonReason: '', recipientName: '王明', hospital: '青岛大学附属医院', doctor: '李医生' },
        { organNo: 'O202401002', organName: '左肾', donorName: '张三', region: '青岛地区', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-15 10:30', transplantTime: '2024-01-15 13:20', abandonReason: '', recipientName: '陈强', hospital: '青岛市立医院', doctor: '张医生' },
        { organNo: 'O202401003', organName: '右肾', donorName: '张三', region: '青岛地区', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-15 10:30', transplantTime: '2024-01-15 13:50', abandonReason: '', recipientName: '刘伟', hospital: '青岛市中心医院', doctor: '王医生' },
        { organNo: 'O202401004', organName: '全肝', donorName: '李四', region: '青岛地区', year: '2024', status: 'abandoned', acquisitionTime: '2024-01-20 14:20', transplantTime: null, abandonReason: '器官质量不合格', recipientName: '', hospital: '青岛市立医院', doctor: '张医生' },
        { organNo: 'O202401005', organName: '心脏', donorName: '李四', region: '青岛地区', year: '2024', status: 'transplanted', acquisitionTime: '2024-01-20 14:20', transplantTime: '2024-01-20 15:30', abandonReason: '', recipientName: '赵刚', hospital: '青岛市立医院', doctor: '张医生' }
      ]
    };
  },
  methods: {
    getStatusTag(status) {
      const map = { transplanted: 'success', abandoned: 'danger', pending: 'warning' };
      return map[status] || 'info';
    },
    getStatusText(status) {
      const map = { transplanted: '已移植', abandoned: '已弃用', pending: '待移植' };
      return map[status] || '未知';
    }
  }
};
</script>
<style scoped>
.organ-detail {
  padding: 0;
}
</style>
src/views/statistics/donor.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,702 @@
<!-- src/views/statistics/donor.vue -->
<template>
  <div class="statistics-page">
    <!-- ç­›é€‰é¢æ¿ -->
    <FilterPanel
      :fields="filterFields"
      :default-value="query"
      @search="handleSearch"
      @reset="handleReset"
    >
      <template #extra-actions>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </template>
    </FilterPanel>
    <!-- æ•°æ®æ¦‚览 -->
    <el-row :gutter="20" class="overview-row">
      <el-col :span="6" 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 }">
              <i :class="item.icon"></i>
            </div>
            <div class="overview-info">
              <div class="overview-title">{{ item.title }}</div>
              <div class="overview-value">{{ item.value }}</div>
              <div class="overview-desc">{{ item.desc }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>捐献者年龄分布</span>
          </div>
          <EChartsWrapper
            :id="'age-chart'"
            :options="ageChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>性别与血型分布</span>
          </div>
          <EChartsWrapper
            :id="'gender-blood-chart'"
            :options="genderBloodChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>中国分类分布</span>
          </div>
          <EChartsWrapper
            :id="'category-chart'"
            :options="categoryChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>死亡原因分析</span>
          </div>
          <EChartsWrapper
            :id="'death-chart'"
            :options="deathChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
    </el-row>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <div slot="header" class="clearfix">
        <span>捐献者详细信息</span>
      </div>
      <el-table
        v-loading="loading"
        :data="tableData"
        style="width: 100%"
        border
        stripe
      >
        <el-table-column label="捐献者编号" prop="donorNo" align="center"  />
        <el-table-column label="姓名" prop="name" align="center"  />
        <el-table-column label="性别" prop="gender" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="row.gender === '男' ? 'primary' : 'danger'" size="small">
              {{ row.gender }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="年龄" prop="age" align="center"  />
        <el-table-column label="血型" prop="bloodType" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="getBloodTypeTag(row.bloodType)" size="small">
              {{ row.bloodType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="地区" prop="region" align="center"  />
        <el-table-column label="捐献时间" prop="donationTime" align="center"  />
        <el-table-column label="中国分类" prop="chinaCategory" align="center"  />
        <el-table-column label="死亡原因" prop="deathCause" align="center"  />
        <el-table-column label="捐献器官" prop="organs" align="center" >
          <template slot-scope="{ row }">
            <span>{{ row.organs.join('、') }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" " fixed="right">
          <template slot-scope="{ row }">
            <el-button
              type="text"
              size="mini"
              @click="viewDonorDetail(row)"
            >
              è¯¦æƒ…
            </el-button>
          </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>
<script>
// ä¿®æ­£å¯¼å…¥è·¯å¾„
import FilterPanel from '@/components/charts/FilterPanel.vue';
import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
export default {
  name: 'DonorStatistics',
  components: {
    FilterPanel,
    EChartsWrapper
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      query: {
        region: [],
        monthRange: [],
        ageRange: [],
        gender: null,
        bloodType: null,
        page: 1,
        limit: 10
      },
      // ç­›é€‰å­—段配置
      filterFields: [
        {
          label: '地区',
          prop: 'region',
          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' }
          ],
          width: '300px'
        },
        {
          label: '月份范围',
          prop: 'monthRange',
          type: 'daterange',
          dateType: 'monthrange',
          startPlaceholder: '开始月份',
          endPlaceholder: '结束月份',
          valueFormat: 'yyyy-MM',
          width: '300px'
        },
        {
          label: '年龄段',
          prop: 'ageRange',
          type: 'select',
          multiple: true,
          options: [
            { label: '<17岁', value: '0-17' },
            { label: '18-49岁', value: '18-49' },
            { label: '50-69岁', value: '50-69' },
            { label: '>69岁', value: '70-100' }
          ],
          width: '200px'
        },
        {
          label: '性别',
          prop: 'gender',
          type: 'select',
          options: [
            { label: '男', value: '男' },
            { label: '女', value: '女' }
          ],
          width: '100px'
        },
        {
          label: '血型',
          prop: 'bloodType',
          type: 'select',
          options: [
            { label: 'A型', value: 'A' },
            { label: 'B型', value: 'B' },
            { label: 'O型', value: 'O' },
            { label: 'AB型', value: 'AB' }
          ],
          width: '100px'
        }
      ],
      // åŠ è½½çŠ¶æ€
      loading: false,
      // æ¦‚览数据
      overviewData: [
        { title: '总捐献者数', value: 0, desc: '统计周期内', icon: 'el-icon-user', color: '#409EFF' },
        { title: '男性占比', value: '0%', desc: '统计周期内', icon: 'el-icon-male', color: '#67C23A' },
        { title: '女性占比', value: '0%', desc: '统计周期内', icon: 'el-icon-female', color: '#F56C6C' },
        { title: '平均年龄', value: 0, desc: '统计周期内', icon: 'el-icon-s-data', color: '#E6A23C' }
      ],
      // å›¾è¡¨é…ç½®
      ageChartOptions: {},
      genderBloodChartOptions: {},
      categoryChartOptions: {},
      deathChartOptions: {},
      // è¡¨æ ¼æ•°æ®
      tableData: [],
      total: 0,
      // æ¨¡æ‹Ÿæ•°æ®
      mockData: {
        table: [
          { id: 1, donorNo: 'D2024001', name: '张三', gender: '男', age: 45, bloodType: 'A', region: '青岛地区', donationTime: '2024-01-15', chinaCategory: '中国一类(DBD)', deathCause: '脑外伤', organs: ['全肝', '左肾', '右肾'] },
          { id: 2, donorNo: 'D2024002', name: '李四', gender: '女', age: 32, bloodType: 'B', region: '青岛地区', donationTime: '2024-01-20', chinaCategory: '中国二类(DCD)', deathCause: '心搏骤停', organs: ['心脏', '眼角膜'] },
          { id: 3, donorNo: 'D2024003', name: '王五', gender: '男', age: 58, bloodType: 'O', region: '日照地区', donationTime: '2024-02-10', chinaCategory: '中国三类(DBCD)', deathCause: '脑血管意外', organs: ['全肝', '胰腺'] },
          { id: 4, donorNo: 'D2024004', name: '赵六', gender: '男', age: 28, bloodType: 'AB', region: '济南地区', donationTime: '2024-02-15', chinaCategory: '中国一类(DBD)', deathCause: '交通事故', organs: ['全肺', '左肾', '右肾'] },
          { id: 5, donorNo: 'D2024005', name: '钱七', gender: '女', age: 65, bloodType: 'A', region: '烟台地区', donationTime: '2024-02-20', chinaCategory: '中国二类(DCD)', deathCause: '心肌梗死', organs: ['心脏'] },
          { id: 6, donorNo: 'D2024006', name: '孙八', gender: '男', age: 42, bloodType: 'B', region: '威海地区', donationTime: '2024-03-05', chinaCategory: '中国一类(DBD)', deathCause: '脑肿瘤', organs: ['左肾', '右肾', '眼角膜'] },
          { id: 7, donorNo: 'D2024007', name: '周九', gender: '男', age: 37, bloodType: 'O', region: '潍坊地区', donationTime: '2024-03-10', chinaCategory: '中国三类(DBCD)', deathCause: '脑外伤', organs: ['肝脏', '胰腺', '眼角膜'] },
          { id: 8, donorNo: 'D2024008', name: '吴十', gender: '女', age: 25, bloodType: 'AB', region: '临沂地区', donationTime: '2024-03-15', chinaCategory: '中国二类(DCD)', deathCause: '交通事故', organs: ['心脏', '肺'] }
        ],
        age: [
          { ageRange: '<17岁', male: 5, female: 3 },
          { ageRange: '18-49岁', male: 45, female: 30 },
          { ageRange: '50-69岁', male: 35, female: 25 },
          { ageRange: '>69岁', male: 10, female: 8 }
        ],
        gender: [
          { name: '男性', value: 95 },
          { name: '女性', value: 66 }
        ],
        blood: [
          { name: 'A型', value: 45 },
          { name: 'B型', value: 38 },
          { name: 'O型', value: 52 },
          { name: 'AB型', value: 26 }
        ],
        category: [
          { name: '中国一类(DBD)', value: 85 },
          { name: '中国二类(DCD)', value: 55 },
          { name: '中国三类(DBCD)', value: 21 }
        ],
        death: [
          { name: '脑外伤', value: 45 },
          { name: '心搏骤停', value: 35 },
          { name: '脑血管意外', value: 35 },
          { name: '交通事故', value: 20 },
          { name: '心肌梗死', value: 25 },
          { name: '其他', value: 15 }
        ]
      }
    };
  },
  created() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await new Promise(resolve => setTimeout(resolve, 500));
        // å¤„理表格数据
        let filteredData = [...this.mockData.table];
        if (this.query.region.length > 0) {
          filteredData = filteredData.filter(item =>
            this.query.region.includes(item.region.replace('地区', '').toLowerCase())
          );
        }
        if (this.query.monthRange && this.query.monthRange.length === 2) {
          const [start, end] = this.query.monthRange;
          filteredData = filteredData.filter(item =>
            item.donationTime >= start && item.donationTime <= end
          );
        }
        if (this.query.ageRange.length > 0) {
          filteredData = filteredData.filter(item => {
            const age = item.age;
            return this.query.ageRange.some(range => {
              if (range === '0-17') return age < 18;
              if (range === '18-49') return age >= 18 && age <= 49;
              if (range === '50-69') return age >= 50 && age <= 69;
              if (range === '70-100') return age >= 70;
              return false;
            });
          });
        }
        if (this.query.gender) {
          filteredData = filteredData.filter(item => item.gender === this.query.gender);
        }
        if (this.query.bloodType) {
          filteredData = filteredData.filter(item => item.bloodType === this.query.bloodType);
        }
        // åˆ†é¡µ
        const start = (this.query.page - 1) * this.query.limit;
        this.tableData = filteredData.slice(start, start + this.query.limit);
        this.total = filteredData.length;
        // æ›´æ–°æ¦‚览数据
        this.updateOverviewData(filteredData);
        // æ›´æ–°å›¾è¡¨
        this.updateCharts();
      } catch (error) {
        console.error('加载数据失败:', error);
        this.$message.error('数据加载失败');
      } finally {
        this.loading = false;
      }
    },
    // æ›´æ–°æ¦‚览数据
    updateOverviewData(data) {
      if (data.length === 0) return;
      const total = data.length;
      const males = data.filter(item => item.gender === '男').length;
      const females = data.filter(item => item.gender === '女').length;
      const avgAge = data.reduce((sum, item) => sum + item.age, 0) / total;
      this.overviewData[0].value = total;
      this.overviewData[1].value = `${((males / total) * 100).toFixed(1)}%`;
      this.overviewData[2].value = `${((females / total) * 100).toFixed(1)}%`;
      this.overviewData[3].value = avgAge.toFixed(1);
    },
    // æ›´æ–°å›¾è¡¨
    updateCharts() {
      // å¹´é¾„分布图
      this.ageChartOptions = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        legend: {
          data: ['男性', '女性']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: [
          {
            type: 'value',
            position: 'bottom',
            axisLabel: {
              formatter: function(value) {
                return Math.abs(value);
              }
            }
          }
        ],
        yAxis: [
          {
            type: 'category',
            axisTick: {
              show: false
            },
            data: this.mockData.age.map(item => item.ageRange)
          }
        ],
        series: [
          {
            name: '男性',
            type: 'bar',
            stack: '性别',
            data: this.mockData.age.map(item => item.male),
            itemStyle: { color: '#409EFF' }
          },
          {
            name: '女性',
            type: 'bar',
            stack: '性别',
            data: this.mockData.age.map(item => -item.female),
            itemStyle: { color: '#F56C6C' }
          }
        ]
      };
      // æ€§åˆ«ä¸Žè¡€åž‹ç»„合图
      this.genderBloodChartOptions = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b} : {c} ({d}%)'
        },
        legend: {
          data: [
            '男性', '女性',
            'A型', 'B型', 'O型', 'AB型'
          ],
          top: 20
        },
        grid: {
          top: '20%',
          bottom: '20%',
          left: '10%',
          right: '10%'
        },
        series: [
          {
            name: '性别分布',
            type: 'pie',
            radius: [0, '30%'],
            center: ['25%', '50%'],
            label: {
              position: 'inner',
              fontSize: 12
            },
            data: this.mockData.gender
          },
          {
            name: '血型分布',
            type: 'pie',
            radius: ['40%', '55%'],
            center: ['25%', '50%'],
            data: this.mockData.blood
          },
          {
            name: '性别血型对比',
            type: 'pie',
            radius: [0, '30%'],
            center: ['75%', '50%'],
            label: {
              position: 'inner',
              fontSize: 12
            },
            data: [
              { name: '男-A', value: 20 },
              { name: '男-B', value: 15 },
              { name: '男-O', value: 25 },
              { name: '男-AB', value: 10 },
              { name: '女-A', value: 12 },
              { name: '女-B', value: 8 },
              { name: '女-O', value: 10 },
              { name: '女-AB', value: 5 }
            ]
          }
        ]
      };
      // ä¸­å›½åˆ†ç±»åˆ†å¸ƒå›¾
      this.categoryChartOptions = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b} : {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          right: 10,
          top: 'center',
          data: this.mockData.category.map(item => item.name)
        },
        series: [
          {
            name: '中国分类',
            type: 'pie',
            radius: ['30%', '70%'],
            center: ['40%', '50%'],
            roseType: 'radius',
            label: {
              show: true
            },
            emphasis: {
              label: {
                show: true
              }
            },
            data: this.mockData.category
          }
        ]
      };
      // æ­»äº¡åŽŸå› åˆ†æžå›¾
      this.deathChartOptions = {
        tooltip: {
          trigger: 'item',
          formatter: '{b}: {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          left: 10,
          top: 'center',
          data: this.mockData.death.map(item => item.name)
        },
        series: [
          {
            name: '死亡原因',
            type: 'pie',
            radius: ['30%', '60%'],
            center: ['60%', '50%'],
            avoidLabelOverlap: false,
            label: {
              show: true,
              formatter: '{b|{b}}\n{c|{c} ({d}%)}',
              rich: {
                b: {
                  fontSize: 12,
                  lineHeight: 20
                },
                c: {
                  fontSize: 10,
                  color: '#999'
                }
              }
            },
            emphasis: {
              label: {
                show: true,
                fontSize: '16',
                fontWeight: 'bold'
              }
            },
            data: this.mockData.death
          }
        ]
      };
    },
    // èŽ·å–è¡€åž‹æ ‡ç­¾ç±»åž‹
    getBloodTypeTag(type) {
      const map = { 'A': 'danger', 'B': 'primary', 'O': 'success', 'AB': 'warning' };
      return map[type] || 'info';
    },
    // å¤„理查询
    handleSearch(formData) {
      this.query = { ...this.query, ...formData, page: 1 };
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.query = {
        region: [],
        monthRange: [],
        ageRange: [],
        gender: null,
        bloodType: 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();
    },
    // å¤„理导出
    handleExport() {
      this.$message.info('导出功能开发中...');
    },
    // æŸ¥çœ‹æçŒ®è€…详情
    viewDonorDetail(row) {
      this.$router.push({
        path: '/donor/detail',
        query: { id: row.id }
      });
    }
  }
};
</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>
src/views/statistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<!-- src/views/statistics/index.vue -->
<template>
  <div class="statistics-home">
    <el-row :gutter="20">
      <el-col :span="6" v-for="item in menuList" :key="item.path">
        <el-card
          class="stat-card"
          shadow="hover"
          @click.native="goToPage(item.path)"
        >
          <div class="card-content">
            <div class="card-icon" :style="{ backgroundColor: item.color }">
              <i :class="item.icon"></i>
            </div>
            <div class="card-info">
              <h3>{{ item.title }}</h3>
              <p>{{ item.desc }}</p>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script>
export default {
  name: 'StatisticsHome',
  data() {
    return {
      menuList: [
        {
          title: '案例统计',
          desc: '地区、月份维度的案例统计分析',
          icon: 'el-icon-s-data',
          color: '#409EFF',
          path: '/statistics/case'
        },
        {
          title: '捐献器官统计',
          desc: '器官获取、移植数量统计',
          icon: 'el-icon-s-claim',
          color: '#67C23A',
          path: '/statistics/organ'
        },
        {
          title: '获取率统计',
          desc: '捐献获取率、器官弃用率分析',
          icon: 'el-icon-s-flag',
          color: '#E6A23C',
          path: '/statistics/rate'
        },
        {
          title: '捐献意愿统计',
          desc: '捐献意愿趋势与分布分析',
          icon: 'el-icon-star-on',
          color: '#F56C6C',
          path: '/statistics/willingness'
        },
        {
          title: '捐献者分析',
          desc: '多维度捐献者特征分析',
          icon: 'el-icon-user',
          color: '#909399',
          path: '/statistics/donor'
        },
        {
          title: '器官获取利用',
          desc: '器官获取利用效率分析',
          icon: 'el-icon-setting',
          color: '#9966CC',
          path: '/statistics/utilization'
        }
      ]
    };
  },
  methods: {
    goToPage(path) {
      this.$router.push(path);
    }
  }
};
</script>
<style scoped>
.statistics-home {
  padding: 20px;
}
.stat-card {
  cursor: pointer;
  margin-bottom: 20px;
  transition: all 0.3s;
}
.stat-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
}
.card-content {
  display: flex;
  align-items: center;
  padding: 10px 0;
}
.card-icon {
  width: 60px;
  height: 60px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 20px;
}
.card-icon i {
  font-size: 30px;
  color: white;
}
.card-info h3 {
  margin: 0 0 8px 0;
  color: #303133;
  font-size: 18px;
}
.card-info p {
  margin: 0;
  color: #909399;
  font-size: 13px;
  line-height: 1.5;
}
</style>
src/views/statistics/organ.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,519 @@
<!-- src/views/statistics/organ.vue -->
<template>
  <div class="statistics-page">
    <!-- ç­›é€‰é¢æ¿ -->
    <FilterPanel
      :fields="filterFields"
      :default-value="query"
      @search="handleSearch"
      @reset="handleReset"
    >
      <template #extra-actions>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </template>
    </FilterPanel>
    <!-- æ•°æ®æ¦‚览 -->
    <el-row :gutter="20" class="overview-row">
      <el-col :span="6" 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 }">
              <i :class="item.icon"></i>
            </div>
            <div class="overview-info">
              <div class="overview-title">{{ item.title }}</div>
              <div class="overview-value">{{ item.value }}</div>
              <div class="overview-desc">{{ item.desc }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>器官获取/移植趋势</span>
          </div>
          <EChartsWrapper
            :id="'organ-trend-chart'"
            :options="trendChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>器官类型分布</span>
          </div>
          <EChartsWrapper
            :id="'organ-distribution-chart'"
            :options="distributionChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
    </el-row>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <div slot="header" class="clearfix">
        <span>捐献器官统计明细</span>
        <el-button
          style="float: right; margin-left: 10px;"
          type="primary"
          icon="el-icon-zoom-in"
          @click="showOrganDetail = true"
        >
          æŸ¥çœ‹å™¨å®˜æ˜Žç»†
        </el-button>
      </div>
      <el-table
        v-loading="loading"
        :data="tableData"
        style="width: 100%"
        border
        stripe
      >
        <el-table-column label="年份" prop="year" align="center"  />
        <el-table-column label="器官名称" prop="organName" align="center"  />
        <el-table-column label="获取数量" prop="acquisitionCount" align="center"  />
        <el-table-column label="移植数量" prop="transplantCount" align="center"  />
        <el-table-column label="移植率" prop="transplantRate" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="getRateTag(row.transplantRate)" size="small">
              {{ row.transplantRate }}%
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="弃用数量" prop="abandonCount" align="center"  />
        <el-table-column label="地区" prop="region" align="center"  />
        <el-table-column label="操作" align="center"  fixed="right">
          <template slot-scope="{ row }">
            <el-button
              type="text"
              size="mini"
              @click="viewOrganDetail(row)"
            >
              è¯¦æƒ…
            </el-button>
          </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>
    <!-- å™¨å®˜æ˜Žç»†å¼¹çª— -->
    <el-dialog
      title="器官明细"
      :visible.sync="showOrganDetail"
      width="80%"
      append-to-body
    >
      <OrganDetail
        :query="detailQuery"
        @close="showOrganDetail = false"
      />
    </el-dialog>
  </div>
</template>
<script>
import FilterPanel from '@/components/charts/FilterPanel.vue';
import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
import OrganDetail from './components/OrganDetail.vue';
export default {
  name: 'OrganStatistics',
  components: {
    FilterPanel,
    EChartsWrapper,
    OrganDetail
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      query: {
        region: [],
        yearRange: [],
        organType: null,
        page: 1,
        limit: 10
      },
      // ç­›é€‰å­—段配置
      filterFields: [
        {
          label: '地区',
          prop: 'region',
          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' }
          ],
          width: '300px'
        },
        {
          label: '年份范围',
          prop: 'yearRange',
          type: 'daterange',
          dateType: 'yearrange',
          startPlaceholder: '开始年份',
          endPlaceholder: '结束年份',
          valueFormat: 'yyyy',
          width: '300px'
        },
        {
          label: '器官类型',
          prop: 'organType',
          type: 'select',
          options: [
            { label: '全肝', value: 'liver' },
            { label: '肾脏', value: 'kidney' },
            { label: '心脏', value: 'heart' },
            { label: '肺', value: 'lung' },
            { label: '胰腺', value: 'pancreas' },
            { label: '眼角膜', value: 'cornea' }
          ],
          width: '200px'
        }
      ],
      // åŠ è½½çŠ¶æ€
      loading: false,
      // è¡¨æ ¼æ•°æ®
      tableData: [],
      total: 0,
      // æ¦‚览数据
      overviewData: [
        { title: '总获取数量', value: 0, desc: '统计周期内', icon: 'el-icon-s-claim', color: '#409EFF' },
        { title: '总移植数量', value: 0, desc: '统计周期内', icon: 'el-icon-success', color: '#67C23A' },
        { title: '总移植率', value: '0%', desc: '统计周期内', icon: 'el-icon-s-data', color: '#E6A23C' },
        { title: '总弃用数量', value: 0, desc: '统计周期内', icon: 'el-icon-warning', color: '#F56C6C' }
      ],
      // å›¾è¡¨é…ç½®
      trendChartOptions: {},
      distributionChartOptions: {},
      // å¼¹çª—控制
      showOrganDetail: false,
      detailQuery: {},
      // æ¨¡æ‹Ÿæ•°æ®
      mockData: {
        table: [
          { id: 1, year: '2024', organName: '全肝', acquisitionCount: 15, transplantCount: 12, transplantRate: 80.0, abandonCount: 3, region: '青岛地区' },
          { id: 2, year: '2024', organName: '肾脏', acquisitionCount: 20, transplantCount: 18, transplantRate: 90.0, abandonCount: 2, region: '青岛地区' },
          { id: 3, year: '2024', organName: '全肝', acquisitionCount: 8, transplantCount: 6, transplantRate: 75.0, abandonCount: 2, region: '日照地区' },
          { id: 4, year: '2024', organName: '心脏', acquisitionCount: 5, transplantCount: 4, transplantRate: 80.0, abandonCount: 1, region: '青岛地区' },
          { id: 5, year: '2023', organName: '全肝', acquisitionCount: 12, transplantCount: 10, transplantRate: 83.3, abandonCount: 2, region: '青岛地区' },
          { id: 6, year: '2023', organName: '眼角膜', acquisitionCount: 25, transplantCount: 22, transplantRate: 88.0, abandonCount: 3, region: '济南地区' }
        ],
        trend: [
          { year: '2020', acquisition: 120, transplant: 100 },
          { year: '2021', acquisition: 150, transplant: 130 },
          { year: '2022', acquisition: 180, transplant: 160 },
          { year: '2023', acquisition: 220, transplant: 200 },
          { year: '2024', acquisition: 250, transplant: 230 }
        ],
        distribution: [
          { name: '全肝', value: 85 },
          { name: '肾脏', value: 120 },
          { name: '心脏', value: 45 },
          { name: '肺', value: 60 },
          { name: '胰腺', value: 30 },
          { name: '眼角膜', value: 200 }
        ]
      }
    };
  },
  created() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await new Promise(resolve => setTimeout(resolve, 500));
        // å¤„理表格数据
        let filteredData = [...this.mockData.table];
        if (this.query.region.length > 0) {
          filteredData = filteredData.filter(item =>
            this.query.region.includes(item.region.replace('地区', '').toLowerCase())
          );
        }
        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)
          );
        }
        if (this.query.organType) {
          const organMap = { liver: '肝', kidney: '肾', heart: '心脏', lung: '肺', pancreas: '胰腺', cornea: '眼角膜' };
          const organName = organMap[this.query.organType];
          if (organName) {
            filteredData = filteredData.filter(item => item.organName.includes(organName));
          }
        }
        // åˆ†é¡µ
        const start = (this.query.page - 1) * this.query.limit;
        this.tableData = filteredData.slice(start, start + this.query.limit);
        this.total = filteredData.length;
        // æ›´æ–°æ¦‚览数据
        this.updateOverviewData(filteredData);
        // æ›´æ–°å›¾è¡¨
        this.updateCharts();
      } catch (error) {
        console.error('加载数据失败:', error);
        this.$message.error('数据加载失败');
      } finally {
        this.loading = false;
      }
    },
    // æ›´æ–°æ¦‚览数据
    updateOverviewData(data) {
      if (data.length === 0) return;
      const totalAcquisition = data.reduce((sum, item) => sum + item.acquisitionCount, 0);
      const totalTransplant = data.reduce((sum, item) => sum + item.transplantCount, 0);
      const totalAbandon = data.reduce((sum, item) => sum + item.abandonCount, 0);
      const transplantRate = totalAcquisition > 0 ? (totalTransplant / totalAcquisition * 100).toFixed(1) : 0;
      this.overviewData[0].value = totalAcquisition;
      this.overviewData[1].value = totalTransplant;
      this.overviewData[2].value = `${transplantRate}%`;
      this.overviewData[3].value = totalAbandon;
    },
    // æ›´æ–°å›¾è¡¨
    updateCharts() {
      // è¶‹åŠ¿å›¾
      this.trendChartOptions = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        legend: {
          data: ['获取数量', '移植数量']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: this.mockData.trend.map(item => item.year + 'å¹´')
        },
        yAxis: {
          type: 'value',
          name: '数量(个)'
        },
        series: [
          {
            name: '获取数量',
            type: 'bar',
            data: this.mockData.trend.map(item => item.acquisition),
            itemStyle: { color: '#409EFF' }
          },
          {
            name: '移植数量',
            type: 'bar',
            data: this.mockData.trend.map(item => item.transplant),
            itemStyle: { color: '#67C23A' }
          }
        ]
      };
      // åˆ†å¸ƒå›¾
      this.distributionChartOptions = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b} : {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          left: 'left',
          data: this.mockData.distribution.map(item => item.name)
        },
        series: [
          {
            name: '器官分布',
            type: 'pie',
            radius: '55%',
            center: ['50%', '60%'],
            data: this.mockData.distribution,
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.5)'
              }
            }
          }
        ]
      };
    },
    // èŽ·å–ç§»æ¤çŽ‡æ ‡ç­¾ç±»åž‹
    getRateTag(rate) {
      if (rate >= 85) return 'success';
      if (rate >= 75) return 'warning';
      return 'danger';
    },
    // å¤„理查询
    handleSearch(formData) {
      this.query = { ...this.query, ...formData, page: 1 };
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.query = {
        region: [],
        yearRange: [],
        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();
    },
    // å¤„理导出
    handleExport() {
      this.$message.info('导出功能开发中...');
    },
    // æŸ¥çœ‹å™¨å®˜è¯¦æƒ…
    viewOrganDetail(row) {
      this.detailQuery = {
        year: row.year,
        organType: row.organName,
        region: row.region
      };
      this.showOrganDetail = true;
    }
  }
};
</script>
<style scoped>
.statistics-page {
  padding: 20px;
}
.filter-card {
  margin-bottom: 20px;
}
.filter-form {
  margin-bottom: 0;
}
.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>
src/views/statistics/rate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,506 @@
<!-- src/views/statistics/rate.vue -->
<template>
  <div class="statistics-page">
    <!-- ç­›é€‰é¢æ¿ -->
    <FilterPanel
      :fields="filterFields"
      :default-value="query"
      @search="handleSearch"
      @reset="handleReset"
    >
      <template #extra-actions>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </template>
    </FilterPanel>
    <!-- æ ¸å¿ƒæŒ‡æ ‡ä»ªè¡¨ç›˜ -->
    <el-row :gutter="20" class="dashboard-row">
      <el-col :span="8" v-for="item in dashboardData" :key="item.title">
        <el-card class="dashboard-card">
          <div class="dashboard-title">{{ item.title }}</div>
          <div class="dashboard-value">{{ item.value }}</div>
          <div class="dashboard-chart">
            <EChartsWrapper
              :id="`dashboard-${item.key}`"
              :options="getDashboardOptions(item)"
              height="150px"
            />
          </div>
          <div class="dashboard-trend">
            <span :class="item.trendClass">
              <i :class="item.trendIcon"></i> {{ item.trendText }}
            </span>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>获取率趋势分析</span>
          </div>
          <EChartsWrapper
            :id="'rate-trend-chart'"
            :options="trendChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>弃用率构成分析</span>
          </div>
          <EChartsWrapper
            :id="'abandon-chart'"
            :options="abandonChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
    </el-row>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <div slot="header" class="clearfix">
        <span>获取率统计明细</span>
      </div>
      <el-table
        v-loading="loading"
        :data="tableData"
        style="width: 100%"
        border
        stripe
      >
        <el-table-column label="年份" prop="year" align="center"  />
        <el-table-column label="地区" prop="region" align="center"  />
        <el-table-column label="捐献获取率" prop="acquisitionRate" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="getRateTag(row.acquisitionRate, 'acquisition')" size="small">
              {{ row.acquisitionRate }}%
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="平均器官数" prop="avgOrgans" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="getRateTag(row.avgOrgans, 'avg')" size="small">
              {{ row.avgOrgans }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="器官弃用率" prop="abandonRate" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="getRateTag(row.abandonRate, 'abandon')" size="small">
              {{ row.abandonRate }}%
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="捐献总数" prop="totalDonation" align="center"  />
        <el-table-column label="器官总数" prop="totalOrgans" align="center"  />
        <el-table-column label="移植总数" prop="totalTransplants" align="center"  />
      </el-table>
    </el-card>
  </div>
</template>
<script>
import FilterPanel from '@/components/charts/FilterPanel.vue';
import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
export default {
  name: 'RateStatistics',
  components: {
    FilterPanel,
    EChartsWrapper
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      query: {
        region: [],
        yearRange: [],
        page: 1,
        limit: 10
      },
      // ç­›é€‰å­—段配置
      filterFields: [
        {
          label: '地区',
          prop: 'region',
          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' }
          ],
          width: '300px'
        },
        {
          label: '年份范围',
          prop: 'yearRange',
          type: 'daterange',
          dateType: 'yearrange',
          startPlaceholder: '开始年份',
          endPlaceholder: '结束年份',
          valueFormat: 'yyyy',
          width: '300px'
        }
      ],
      // åŠ è½½çŠ¶æ€
      loading: false,
      // ä»ªè¡¨ç›˜æ•°æ®
      dashboardData: [
        {
          key: 'acquisition',
          title: '捐献获取率',
          value: '85.5%',
          trendIcon: 'el-icon-top',
          trendText: '↑2.7%',
          trendClass: 'up'
        },
        {
          key: 'avg',
          title: '平均器官数',
          value: '3.2',
          trendIcon: 'el-icon-top',
          trendText: '↑0.2',
          trendClass: 'up'
        },
        {
          key: 'abandon',
          title: '器官弃用率',
          value: '12.3%',
          trendIcon: 'el-icon-bottom',
          trendText: '↓2.7%',
          trendClass: 'down'
        }
      ],
      // å›¾è¡¨é…ç½®
      trendChartOptions: {},
      abandonChartOptions: {},
      // è¡¨æ ¼æ•°æ®
      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 }
        ]
      }
    };
  },
  created() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await new Promise(resolve => setTimeout(resolve, 500));
        // å¤„理表格数据
        let filteredData = [...this.mockData.table];
        if (this.query.region.length > 0) {
          filteredData = filteredData.filter(item =>
            this.query.region.includes(item.region.replace('地区', '').toLowerCase())
          );
        }
        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)
          );
        }
        this.tableData = filteredData;
        this.total = filteredData.length;
        // æ›´æ–°å›¾è¡¨
        this.updateCharts();
      } catch (error) {
        console.error('加载数据失败:', error);
        this.$message.error('数据加载失败');
      } finally {
        this.loading = false;
      }
    },
    // æ›´æ–°å›¾è¡¨
    updateCharts() {
      // è¶‹åŠ¿å›¾
      this.trendChartOptions = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross',
            label: {
              backgroundColor: '#6a7985'
            }
          }
        },
        legend: {
          data: ['捐献获取率', '平均器官数', '器官弃用率']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          boundaryGap: false,
          data: this.mockData.trend.map(item => item.year + 'å¹´')
        },
        yAxis: {
          type: 'value',
          axisLabel: {
            formatter: '{value}'
          }
        },
        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
          }
        ]
      };
      // å¼ƒç”¨çŽ‡æž„æˆå›¾
      this.abandonChartOptions = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b} : {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          left: 'left',
          data: this.mockData.abandon.map(item => item.name)
        },
        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
          }
        ]
      };
    },
    // èŽ·å–ä»ªè¡¨ç›˜å›¾è¡¨é…ç½®
    getDashboardOptions(item) {
      const value = parseFloat(item.value);
      const max = item.key === 'avg' ? 5 : 100;
      const data = [{ value, name: item.title }];
      return {
        tooltip: {
          formatter: '{b}: {c}' + (item.key === 'abandon' ? '%' : item.key === 'acquisition' ? '%' : '')
        },
        series: [
          {
            name: item.title,
            type: 'gauge',
            progress: {
              show: true,
              width: 18
            },
            axisLine: {
              lineStyle: {
                width: 18
              }
            },
            axisTick: {
              show: false
            },
            splitLine: {
              length: 15,
              lineStyle: {
                width: 2,
                color: '#999'
              }
            },
            axisLabel: {
              distance: 25,
              color: '#999',
              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
          }
        ]
      };
    },
    // èŽ·å–æ ‡ç­¾ç±»åž‹
    getRateTag(value, type) {
      if (type === 'acquisition') {
        if (value >= 85) return 'success';
        if (value >= 80) return 'warning';
        return 'danger';
      } else if (type === 'avg') {
        if (value >= 3.2) return 'success';
        if (value >= 2.8) return 'warning';
        return 'danger';
      } else if (type === 'abandon') {
        if (value <= 10) return 'success';
        if (value <= 15) return 'warning';
        return 'danger';
      }
      return '';
    },
    // å¤„理查询
    handleSearch(formData) {
      this.query = { ...this.query, ...formData, page: 1 };
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.query = {
        region: [],
        yearRange: [],
        page: 1,
        limit: 10
      };
      this.loadData();
    },
    // å¤„理导出
    handleExport() {
      this.$message.info('导出功能开发中...');
    }
  }
};
</script>
<style scoped>
.dashboard-row {
  margin-bottom: 20px;
}
.dashboard-card {
  text-align: center;
  padding: 20px 0;
}
.dashboard-title {
  font-size: 16px;
  color: #606266;
  margin-bottom: 10px;
}
.dashboard-value {
  font-size: 28px;
  font-weight: bold;
  color: #303133;
  margin: 10px 0;
}
.dashboard-chart {
  height: 150px;
  margin: 20px 0;
}
.dashboard-trend {
  font-size: 14px;
  margin-top: 10px;
}
.dashboard-trend .up {
  color: #67C23A;
}
.dashboard-trend .down {
  color: #F56C6C;
}
</style>
src/views/statistics/utilization.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,688 @@
<!-- src/views/statistics/utilization.vue -->
<template>
  <div class="statistics-page">
    <!-- ç­›é€‰é¢æ¿ -->
    <FilterPanel
      :fields="filterFields"
      :default-value="query"
      @search="handleSearch"
      @reset="handleReset"
    >
      <template #extra-actions>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </template>
    </FilterPanel>
    <!-- ç¬¬ä¸€éƒ¨åˆ†ï¼šå™¨å®˜èŽ·å–/移植统计 -->
    <el-card style="margin-bottom: 20px;">
      <div slot="header" class="clearfix">
        <h3>1. å™¨å®˜èŽ·å–/移植数量统计</h3>
      </div>
      <EChartsWrapper
        :id="'acquisition-transplant-chart'"
        :options="acquisitionChartOptions"
        height="500px"
      />
    </el-card>
    <!-- ç¬¬äºŒéƒ¨åˆ†ï¼šå™¨å®˜å¼ƒç”¨åŽŸå› åˆ†æž -->
    <el-card style="margin-bottom: 20px;">
      <div slot="header" class="clearfix">
        <h3>2. å™¨å®˜å¼ƒç”¨åŽŸå› åˆ†æž</h3>
        <el-form :inline="true" style="float: right;">
          <el-form-item label="器官类型" style="margin-bottom: 0;">
            <el-select
              v-model="abandonQuery.organType"
              placeholder="请选择器官类型"
              clearable
              size="mini"
              style="width: 150px"
              @change="handleAbandonQuery"
            >
              <el-option
                v-for="item in organOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-form>
      </div>
      <EChartsWrapper
        :id="'abandon-analysis-chart'"
        :options="abandonChartOptions"
        height="400px"
      />
    </el-card>
    <!-- ç¬¬ä¸‰éƒ¨åˆ†ï¼šæ¯ä¾›ä½“器官获取数量 -->
    <el-card>
      <div slot="header" class="clearfix">
        <h3>3. æ¯ä¾›ä½“器官获取数量</h3>
        <el-form :inline="true" style="float: right;">
          <el-form-item label="年份" style="margin-bottom: 0;">
            <el-date-picker
              v-model="perDonorQuery.year"
              type="year"
              placeholder="选择年份"
              value-format="yyyy"
              size="mini"
              style="width: 120px"
              @change="handlePerDonorQuery"
            />
          </el-form-item>
          <el-form-item label="器官类型" style="margin-bottom: 0;">
            <el-select
              v-model="perDonorQuery.organType"
              placeholder="请选择器官类型"
              clearable
              size="mini"
              style="width: 150px"
              @change="handlePerDonorQuery"
            >
              <el-option
                v-for="item in organOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-form>
      </div>
      <el-table
        v-loading="loading"
        :data="tableData"
        style="width: 100%"
        border
        stripe
      >
        <el-table-column label="年份" prop="year" align="center"  />
        <el-table-column label="供体姓名" prop="donorName" align="center"  />
        <el-table-column label="获取数量" prop="acquisitionCount" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="getCountTag(row.acquisitionCount)" size="small">
              {{ row.acquisitionCount }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="全肝" prop="liver" align="center"  />
        <el-table-column label="左半肝" prop="leftLiver" align="center"  />
        <el-table-column label="右半肝" prop="rightLiver" align="center"  />
        <el-table-column label="心脏" prop="heart" align="center"  />
        <el-table-column label="小肠" prop="intestine" align="center"  />
        <el-table-column label="胰腺" prop="pancreas" align="center"  />
        <el-table-column label="左肾" prop="leftKidney" align="center"  />
        <el-table-column label="右肾" prop="rightKidney" align="center"  />
        <el-table-column label="全肺" prop="fullLung" align="center"  />
        <el-table-column label="左肺" prop="leftLung" align="center"  />
        <el-table-column label="右肺" prop="rightLung" align="center"  />
        <el-table-column label="左眼角膜" prop="leftCornea" align="center"  />
        <el-table-column label="右眼角膜" prop="rightCornea" align="center"  />
        <el-table-column label="地区" prop="region" align="center"  />
        <el-table-column label="操作" align="center"  fixed="right">
          <template slot-scope="{ row }">
            <el-button
              type="text"
              size="mini"
              @click="viewDonorDetail(row)"
            >
              è¯¦æƒ…
            </el-button>
          </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>
<script>
import FilterPanel from '@/components/charts/FilterPanel.vue';
import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
export default {
  name: 'OrganUtilization',
  components: {
    FilterPanel,
    EChartsWrapper
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      query: {
        region: [],
        monthRange: [],
        ageRange: [],
        chinaCategory: [],
        organType: null,
        page: 1,
        limit: 10
      },
      // ç­›é€‰å­—段配置
      filterFields: [
        {
          label: '地区',
          prop: 'region',
          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' }
          ],
          width: '300px'
        },
        {
          label: '月份范围',
          prop: 'monthRange',
          type: 'daterange',
          dateType: 'monthrange',
          startPlaceholder: '开始月份',
          endPlaceholder: '结束月份',
          valueFormat: 'yyyy-MM',
          width: '300px'
        },
        {
          label: '年龄段',
          prop: 'ageRange',
          type: 'select',
          multiple: true,
          options: [
            { label: '<17岁', value: '0-17' },
            { label: '18-49岁', value: '18-49' },
            { label: '50-69岁', value: '50-69' },
            { label: '>69岁', value: '70-100' }
          ],
          width: '200px'
        },
        {
          label: '中国分类',
          prop: 'chinaCategory',
          type: 'select',
          multiple: true,
          options: [
            { label: '中国一类(DBD)', value: '1' },
            { label: '中国二类(DCD)', value: '2' },
            { label: '中国三类(DBCD)', value: '3' }
          ],
          width: '200px'
        },
        {
          label: '器官类型',
          prop: 'organType',
          type: 'select',
          options: [
            { label: '全肝', value: 'liver' },
            { label: '肾脏', value: 'kidney' },
            { label: '心脏', value: 'heart' },
            { label: '肺', value: 'lung' },
            { label: '眼角膜', value: 'cornea' }
          ],
          width: '200px'
        }
      ],
      // å™¨å®˜é€‰é¡¹
      organOptions: [
        { label: '全肝', value: 'liver' },
        { label: '左半肝', value: 'leftLiver' },
        { label: '右半肝', value: 'rightLiver' },
        { label: '心脏', value: 'heart' },
        { label: '小肠', value: 'intestine' },
        { label: '胰腺', value: 'pancreas' },
        { label: '左肾', value: 'leftKidney' },
        { label: '右肾', value: 'rightKidney' },
        { label: '全肺', value: 'fullLung' },
        { label: '左肺', value: 'leftLung' },
        { label: '右肺', value: 'rightLung' },
        { label: '左眼角膜', value: 'leftCornea' },
        { label: '右眼角膜', value: 'rightCornea' }
      ],
      // åŠ è½½çŠ¶æ€
      loading: false,
      // å›¾è¡¨é…ç½®
      acquisitionChartOptions: {},
      abandonChartOptions: {},
      // è¡¨æ ¼æ•°æ®
      tableData: [],
      total: 0,
      // å¼ƒç”¨åˆ†æžæŸ¥è¯¢å‚æ•°
      abandonQuery: {
        organType: null
      },
      // æ¯ä¾›ä½“查询参数
      perDonorQuery: {
        year: '2024',
        organType: null
      },
      // æ¨¡æ‹Ÿæ•°æ®
      mockData: {
        acquisition: [
          {
            month: '2024-01',
            region: '青岛地区',
            liver: { acquisition: 8, transplant: 7 },
            kidney: { acquisition: 12, transplant: 10 },
            heart: { acquisition: 3, transplant: 3 },
            lung: { acquisition: 5, transplant: 4 },
            cornea: { acquisition: 20, transplant: 18 }
          },
          {
            month: '2024-02',
            region: '青岛地区',
            liver: { acquisition: 10, transplant: 9 },
            kidney: { acquisition: 15, transplant: 13 },
            heart: { acquisition: 4, transplant: 3 },
            lung: { acquisition: 6, transplant: 5 },
            cornea: { acquisition: 25, transplant: 22 }
          },
          {
            month: '2024-01',
            region: '日照地区',
            liver: { acquisition: 5, transplant: 4 },
            kidney: { acquisition: 8, transplant: 7 },
            heart: { acquisition: 2, transplant: 2 },
            lung: { acquisition: 3, transplant: 2 },
            cornea: { acquisition: 15, transplant: 13 }
          }
        ],
        abandon: [
          { name: '器官质量不合格', value: 45 },
          { name: '供体疾病', value: 25 },
          { name: '运输问题', value: 15 },
          { name: '受体匹配问题', value: 10 },
          { name: '其他原因', value: 5 }
        ],
        table: [
          {
            id: 1,
            year: '2024',
            donorName: '张三',
            acquisitionCount: 5,
            liver: 1,
            leftLiver: 0,
            rightLiver: 0,
            heart: 0,
            intestine: 0,
            pancreas: 0,
            leftKidney: 1,
            rightKidney: 1,
            fullLung: 0,
            leftLung: 0,
            rightLung: 0,
            leftCornea: 1,
            rightCornea: 1,
            region: '青岛地区'
          },
          {
            id: 2,
            year: '2024',
            donorName: '李四',
            acquisitionCount: 3,
            liver: 0,
            leftLiver: 0,
            rightLiver: 0,
            heart: 1,
            intestine: 0,
            pancreas: 0,
            leftKidney: 0,
            rightKidney: 0,
            fullLung: 0,
            leftLung: 1,
            rightLung: 1,
            leftCornea: 0,
            rightCornea: 0,
            region: '青岛地区'
          },
          {
            id: 3,
            year: '2024',
            donorName: '王五',
            acquisitionCount: 6,
            liver: 1,
            leftLiver: 0,
            rightLiver: 0,
            heart: 0,
            intestine: 0,
            pancreas: 1,
            leftKidney: 1,
            rightKidney: 1,
            fullLung: 0,
            leftLung: 0,
            rightLung: 0,
            leftCornea: 1,
            rightCornea: 1,
            region: '日照地区'
          },
          {
            id: 4,
            year: '2024',
            donorName: '赵六',
            acquisitionCount: 4,
            liver: 0,
            leftLiver: 0,
            rightLiver: 0,
            heart: 0,
            intestine: 0,
            pancreas: 0,
            leftKidney: 1,
            rightKidney: 1,
            fullLung: 0,
            leftLung: 0,
            rightLung: 0,
            leftCornea: 1,
            rightCornea: 1,
            region: '济南地区'
          },
          {
            id: 5,
            year: '2023',
            donorName: '钱七',
            acquisitionCount: 3,
            liver: 0,
            leftLiver: 0,
            rightLiver: 0,
            heart: 1,
            intestine: 0,
            pancreas: 0,
            leftKidney: 0,
            rightKidney: 0,
            fullLung: 1,
            leftLung: 0,
            rightLung: 1,
            leftCornea: 0,
            rightCornea: 0,
            region: '烟台地区'
          }
        ]
      }
    };
  },
  created() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await new Promise(resolve => setTimeout(resolve, 500));
        // å¤„理表格数据
        let filteredData = [...this.mockData.table];
        if (this.query.region.length > 0) {
          filteredData = filteredData.filter(item =>
            this.query.region.includes(item.region.replace('地区', '').toLowerCase())
          );
        }
        if (this.query.monthRange && this.query.monthRange.length === 2) {
          // è¿™é‡Œå¯ä»¥æ·»åŠ æœˆä»½ç­›é€‰é€»è¾‘
        }
        if (this.query.ageRange.length > 0) {
          // è¿™é‡Œå¯ä»¥æ·»åŠ å¹´é¾„ç­›é€‰é€»è¾‘
        }
        if (this.query.chinaCategory.length > 0) {
          // è¿™é‡Œå¯ä»¥æ·»åŠ ä¸­å›½åˆ†ç±»ç­›é€‰é€»è¾‘
        }
        if (this.query.organType) {
          const organMap = { liver: 'liver', kidney: ['leftKidney', 'rightKidney'], heart: 'heart', lung: ['fullLung', 'leftLung', 'rightLung'], cornea: ['leftCornea', 'rightCornea'] };
          const organProp = organMap[this.query.organType];
          if (organProp) {
            if (Array.isArray(organProp)) {
              filteredData = filteredData.filter(item =>
                organProp.some(prop => item[prop] > 0)
              );
            } else {
              filteredData = filteredData.filter(item => item[organProp] > 0);
            }
          }
        }
        // åˆ†é¡µ
        const start = (this.query.page - 1) * this.query.limit;
        this.tableData = filteredData.slice(start, start + this.query.limit);
        this.total = filteredData.length;
        // æ›´æ–°å›¾è¡¨
        this.updateCharts();
      } catch (error) {
        console.error('加载数据失败:', error);
        this.$message.error('数据加载失败');
      } finally {
        this.loading = false;
      }
    },
    // æ›´æ–°å›¾è¡¨
    updateCharts() {
      // èŽ·å–ç§»æ¤ç»Ÿè®¡å›¾
      const regions = [...new Set(this.mockData.acquisition.map(item => item.region))];
      const months = [...new Set(this.mockData.acquisition.map(item => item.month))];
      const organTypes = ['liver', 'kidney', 'heart', 'lung', 'cornea'];
      const organNames = ['肝脏', '肾脏', '心脏', '肺', '眼角膜'];
      const seriesData = [];
      const xAxisData = [];
      // ç”Ÿæˆx轴数据
      months.forEach(month => {
        regions.forEach(region => {
          xAxisData.push(`${month}\n${region}`);
        });
      });
      // ç”Ÿæˆç³»åˆ—数据
      organTypes.forEach((organType, index) => {
        const data = [];
        months.forEach(month => {
          regions.forEach(region => {
            const item = this.mockData.acquisition.find(d => d.month === month && d.region === region);
            if (item) {
              data.push(item[organType]?.acquisition || 0);
            } else {
              data.push(0);
            }
          });
        });
        seriesData.push({
          name: `${organNames[index]}-获取`,
          type: 'bar',
          stack: '总量',
          data: data,
          itemStyle: this.getSeriesColor(index * 2)
        });
        const transplantData = [];
        months.forEach(month => {
          regions.forEach(region => {
            const item = this.mockData.acquisition.find(d => d.month === month && d.region === region);
            if (item) {
              transplantData.push(item[organType]?.transplant || 0);
            } else {
              transplantData.push(0);
            }
          });
        });
        seriesData.push({
          name: `${organNames[index]}-移植`,
          type: 'bar',
          stack: '总量',
          data: transplantData,
          itemStyle: this.getSeriesColor(index * 2 + 1)
        });
      });
      this.acquisitionChartOptions = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        legend: {
          data: seriesData.map(item => item.name),
          top: 20
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: xAxisData,
          axisLabel: {
            interval: 0,
            rotate: 45
          }
        },
        yAxis: {
          type: 'value',
          name: '数量(个)'
        },
        series: seriesData
      };
      // å¼ƒç”¨åˆ†æžå›¾
      this.abandonChartOptions = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b} : {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          left: 'left',
          data: this.mockData.abandon.map(item => item.name)
        },
        series: [
          {
            name: '弃用原因',
            type: 'pie',
            radius: '55%',
            center: ['50%', '60%'],
            data: this.mockData.abandon,
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.5)'
              }
            }
          }
        ]
      };
    },
    // èŽ·å–ç³»åˆ—é¢œè‰²
    getSeriesColor(index) {
      const colors = [
        '#409EFF', '#8cc5ff',  // è‚è„
        '#67C23A', '#95d475',  // è‚¾è„
        '#E6A23C', '#f3c96b',  // å¿ƒè„
        '#F56C6C', '#f89898',  // è‚º
        '#909399', '#b1b3b8'   // çœ¼è§’膜
      ];
      return { color: colors[index % colors.length] };
    },
    // èŽ·å–æ•°é‡æ ‡ç­¾
    getCountTag(count) {
      if (count >= 5) return 'success';
      if (count >= 3) return 'warning';
      return 'danger';
    },
    // å¤„理查询
    handleSearch(formData) {
      this.query = { ...this.query, ...formData, page: 1 };
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.query = {
        region: [],
        monthRange: [],
        ageRange: [],
        chinaCategory: [],
        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();
    },
    // å¤„理弃用查询
    handleAbandonQuery() {
      // è¿™é‡Œå¯ä»¥æ ¹æ®abandonQuery筛选数据
      console.log('弃用查询:', this.abandonQuery);
    },
    // å¤„理每供体查询
    handlePerDonorQuery() {
      this.loadData();
    },
    // å¤„理导出
    handleExport() {
      this.$message.info('导出功能开发中...');
    },
    // æŸ¥çœ‹æçŒ®è€…详情
    viewDonorDetail(row) {
      this.$router.push({
        path: '/donor/detail',
        query: { id: row.id }
      });
    }
  }
};
</script>
src/views/statistics/willingness.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,474 @@
<!-- src/views/statistics/willingness.vue -->
<template>
  <div class="statistics-page">
    <!-- ç­›é€‰é¢æ¿ -->
    <FilterPanel
      :fields="filterFields"
      :default-value="query"
      @search="handleSearch"
      @reset="handleReset"
    >
      <template #extra-actions>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </template>
    </FilterPanel>
    <!-- æ•°æ®æ¦‚览 -->
    <el-row :gutter="20" class="overview-row">
      <el-col :span="6" 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 }">
              <i :class="item.icon"></i>
            </div>
            <div class="overview-info">
              <div class="overview-title">{{ item.title }}</div>
              <div class="overview-value">{{ item.value }}</div>
              <div class="overview-desc">{{ item.desc }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>捐献意愿趋势</span>
          </div>
          <EChartsWrapper
            :id="'willingness-trend-chart'"
            :options="trendChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card>
          <div slot="header" class="clearfix">
            <span>地区捐献意愿对比</span>
          </div>
          <EChartsWrapper
            :id="'region-compare-chart'"
            :options="regionChartOptions"
            height="400px"
          />
        </el-card>
      </el-col>
    </el-row>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-card>
      <div slot="header" class="clearfix">
        <span>捐献意愿统计明细</span>
      </div>
      <el-table
        v-loading="loading"
        :data="tableData"
        style="width: 100%"
        border
        stripe
      >
        <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="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" >
          <template slot-scope="{ row }">
            <el-progress
              :percentage="parseFloat(row.willingnessRate)"
              :color="getProgressColor(parseFloat(row.willingnessRate))"
            />
          </template>
        </el-table-column>
        <el-table-column label="环比变化" prop="monthCompare" align="center" >
          <template slot-scope="{ row }">
            <el-tag :type="getCompareTag(row.monthCompare)" size="small">
              {{ row.monthCompare > 0 ? '+' : '' }}{{ row.monthCompare }}%
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>
<script>
import FilterPanel from '@/components/charts/FilterPanel.vue';
import EChartsWrapper from '@/components/charts/EChartsWrapper.vue';
export default {
  name: 'WillingnessStatistics',
  components: {
    FilterPanel,
    EChartsWrapper
  },
  data() {
    return {
      // æŸ¥è¯¢å‚æ•°
      query: {
        region: [],
        monthRange: [],
        organType: null,
        page: 1,
        limit: 10
      },
      // ç­›é€‰å­—段配置
      filterFields: [
        {
          label: '地区',
          prop: 'region',
          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' }
          ],
          width: '300px'
        },
        {
          label: '月份范围',
          prop: 'monthRange',
          type: 'daterange',
          dateType: 'monthrange',
          startPlaceholder: '开始月份',
          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'
        }
      ],
      // åŠ è½½çŠ¶æ€
      loading: false,
      // æ¦‚览数据
      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' }
      ],
      // å›¾è¡¨é…ç½®
      trendChartOptions: {},
      regionChartOptions: {},
      // è¡¨æ ¼æ•°æ®
      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 }
        ]
      }
    };
  },
  created() {
    this.loadData();
  },
  methods: {
    // åŠ è½½æ•°æ®
    async loadData() {
      this.loading = true;
      try {
        await new Promise(resolve => setTimeout(resolve, 500));
        // å¤„理表格数据
        let filteredData = [...this.mockData.table];
        if (this.query.region.length > 0) {
          filteredData = filteredData.filter(item =>
            this.query.region.includes(item.region.replace('地区', '').toLowerCase())
          );
        }
        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
          );
        }
        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));
          }
        }
        this.tableData = filteredData;
        this.total = filteredData.length;
        // æ›´æ–°æ¦‚览数据
        this.updateOverviewData(filteredData);
        // æ›´æ–°å›¾è¡¨
        this.updateCharts();
      } catch (error) {
        console.error('加载数据失败:', error);
        this.$message.error('数据加载失败');
      } finally {
        this.loading = false;
      }
    },
    // æ›´æ–°æ¦‚览数据
    updateOverviewData(data) {
      if (data.length === 0) return;
      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;
      this.overviewData[0].value = totalCases;
      this.overviewData[1].value = totalDonations;
      this.overviewData[2].value = `${avgWillingness}%`;
      this.overviewData[3].value = `${trend > 0 ? '+' : ''}${trend}%`;
    },
    // æ›´æ–°å›¾è¡¨
    updateCharts() {
      // è¶‹åŠ¿å›¾
      this.trendChartOptions = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'cross',
            label: {
              backgroundColor: '#6a7985'
            }
          }
        },
        legend: {
          data: ['捐献意愿', '案例数量', '捐献数量']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          boundaryGap: false,
          data: this.mockData.trend.map(item => item.month.replace('-', 'å¹´') + '月')
        },
        yAxis: [
          {
            type: 'value',
            name: '意愿率(%)',
            min: 60,
            max: 90,
            axisLabel: {
              formatter: '{value}%'
            }
          },
          {
            type: 'value',
            name: '数量(个)',
            position: 'right'
          }
        ],
        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' }
          }
        ]
      };
      // åœ°åŒºå¯¹æ¯”图
      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'
          }
        ],
        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' }
          }
        ]
      };
    },
    // æ ¼å¼åŒ–月份
    formatMonth(month) {
      if (!month) return '';
      return month.replace('-', 'å¹´') + '月';
    },
    // èŽ·å–è¿›åº¦æ¡é¢œè‰²
    getProgressColor(percentage) {
      if (percentage >= 80) return '#67C23A';
      if (percentage >= 70) return '#E6A23C';
      return '#F56C6C';
    },
    // èŽ·å–æ¯”è¾ƒæ ‡ç­¾ç±»åž‹
    getCompareTag(value) {
      if (value > 0) return 'success';
      if (value < 0) return 'danger';
      return 'info';
    },
    // å¤„理查询
    handleSearch(formData) {
      this.query = { ...this.query, ...formData, page: 1 };
      this.loadData();
    },
    // å¤„理重置
    handleReset() {
      this.query = {
        region: [],
        monthRange: [],
        organType: null,
        page: 1,
        limit: 10
      };
      this.loadData();
    },
    // å¤„理导出
    handleExport() {
      this.$message.info('导出功能开发中...');
    }
  }
};
</script>