From b22b937bf902dcfbbf6d2cc6dc95ca47d160e199 Mon Sep 17 00:00:00 2001
From: WXL <wl_5969728@163.com>
Date: 星期日, 17 五月 2026 21:40:00 +0800
Subject: [PATCH] 统计维护
---
src/api/statistics/index.js | 56
src/views/statistics/index.vue | 134 +
src/styles/statistics.css | 47
src/views/statistics/donor.vue | 702 ++++++++++
src/views/statistics/rate.vue | 506 +++++++
src/views/statistics/components/OrganDetail.vue | 87 +
src/views/statistics/organ.vue | 519 +++++++
package.json | 7
src/views/statistics/case.vue | 546 +++++++
src/views/statistics/components/CaseDetail.vue | 76 +
src/components/charts/EChartsWrapper.vue | 79 +
src/components/charts/FilterPanel.vue | 164 ++
src/utils/excel.js | 44
src/views/statistics/willingness.vue | 474 ++++++
src/utils/statistics.js | 37
src/views/statistics/utilization.vue | 688 +++++++++
16 files changed, 4,163 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index fd6a9d5..3dee21b 100644
--- a/package.json
+++ b/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",
diff --git a/src/api/statistics/index.js b/src/api/statistics/index.js
new file mode 100644
index 0000000..86f4016
--- /dev/null
+++ b/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
+ });
+}
diff --git a/src/components/charts/EChartsWrapper.vue b/src/components/charts/EChartsWrapper.vue
new file mode 100644
index 0000000..47c67db
--- /dev/null
+++ b/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>
diff --git a/src/components/charts/FilterPanel.vue b/src/components/charts/FilterPanel.vue
new file mode 100644
index 0000000..ee55641
--- /dev/null
+++ b/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>
diff --git a/src/styles/statistics.css b/src/styles/statistics.css
new file mode 100644
index 0000000..8d58ca0
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/utils/excel.js b/src/utils/excel.js
new file mode 100644
index 0000000..9dac8d6
--- /dev/null
+++ b/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);
+}
diff --git a/src/utils/statistics.js b/src/utils/statistics.js
new file mode 100644
index 0000000..14782ab
--- /dev/null
+++ b/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`);
+ });
+}
diff --git a/src/views/statistics/case.vue b/src/views/statistics/case.vue
new file mode 100644
index 0000000..0b7f00b
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/components/CaseDetail.vue b/src/views/statistics/components/CaseDetail.vue
new file mode 100644
index 0000000..1d2d5d2
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/components/OrganDetail.vue b/src/views/statistics/components/OrganDetail.vue
new file mode 100644
index 0000000..6dfdb21
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/donor.vue b/src/views/statistics/donor.vue
new file mode 100644
index 0000000..192e13c
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/index.vue b/src/views/statistics/index.vue
new file mode 100644
index 0000000..65545dd
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/organ.vue b/src/views/statistics/organ.vue
new file mode 100644
index 0000000..bff9883
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/rate.vue b/src/views/statistics/rate.vue
new file mode 100644
index 0000000..58c3edc
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/utilization.vue b/src/views/statistics/utilization.vue
new file mode 100644
index 0000000..51ea1c9
--- /dev/null
+++ b/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>
diff --git a/src/views/statistics/willingness.vue b/src/views/statistics/willingness.vue
new file mode 100644
index 0000000..20e469b
--- /dev/null
+++ b/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>
--
Gitblit v1.9.3