From 3df460667d74868d0a54fce12d5e881bbd6c0c1a Mon Sep 17 00:00:00 2001
From: WXL <wl_5969728@163.com>
Date: 星期四, 17 九月 2026 17:11:18 +0800
Subject: [PATCH] 青岛维护统计
---
src/views/statistics/rate.vue | 406 +++++++++++++++++++++++++++++++++------------------------
1 files changed, 233 insertions(+), 173 deletions(-)
diff --git a/src/views/statistics/rate.vue b/src/views/statistics/rate.vue
index 58c3edc..9eeb6d3 100644
--- a/src/views/statistics/rate.vue
+++ b/src/views/statistics/rate.vue
@@ -53,7 +53,7 @@
<el-col :span="12">
<el-card>
<div slot="header" class="clearfix">
- <span>寮冪敤鐜囨瀯鎴愬垎鏋�</span>
+ <span>鍦板尯鑾峰彇/寮冪敤鐜囧姣�</span>
</div>
<EChartsWrapper
:id="'abandon-chart'"
@@ -104,6 +104,18 @@
<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>
@@ -111,6 +123,10 @@
<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',
@@ -136,13 +152,13 @@
type: 'select',
multiple: true,
options: [
- { label: '闈掑矝鍦板尯', value: 'qingdao' },
- { label: '鏃ョ収鍦板尯', value: 'rizhao' },
- { label: '娴庡崡鍦板尯', value: 'jinan' },
- { label: '鐑熷彴鍦板尯', value: 'yantai' },
- { label: '濞佹捣鍦板尯', value: 'weihai' },
- { label: '娼嶅潑鍦板尯', value: 'weifang' },
- { label: '涓存矀鍦板尯', value: 'linyi' }
+ { label: '闈掑矝鍦板尯', value: '闈掑矝鍦板尯' },
+ { label: '鏃ョ収鍦板尯', value: '鏃ョ収鍦板尯' },
+ { label: '娴庡崡鍦板尯', value: '娴庡崡鍦板尯' },
+ { label: '鐑熷彴鍦板尯', value: '鐑熷彴鍦板尯' },
+ { label: '濞佹捣鍦板尯', value: '濞佹捣鍦板尯' },
+ { label: '娼嶅潑鍦板尯', value: '娼嶅潑鍦板尯' },
+ { label: '涓存矀鍦板尯', value: '涓存矀鍦板尯' }
],
width: '300px'
},
@@ -166,25 +182,25 @@
{
key: 'acquisition',
title: '鎹愮尞鑾峰彇鐜�',
- value: '85.5%',
+ value: '0%',
trendIcon: 'el-icon-top',
- trendText: '鈫�2.7%',
+ trendText: '鈥�',
trendClass: 'up'
},
{
key: 'avg',
title: '骞冲潎鍣ㄥ畼鏁�',
- value: '3.2',
+ value: '0',
trendIcon: 'el-icon-top',
- trendText: '鈫�0.2',
+ trendText: '鈥�',
trendClass: 'up'
},
{
key: 'abandon',
title: '鍣ㄥ畼寮冪敤鐜�',
- value: '12.3%',
+ value: '0%',
trendIcon: 'el-icon-bottom',
- trendText: '鈫�2.7%',
+ trendText: '鈥�',
trendClass: 'down'
}
],
@@ -193,69 +209,150 @@
trendChartOptions: {},
abandonChartOptions: {},
+ // 鍥捐〃鏁版嵁
+ trendData: [],
+ regionCompareData: [],
+
// 琛ㄦ牸鏁版嵁
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 }
- ]
- }
+ 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 {
- await new Promise(resolve => setTimeout(resolve, 500));
+ const q = this.buildQuery();
+ const [organRes, moonRes, caseMoonRes] = await Promise.all([
+ organ(q),
+ organMoon(q),
+ caseTrendMoon(q)
+ ]);
- // 澶勭悊琛ㄦ牸鏁版嵁
- let filteredData = [...this.mockData.table];
+ // 鍣ㄥ畼鎸夋湀瓒嬪娍锛氳幏鍙栫巼(绉绘/鑾峰彇)銆佸純鐢ㄧ巼
+ 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)));
- if (this.query.region.length > 0) {
- filteredData = filteredData.filter(item =>
- this.query.region.includes(item.region.replace('鍦板尯', '').toLowerCase())
- );
- }
+ // 鎸� 骞翠唤|鍦板尯 姹囨�诲櫒瀹樻暟鎹�
+ 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);
+ });
- 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)
- );
- }
+ // 鎸� 骞翠唤|鍦板尯 姹囨�绘渚嬫暟鎹紙鎹愮尞鎬绘暟銆佹綔鍦ㄦ渚嬶級
+ 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);
+ });
- this.tableData = filteredData;
- this.total = filteredData.length;
+ // 鍚堝苟 骞翠唤|鍦板尯 鏄庣粏
+ 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('鏁版嵁鍔犺浇澶辫触');
@@ -264,157 +361,107 @@
}
},
+ // 鏇存柊浠〃鐩�
+ 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: {
- trigger: 'axis',
- axisPointer: {
- type: 'cross',
- label: {
- backgroundColor: '#6a7985'
- }
- }
- },
- legend: {
- data: ['鎹愮尞鑾峰彇鐜�', '骞冲潎鍣ㄥ畼鏁�', '鍣ㄥ畼寮冪敤鐜�']
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '3%',
- containLabel: true
- },
+ tooltip: axisTooltip(),
+ legend: legend(['鍣ㄥ畼鍒╃敤鐜�', '鍣ㄥ畼寮冪敤鐜�']),
+ grid: grid(),
xAxis: {
type: 'category',
boundaryGap: false,
- data: this.mockData.trend.map(item => item.year + '骞�')
+ data: this.trendData.map(item => item.month),
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: '#8a919f', fontSize: 12 }
},
yAxis: {
type: 'value',
- axisLabel: {
- formatter: '{value}'
- }
+ axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' },
+ splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } }
},
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
- }
+ lineSeries('鍣ㄥ畼鍒╃敤鐜�', this.trendData.map(item => item.acquisition), COLORS.blue, { area: true }),
+ lineSeries('鍣ㄥ畼寮冪敤鐜�', this.trendData.map(item => item.abandon), COLORS.coral)
]
};
- // 寮冪敤鐜囨瀯鎴愬浘
+ // 鍦板尯鑾峰彇/寮冪敤鐜囧姣斿浘
this.abandonChartOptions = {
- tooltip: {
- trigger: 'item',
- formatter: '{a} <br/>{b} : {c} ({d}%)'
- },
- legend: {
- orient: 'vertical',
- left: 'left',
- data: this.mockData.abandon.map(item => item.name)
+ 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: [
- {
- 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
- }
+ 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 data = [{ value, name: item.title }];
+ 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 {
- tooltip: {
- formatter: '{b}: {c}' + (item.key === 'abandon' ? '%' : item.key === 'acquisition' ? '%' : '')
- },
series: [
{
- name: item.title,
type: 'gauge',
- progress: {
- show: true,
- width: 18
- },
+ startAngle: 210,
+ endAngle: -30,
+ min: 0,
+ max,
+ splitNumber,
+ radius: '60%',
+ center: ['50%', '52%'],
axisLine: {
lineStyle: {
- width: 18
+ width: 10,
+ color: [[value / max, color], [1, '#eef0f5']]
}
},
- axisTick: {
- show: false
+ pointer: {
+ length: '60%',
+ width: 4,
+ itemStyle: { color }
},
- splitLine: {
- length: 15,
- lineStyle: {
- width: 2,
- color: '#999'
- }
- },
+ anchor: { show: false },
+ axisTick: { show: false },
+ // length 褰掗浂 + 璐熻窛绂伙紝浣垮埢搴︽暟瀛楄惤鍦ㄨ〃鐩樺渾寮у渚э紙澶栧洿锛�
+ splitLine: { show: false, length: 0 },
axisLabel: {
- distance: 25,
- color: '#999',
+ distance: -18,
+ color: '#8a919f',
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
+ title: { show: false },
+ detail: { show: false },
+ data: [{ value, name: item.title }]
}
]
};
@@ -455,6 +502,19 @@
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('瀵煎嚭鍔熻兘寮�鍙戜腑...');
--
Gitblit v1.9.3