<!-- 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="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-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 { caseTrend, caseTrendArea, 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';
|
|
// 捐献意愿 = 完成捐献 / 潜在捐献,统一复用 case 相关接口推导
|
export default {
|
name: 'WillingnessStatistics',
|
components: {
|
FilterPanel,
|
EChartsWrapper
|
},
|
data() {
|
return {
|
// 查询参数
|
query: {
|
region: [],
|
monthRange: [],
|
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: 'monthRange',
|
type: 'daterange',
|
dateType: 'monthrange',
|
startPlaceholder: '开始月份',
|
endPlaceholder: '结束月份',
|
valueFormat: 'yyyy-MM',
|
width: '300px'
|
}
|
],
|
|
// 加载状态
|
loading: false,
|
|
// 概览数据
|
overviewData: [
|
{ title: '总案例数', value: 0, desc: '统计周期内', icon: 'el-icon-s-flag', color: '#5B8FF9' },
|
{ title: '总捐献数量', value: 0, desc: '统计周期内', icon: 'el-icon-check', color: '#5AD8A6' },
|
{ title: '平均捐献意愿', value: '0%', desc: '统计周期内', icon: 'el-icon-s-data', color: '#F6BD16' },
|
{ title: '趋势变化', value: '0%', desc: '统计周期内', icon: 'el-icon-trend', color: '#E8684A' }
|
],
|
|
// 图表配置
|
trendChartOptions: {},
|
regionChartOptions: {},
|
|
// 图表数据
|
trendData: [],
|
regionData: [],
|
|
// 表格数据
|
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.monthRange && this.query.monthRange.length === 2) {
|
q.startTime = this.query.monthRange[0];
|
q.endTime = this.query.monthRange[1];
|
}
|
return q;
|
},
|
|
// 计算意愿率
|
calcWillingness(potential, complete) {
|
const p = toNum(potential);
|
return p > 0 ? Number(((toNum(complete) / p) * 100).toFixed(1)) : 0;
|
},
|
|
// 加载数据
|
async loadData() {
|
this.loading = true;
|
try {
|
const q = this.buildQuery();
|
const [trendRes, areaRes, moonRes] = await Promise.all([
|
caseTrend(q),
|
caseTrendArea(q),
|
caseTrendMoon(q)
|
]);
|
|
// 趋势(月份维度)
|
this.trendData = toTotalsList(trendRes)
|
.map(item => {
|
const potential = toNum(item.potentialCount);
|
const complete = toNum(item.completeCount);
|
return {
|
month: toMonth(item.donateTime) || toMonth(item.month),
|
potential,
|
complete,
|
willingness: this.calcWillingness(potential, complete)
|
};
|
})
|
.sort((a, b) => String(a.month).localeCompare(String(b.month)));
|
|
// 地区对比
|
this.regionData = toTotalsList(areaRes).map(item => {
|
const potential = toNum(item.potentialCount);
|
const complete = toNum(item.completeCount);
|
return {
|
region: item.area || item.region || '',
|
potential,
|
complete,
|
willingness: this.calcWillingness(potential, complete)
|
};
|
});
|
|
// 明细表(地区 x 月份)
|
const rows = toTotalsList(moonRes)
|
.map(item => {
|
const potential = toNum(item.potentialCount);
|
const complete = toNum(item.completeCount);
|
return {
|
region: item.area || item.region || '',
|
month: toMonth(item.donateTime) || toMonth(item.month),
|
potential,
|
complete
|
};
|
})
|
.sort((a, b) => String(a.month).localeCompare(String(b.month)));
|
|
// 计算环比变化(同一地区,相对上一月份意愿率变化,百分点)
|
rows.forEach(row => {
|
const willingness = this.calcWillingness(row.potential, row.complete);
|
// 该地区按月份排序后的上一个月
|
const regionRows = rows.filter(r => r.region === row.region);
|
const idx = regionRows.indexOf(row);
|
let monthCompare = 0;
|
if (idx > 0) {
|
const prev = regionRows[idx - 1];
|
const prevWillingness = this.calcWillingness(prev.potential, prev.complete);
|
monthCompare = Number((willingness - prevWillingness).toFixed(1));
|
}
|
row.willingnessRate = willingness;
|
row.caseCount = row.potential;
|
row.donationCount = row.complete;
|
row.monthCompare = monthCompare;
|
});
|
|
this.total = rows.length;
|
const start = (this.query.page - 1) * this.query.limit;
|
this.tableData = rows.slice(start, start + this.query.limit);
|
|
this.updateOverviewData(rows);
|
this.updateCharts();
|
} catch (error) {
|
console.error('加载数据失败:', error);
|
this.$message.error('数据加载失败');
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
// 更新概览数据
|
updateOverviewData(rows) {
|
const totalCases = rows.reduce((sum, r) => sum + toNum(r.potential), 0);
|
const totalDonations = rows.reduce((sum, r) => sum + toNum(r.complete), 0);
|
const avgWillingness = this.calcWillingness(totalCases, totalDonations);
|
|
// 趋势变化:最近两个月意愿率差值
|
let trend = 0;
|
if (this.trendData.length >= 2) {
|
const last = this.trendData[this.trendData.length - 1].willingness;
|
const prev = this.trendData[this.trendData.length - 2].willingness;
|
trend = Number((last - prev).toFixed(1));
|
}
|
|
this.overviewData[0].value = totalCases;
|
this.overviewData[1].value = totalDonations;
|
this.overviewData[2].value = `${avgWillingness}%`;
|
this.overviewData[3].value = `${trend > 0 ? '+' : ''}${trend}%`;
|
},
|
|
// 更新图表
|
updateCharts() {
|
const dualAxis = [
|
{
|
type: 'value',
|
name: '意愿率(%)',
|
nameTextStyle: { color: '#b0b6c3', fontSize: 12 },
|
axisLabel: { color: '#8a919f', fontSize: 12, formatter: '{value}%' },
|
splitLine: { lineStyle: { type: 'dashed', color: '#f0f1f5' } }
|
},
|
{
|
type: 'value',
|
name: '数量(个)',
|
nameTextStyle: { color: '#b0b6c3', fontSize: 12 },
|
axisLabel: { color: '#8a919f', fontSize: 12 },
|
splitLine: { show: false }
|
}
|
];
|
|
// 趋势图(意愿率折线 + 数量柱状,双轴)
|
this.trendChartOptions = {
|
tooltip: axisTooltip(),
|
legend: legend(['捐献意愿', '案例数量', '捐献数量']),
|
grid: grid(),
|
xAxis: {
|
type: 'category',
|
boundaryGap: true,
|
data: this.trendData.map(item => item.month),
|
axisLine: { show: false },
|
axisTick: { show: false },
|
axisLabel: { color: '#8a919f', fontSize: 12 }
|
},
|
yAxis: dualAxis,
|
series: [
|
lineSeries('捐献意愿', this.trendData.map(item => item.willingness), COLORS.blue, { area: true }),
|
barSeries('案例数量', this.trendData.map(item => item.potential), COLORS.green, { yAxisIndex: 1 }),
|
barSeries('捐献数量', this.trendData.map(item => item.complete), COLORS.amber, { yAxisIndex: 1 })
|
]
|
};
|
|
// 地区对比图
|
this.regionChartOptions = {
|
tooltip: axisTooltip(),
|
legend: legend(['捐献意愿', '案例数量', '捐献数量']),
|
grid: grid(),
|
xAxis: xAxis(this.regionData.map(item => item.region)),
|
yAxis: dualAxis,
|
series: [
|
barSeries('捐献意愿', this.regionData.map(item => item.willingness), COLORS.blue),
|
barSeries('案例数量', this.regionData.map(item => item.potential), COLORS.green, { yAxisIndex: 1 }),
|
barSeries('捐献数量', this.regionData.map(item => item.complete), COLORS.amber, { yAxisIndex: 1 })
|
]
|
};
|
},
|
|
// 格式化月份
|
formatMonth(month) {
|
if (!month) return '';
|
return month.replace('-', '年') + '月';
|
},
|
|
// 获取进度条颜色
|
getProgressColor(percentage) {
|
if (percentage >= 80) return COLORS.green;
|
if (percentage >= 70) return COLORS.amber;
|
return COLORS.coral;
|
},
|
|
// 获取比较标签类型
|
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: [],
|
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>
|
.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>
|