Files
campus-activity-system/web/src/views/admin/ActivityStats.vue

182 lines
4.3 KiB
Vue

<template>
<div class="stats-page">
<van-nav-bar
title="数据统计"
left-arrow
@click-left="onClickLeft"
fixed
placeholder
/>
<div v-if="stats" class="stats-content">
<div class="header-card">
<h3>{{ stats.activityTitle }}</h3>
<van-grid :column-num="3" :border="false">
<van-grid-item>
<div class="stat-num">{{ stats.registeredCount }}</div>
<div class="stat-label">报名人数</div>
</van-grid-item>
<van-grid-item>
<div class="stat-num">{{ stats.checkedInCount }}</div>
<div class="stat-label">签到人数</div>
</van-grid-item>
<van-grid-item>
<div class="stat-num">{{ (stats.checkInRate * 100).toFixed(1) }}%</div>
<div class="stat-label">签到率</div>
</van-grid-item>
</van-grid>
</div>
<div class="chart-card">
<h4>评分分布</h4>
<div class="rating-dist">
<div class="rate-row" v-for="score in [5,4,3,2,1]" :key="score">
<span class="label">{{ score }}</span>
<van-progress
:percentage="getPercentage(score)"
:show-pivot="true"
color="#ffd21e"
stroke-width="8"
/>
<span class="count">{{ stats.ratingDistribution[score] || 0 }}</span>
</div>
</div>
<div class="avg-rating">
平均分: <strong>{{ stats.averageRating.toFixed(1) }}</strong>
</div>
</div>
<div class="actions">
<van-button icon="down" block type="success" @click="handleExport">导出数据报表</van-button>
</div>
</div>
<van-loading v-else class="loading" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { getActivityStats, exportActivityStats } from '@/services/stats';
import type { ActivityStats } from '@/services/stats';
import { showToast } from 'vant';
const route = useRoute();
const activityId = Number(route.params.id);
const stats = ref<ActivityStats | null>(null);
onMounted(async () => {
try {
const res = await getActivityStats(activityId);
stats.value = res;
} catch (error) {
showToast('加载统计数据失败');
}
});
const onClickLeft = () => history.back();
const getPercentage = (score: number) => {
if (!stats.value || !stats.value.reviewCount) return 0;
const count = stats.value.ratingDistribution[score] || 0;
return Math.round((count / stats.value.reviewCount) * 100);
};
const handleExport = async () => {
try {
const blob = await exportActivityStats(activityId);
// Download logic
const url = window.URL.createObjectURL(new Blob([blob as any]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `activity-${activityId}-stats.xlsx`);
document.body.appendChild(link);
link.click();
showToast('导出成功');
} catch (error) {
showToast('导出失败');
}
};
</script>
<style scoped lang="scss">
.stats-page {
min-height: 100vh;
background-color: #f7f8fa;
.stats-content {
padding: 16px;
}
.header-card, .chart-card {
background: #fff;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.stat-num {
font-size: 20px;
font-weight: bold;
color: #333;
}
.stat-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.chart-card {
h4 {
margin: 0 0 16px;
}
.rating-dist {
.rate-row {
display: flex;
align-items: center;
margin-bottom: 12px;
.label {
width: 30px;
font-size: 12px;
}
.van-progress {
flex: 1;
margin: 0 10px;
}
.count {
width: 20px;
font-size: 12px;
color: #999;
}
}
}
.avg-rating {
text-align: center;
margin-top: 16px;
font-size: 16px;
strong {
font-size: 24px;
color: #ffd21e;
}
}
}
.actions {
margin-top: 32px;
}
.loading {
margin-top: 100px;
text-align: center;
}
}
</style>