<!-- 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-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';
|
import { organ, organMoon, caseTrendMoon } from '@/api/businessApi';
|
import { toTotalsList, toMonth, toNum } from '@/utils/totals';
|
import statisticsMode from '@/utils/statisticsMode';
|
import { COLORS, axisTooltip, grid, xAxis, legend, barSeries, lineSeries } from '@/utils/chartStyle';
|
|
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: '青岛地区' },
|
{ label: '日照地区', value: '日照地区' },
|
{ label: '济南地区', value: '济南地区' },
|
{ label: '烟台地区', value: '烟台地区' },
|
{ label: '威海地区', value: '威海地区' },
|
{ label: '潍坊地区', value: '潍坊地区' },
|
{ label: '临沂地区', value: '临沂地区' }
|
],
|
width: '300px'
|
},
|
{
|
label: '年份范围',
|
prop: 'yearRange',
|
type: 'daterange',
|
dateType: 'yearrange',
|
startPlaceholder: '开始年份',
|
endPlaceholder: '结束年份',
|
valueFormat: 'yyyy',
|
width: '300px'
|
}
|
],
|
|
// 加载状态
|
loading: false,
|
|
// 仪表盘数据
|
dashboardData: [
|
{
|
key: 'acquisition',
|
title: '捐献获取率',
|
value: '0%',
|
trendIcon: 'el-icon-top',
|
trendText: '—',
|
trendClass: 'up'
|
},
|
{
|
key: 'avg',
|
title: '平均器官数',
|
value: '0',
|
trendIcon: 'el-icon-top',
|
trendText: '—',
|
trendClass: 'up'
|
},
|
{
|
key: 'abandon',
|
title: '器官弃用率',
|
value: '0%',
|
trendIcon: 'el-icon-bottom',
|
trendText: '—',
|
trendClass: 'down'
|
}
|
],
|
|
// 图表配置
|
trendChartOptions: {},
|
abandonChartOptions: {},
|
|
// 图表数据
|
trendData: [],
|
regionCompareData: [],
|
|
// 表格数据
|
tableData: [],
|
total: 0
|
};
|
},
|
created() {
|
this.loadData();
|
// 数据源(mock/真实接口)切换时重新加载
|
this.$watch(() => statisticsMode.useMock, () => this.loadData());
|
},
|
methods: {
|
// 组装查询条件
|
buildQuery() {
|
const q = {};
|
if (this.query.region && this.query.region.length) {
|
q.area = this.query.region.join(',');
|
}
|
if (this.query.yearRange && this.query.yearRange.length === 2) {
|
q.startTime = this.query.yearRange[0];
|
q.endTime = this.query.yearRange[1];
|
}
|
return q;
|
},
|
|
// 加载数据
|
async loadData() {
|
this.loading = true;
|
try {
|
const q = this.buildQuery();
|
const [organRes, moonRes, caseMoonRes] = await Promise.all([
|
organ(q),
|
organMoon(q),
|
caseTrendMoon(q)
|
]);
|
|
// 器官按月趋势:获取率(移植/获取)、弃用率
|
this.trendData = toTotalsList(moonRes)
|
.map(item => {
|
const get = toNum(item.getCount);
|
const use = toNum(item.useCount);
|
return {
|
month: toMonth(item.donateTime) || toMonth(item.month),
|
acquisition: get > 0 ? Number(((use / get) * 100).toFixed(1)) : 0,
|
abandon: get > 0 ? Number((((get - use) / get) * 100).toFixed(1)) : 0
|
};
|
})
|
.sort((a, b) => String(a.month).localeCompare(String(b.month)));
|
|
// 按 年份|地区 汇总器官数据
|
const organByKey = {};
|
toTotalsList(organRes).forEach(item => {
|
const year = String(item.donateTime || item.year || '').slice(0, 4);
|
const region = item.area || item.region || '';
|
const key = `${year}|${region}`;
|
if (!organByKey[key]) organByKey[key] = { year, region, organs: 0, transplants: 0 };
|
organByKey[key].organs += toNum(item.getCount);
|
organByKey[key].transplants += toNum(item.useCount);
|
});
|
|
// 按 年份|地区 汇总案例数据(捐献总数、潜在案例)
|
const caseByKey = {};
|
toTotalsList(caseMoonRes).forEach(item => {
|
const month = toMonth(item.donateTime) || toMonth(item.month);
|
const year = String(month).slice(0, 4);
|
const region = item.area || item.region || '';
|
const key = `${year}|${region}`;
|
if (!caseByKey[key]) caseByKey[key] = { year, region, potential: 0, complete: 0 };
|
caseByKey[key].potential += toNum(item.potentialCount);
|
caseByKey[key].complete += toNum(item.completeCount);
|
});
|
|
// 合并 年份|地区 明细
|
const keys = new Set([...Object.keys(organByKey), ...Object.keys(caseByKey)]);
|
const rows = [];
|
keys.forEach(key => {
|
const o = organByKey[key] || {};
|
const c = caseByKey[key] || {};
|
const year = o.year || c.year || '';
|
const region = o.region || c.region || '';
|
const organs = o.organs || 0;
|
const transplants = o.transplants || 0;
|
const potential = c.potential || 0;
|
const complete = c.complete || 0;
|
|
rows.push({
|
year,
|
region,
|
acquisitionRate: potential > 0 ? Number(((complete / potential) * 100).toFixed(1)) : 0,
|
avgOrgans: complete > 0 ? Number((organs / complete).toFixed(1)) : 0,
|
abandonRate: organs > 0 ? Number((((organs - transplants) / organs) * 100).toFixed(1)) : 0,
|
totalDonation: complete,
|
totalOrgans: organs,
|
totalTransplants: transplants
|
});
|
});
|
rows.sort((a, b) => String(a.year + a.region).localeCompare(String(b.year + b.region)));
|
|
this.total = rows.length;
|
const start = (this.query.page - 1) * this.query.limit;
|
this.tableData = rows.slice(start, start + this.query.limit);
|
|
// 地区维度的获取率/弃用率,用潜在/完成、器官/移植原始数据汇总
|
const regionCompare = {};
|
toTotalsList(caseMoonRes).forEach(item => {
|
const region = item.area || item.region || '';
|
if (!regionCompare[region]) regionCompare[region] = { region, potential: 0, complete: 0 };
|
regionCompare[region].potential += toNum(item.potentialCount);
|
regionCompare[region].complete += toNum(item.completeCount);
|
});
|
const organRegion = {};
|
toTotalsList(organRes).forEach(item => {
|
const region = item.area || item.region || '';
|
if (!organRegion[region]) organRegion[region] = { organs: 0, transplants: 0 };
|
organRegion[region].organs += toNum(item.getCount);
|
organRegion[region].transplants += toNum(item.useCount);
|
});
|
const regionNames = new Set([...Object.keys(regionCompare), ...Object.keys(organRegion)]);
|
this.regionCompareData = [...regionNames].map(region => {
|
const c = regionCompare[region] || {};
|
const o = organRegion[region] || {};
|
const potential = c.potential || 0;
|
const complete = c.complete || 0;
|
const organs = o.organs || 0;
|
const transplants = o.transplants || 0;
|
return {
|
region,
|
acquisition: potential > 0 ? Number(((complete / potential) * 100).toFixed(1)) : 0,
|
abandon: organs > 0 ? Number((((organs - transplants) / organs) * 100).toFixed(1)) : 0
|
};
|
});
|
|
const totals = {
|
totalPotential: Object.values(caseByKey).reduce((s, c) => s + c.potential, 0),
|
totalDonation: Object.values(caseByKey).reduce((s, c) => s + c.complete, 0),
|
totalOrgans: Object.values(organByKey).reduce((s, o) => s + o.organs, 0),
|
totalTransplants: Object.values(organByKey).reduce((s, o) => s + o.transplants, 0)
|
};
|
|
this.updateOverviewData(totals);
|
this.updateCharts();
|
} catch (error) {
|
console.error('加载数据失败:', error);
|
this.$message.error('数据加载失败');
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
// 更新仪表盘
|
updateOverviewData(totals) {
|
const { totalPotential, totalDonation, totalOrgans, totalTransplants } = totals;
|
|
const acquisitionRate = totalPotential > 0 ? Number(((totalDonation / totalPotential) * 100).toFixed(1)) : 0;
|
const avgOrgans = totalDonation > 0 ? Number((totalOrgans / totalDonation).toFixed(1)) : 0;
|
const abandonRate = totalOrgans > 0 ? Number((((totalOrgans - totalTransplants) / totalOrgans) * 100).toFixed(1)) : 0;
|
|
this.dashboardData[0].value = `${acquisitionRate}%`;
|
this.dashboardData[1].value = `${avgOrgans}`;
|
this.dashboardData[2].value = `${abandonRate}%`;
|
this.dashboardData[0].trendText = acquisitionRate >= 80 ? '↑达标' : '偏低';
|
this.dashboardData[2].trendText = abandonRate <= 15 ? '↓达标' : '偏高';
|
},
|
|
// 更新图表
|
updateCharts() {
|
// 获取率趋势图
|
this.trendChartOptions = {
|
tooltip: axisTooltip(),
|
legend: legend(['器官利用率', '器官弃用率']),
|
grid: grid(),
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
data: this.trendData.map(item => item.month),
|
axisLine: { show: false },
|
axisTick: { show: false },
|
axisLabel: { color: '#8a919f', fontSize: 12 }
|
},
|
yAxis: {
|
type: 'value',
|
axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' },
|
splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } }
|
},
|
series: [
|
lineSeries('器官利用率', this.trendData.map(item => item.acquisition), COLORS.blue, { area: true }),
|
lineSeries('器官弃用率', this.trendData.map(item => item.abandon), COLORS.coral)
|
]
|
};
|
|
// 地区获取/弃用率对比图
|
this.abandonChartOptions = {
|
tooltip: axisTooltip(),
|
legend: legend(['捐献获取率', '器官弃用率']),
|
grid: grid(),
|
xAxis: xAxis(this.regionCompareData.map(item => item.region)),
|
yAxis: {
|
type: 'value',
|
axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' },
|
splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } }
|
},
|
series: [
|
barSeries('捐献获取率', this.regionCompareData.map(item => item.acquisition), COLORS.blue),
|
barSeries('器官弃用率', this.regionCompareData.map(item => item.abandon), COLORS.coral)
|
]
|
};
|
},
|
|
// 获取仪表盘图表配置(表盘 + 指针,数值由上方大数字展示,刻度外置)
|
getDashboardOptions(item) {
|
const value = parseFloat(item.value);
|
const max = item.key === 'avg' ? 5 : 100;
|
const color = item.key === 'abandon' ? COLORS.coral : item.key === 'acquisition' ? COLORS.blue : COLORS.green;
|
// 平均器官数范围小用整数刻度,百分比用 0/25/50/75/100 五个刻度,避免数字拥挤
|
const splitNumber = item.key === 'avg' ? 5 : 4;
|
|
return {
|
series: [
|
{
|
type: 'gauge',
|
startAngle: 210,
|
endAngle: -30,
|
min: 0,
|
max,
|
splitNumber,
|
radius: '60%',
|
center: ['50%', '52%'],
|
axisLine: {
|
lineStyle: {
|
width: 10,
|
color: [[value / max, color], [1, '#eef0f5']]
|
}
|
},
|
pointer: {
|
length: '60%',
|
width: 4,
|
itemStyle: { color }
|
},
|
anchor: { show: false },
|
axisTick: { show: false },
|
// length 归零 + 负距离,使刻度数字落在表盘圆弧外侧(外围)
|
splitLine: { show: false, length: 0 },
|
axisLabel: {
|
distance: -18,
|
color: '#8a919f',
|
fontSize: 12
|
},
|
title: { show: false },
|
detail: { show: false },
|
data: [{ value, name: item.title }]
|
}
|
]
|
};
|
},
|
|
// 获取标签类型
|
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();
|
},
|
|
// 处理分页
|
handlePagination(page) {
|
this.query.page = page;
|
this.loadData();
|
},
|
|
// 处理分页大小变化
|
handlePageSizeChange(size) {
|
this.query.limit = size;
|
this.query.page = 1;
|
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>
|