Files
schoolNews/schoolNewsWeb/src/views/admin/overview/SystemOverviewView.vue

429 lines
9.8 KiB
Vue
Raw Normal View History

2025-10-16 18:03:46 +08:00
<template>
<div class="system-overview">
<h1 class="page-title">系统总览</h1>
<!-- 统计卡片 -->
<div class="stats-grid">
2025-11-14 18:31:39 +08:00
<!-- 总用户 -->
<div class="stat-card">
<div class="stat-icon"><img src="@/assets/imgs/overview-user.svg" alt="用户" /></div>
<div class="stat-content">
<h3>用户总数</h3>
<span>
{{ statistics.totalUsers }}
<span>
{{ statistics.totalUsersChange }} 较昨日
</span>
</span>
</div>
</div>
<!-- 总资源 -->
<div class="stat-card">
<div class="stat-icon"><img src="@/assets/imgs/overview-resource.svg" alt="资源" /></div>
<div class="stat-content">
<h3>资源总数</h3>
<span>
{{ statistics.totalResources }}
<span>
{{ statistics.totalResourcesChange }} 较昨日
</span>
</span>
</div>
</div>
<!-- 今日pv -->
<div class="stat-card">
<div class="stat-icon"><img src="@/assets/imgs/overview-pv.svg" alt="pv" /></div>
<div class="stat-content">
<h3>今日访问</h3>
<span>
{{ statistics.totalPv }}
<span>
{{ statistics.totalPvChange }} 较昨日
</span>
</span>
2025-10-16 18:03:46 +08:00
</div>
2025-11-14 18:31:39 +08:00
</div>
<!-- 今日uv -->
<div class="stat-card">
<div class="stat-icon"><img src="@/assets/imgs/overview-uv.svg" alt="uv" /></div>
2025-10-16 18:03:46 +08:00
<div class="stat-content">
2025-11-14 18:31:39 +08:00
<h3>今日用户</h3>
<span>
{{ statistics.totalUv }}
<span>
{{ statistics.totalUvChange }} 较昨日
</span>
2025-10-16 18:03:46 +08:00
</span>
</div>
</div>
</div>
<!-- 图表区域 -->
<el-row :gutter="20" class="charts-row">
<el-col :span="16">
<el-card class="chart-card">
<template #header>
<div class="card-header">
2025-11-14 18:31:39 +08:00
<span>访问统计趋势PV/UV</span>
2025-10-16 18:03:46 +08:00
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
/>
</div>
</template>
<div class="chart-container" ref="activityChart"></div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="chart-card">
<template #header>
<span>资源分类统计(饼图)</span>
</template>
<div class="chart-container" ref="resourcePieChart"></div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
2025-11-14 18:31:39 +08:00
import { ref, onMounted, onUnmounted, watch } from 'vue';
2025-10-16 18:03:46 +08:00
import { ElRow, ElCol, ElCard, ElDatePicker } from 'element-plus';
import * as echarts from 'echarts';
2025-11-14 15:29:02 +08:00
import { systemOverviewApi } from '@/apis/system/overview';
2025-11-14 18:31:39 +08:00
import dayjs from 'dayjs';
2025-10-16 18:03:46 +08:00
const dateRange = ref<[Date, Date] | null>(null);
const activityChart = ref<HTMLElement | null>(null);
const resourcePieChart = ref<HTMLElement | null>(null);
let activityChartInstance: echarts.ECharts | null = null;
let pieChartInstance: echarts.ECharts | null = null;
2025-11-14 18:31:39 +08:00
const statistics = ref({
totalUsers: 0,
totalUsersChange: '+0%',
totalResources: 0,
totalResourcesChange: '+0%',
totalPv: 0,
totalPvChange: '+0%',
totalUv: 0,
totalUvChange: '+0%'
});
2025-10-16 18:03:46 +08:00
2025-11-14 15:29:02 +08:00
onMounted(async () => {
2025-11-14 18:31:39 +08:00
// 默认选择最近 7 天
const now = new Date();
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
dateRange.value = [sevenDaysAgo, now];
2025-10-16 18:03:46 +08:00
initCharts();
2025-11-14 15:29:02 +08:00
await loadOverviewData();
2025-10-16 18:03:46 +08:00
});
onUnmounted(() => {
if (activityChartInstance) {
activityChartInstance.dispose();
}
if (pieChartInstance) {
pieChartInstance.dispose();
}
});
2025-11-14 18:31:39 +08:00
// 监听日期范围变化,重新加载活跃用户图表
watch(dateRange, async (newRange) => {
if (newRange && newRange.length === 2) {
try {
const activeRes = await systemOverviewApi.getActiveUsersChart(
formatDate(newRange[0]),
formatDate(newRange[1])
);
if (activeRes.success && activeRes.data) {
updateActivityChart(activeRes.data.labels, activeRes.data.pvValues, activeRes.data.uvValues);
}
} catch (error) {
console.error('加载活跃用户图表数据失败:', error);
}
}
});
function formatDate(date: Date) {
return dayjs(date).format('YYYY-MM-DD');
}
2025-11-14 15:29:02 +08:00
async function loadOverviewData() {
try {
const [statRes, activeRes, pieRes, todayRes] = await Promise.all([
systemOverviewApi.getStatistics(),
2025-11-14 18:31:39 +08:00
systemOverviewApi.getActiveUsersChart(formatDate(dateRange.value?.[0]!), formatDate(dateRange.value?.[1]!)),
2025-11-14 15:29:02 +08:00
systemOverviewApi.getResourceCategoryStats(),
systemOverviewApi.getTodayVisits()
]);
if (statRes.success && statRes.data) {
const d = statRes.data;
2025-11-14 18:31:39 +08:00
statistics.value = {
totalUsers: d.totalUsers ?? 0,
totalUsersChange: String(d.totalUsersChange ?? '+0%'),
totalResources: d.totalResources ?? 0,
totalResourcesChange: String(d.totalResourcesChange ?? '+0%'),
totalPv: d.totalPv ?? 0,
totalPvChange: String(d.totalPvChange ?? '+0%'),
totalUv: d.totalUv ?? 0,
totalUvChange: String(d.totalUvChange ?? '+0%')
};
2025-11-14 15:29:02 +08:00
}
if (todayRes.success && todayRes.data) {
const t = todayRes.data;
2025-11-14 18:31:39 +08:00
// todayRes 仍用于“今日访问”区域(如后续需要),当前统计卡片只使用 statistics
2025-11-14 15:29:02 +08:00
}
if (activeRes.success && activeRes.data) {
2025-11-14 18:31:39 +08:00
updateActivityChart(activeRes.data.labels, activeRes.data.pvValues, activeRes.data.uvValues);
2025-11-14 15:29:02 +08:00
}
if (pieRes.success && pieRes.data) {
updatePieChart(pieRes.data.items);
}
} catch (error) {
console.error('加载系统总览数据失败:', error);
}
}
2025-10-16 18:03:46 +08:00
function initCharts() {
if (activityChart.value) {
activityChartInstance = echarts.init(activityChart.value);
}
if (resourcePieChart.value) {
pieChartInstance = echarts.init(resourcePieChart.value);
2025-11-14 15:29:02 +08:00
}
}
2025-11-14 18:31:39 +08:00
function updateActivityChart(labels: string[], pvValues: number[], uvValues: number[]) {
2025-11-14 15:29:02 +08:00
if (!activityChartInstance) return;
const option = {
tooltip: {
2025-11-14 18:31:39 +08:00
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: ['页面访问量 (PV)', '独立访客数 (UV)'],
top: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
2025-11-14 15:29:02 +08:00
},
xAxis: {
type: 'category',
2025-11-14 18:31:39 +08:00
boundaryGap: false,
2025-11-14 15:29:02 +08:00
data: labels
},
yAxis: {
type: 'value'
},
series: [
{
2025-11-14 18:31:39 +08:00
name: '页面访问量 (PV)',
data: pvValues,
type: 'line',
smooth: true,
itemStyle: {
color: '#5470c6'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(84, 112, 198, 0.3)' },
{ offset: 1, color: 'rgba(84, 112, 198, 0.05)' }
]
}
}
},
{
name: '独立访客数 (UV)',
data: uvValues,
2025-11-14 15:29:02 +08:00
type: 'line',
smooth: true,
2025-11-14 18:31:39 +08:00
itemStyle: {
color: '#91cc75'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(145, 204, 117, 0.3)' },
{ offset: 1, color: 'rgba(145, 204, 117, 0.05)' }
]
}
}
2025-11-14 15:29:02 +08:00
}
]
};
activityChartInstance.setOption(option);
}
function updatePieChart(items: { name: string; value: number }[]) {
if (!pieChartInstance) return;
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
2025-10-16 18:03:46 +08:00
type: 'pie',
radius: '50%',
2025-11-14 15:29:02 +08:00
data: items
}
]
};
pieChartInstance.setOption(option);
2025-10-16 18:03:46 +08:00
}
</script>
<style lang="scss" scoped>
.system-overview {
2025-11-14 18:31:39 +08:00
padding: 16px 0;
2025-10-16 18:03:46 +08:00
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
2025-11-14 18:31:39 +08:00
gap: 16px;
margin-bottom: 24px;
2025-10-16 18:03:46 +08:00
}
.stat-card {
2025-11-14 18:31:39 +08:00
background: #ffffff;
padding: 20px 24px;
border-radius: 16px;
2025-10-16 18:03:46 +08:00
display: flex;
2025-11-14 18:31:39 +08:00
align-items: center;
2025-10-16 18:03:46 +08:00
gap: 16px;
2025-11-14 18:31:39 +08:00
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
2025-10-16 18:03:46 +08:00
}
.stat-icon {
2025-11-14 18:31:39 +08:00
width: 48px;
height: 48px;
border-radius: 20%;
background: #f5f7ff;
2025-10-16 18:03:46 +08:00
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
2025-11-14 18:31:39 +08:00
img {
width: 24px;
height: 24px;
}
2025-10-16 18:03:46 +08:00
}
.stat-content {
flex: 1;
2025-11-14 18:31:39 +08:00
2025-10-16 18:03:46 +08:00
h3 {
font-size: 14px;
2025-11-14 18:31:39 +08:00
font-weight: 500;
color: #64748b;
margin-bottom: 6px;
2025-10-16 18:03:46 +08:00
}
2025-11-14 18:31:39 +08:00
> span {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 28px;
font-weight: 600;
color: #0f172a;
> span {
font-size: 12px;
font-weight: 400;
color: #16a34a; // 默认上升为绿色
}
2025-10-16 18:03:46 +08:00
}
}
.charts-row {
margin-bottom: 20px;
}
.chart-card {
:deep(.el-card__body) {
padding: 20px;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-container {
height: 300px;
}
.visit-card {
:deep(.el-card__body) {
padding: 20px;
}
}
.visit-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.visit-item {
text-align: center;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.visit-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.visit-value {
font-size: 24px;
font-weight: 600;
color: #C62828;
}
</style>