Files
schoolNews/schoolNewsWeb/src/views/admin/overview/SystemOverviewView.vue
2025-11-27 17:31:42 +08:00

431 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="system-overview">
<h1 class="page-title">系统总览</h1>
<!-- 统计卡片 -->
<div class="stats-grid">
<!-- 总用户 -->
<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>
</div>
</div>
<!-- 今日uv -->
<div class="stat-card">
<div class="stat-icon"><img src="@/assets/imgs/overview-uv.svg" alt="uv" /></div>
<div class="stat-content">
<h3>今日用户</h3>
<span>
{{ statistics.totalUv }}
<span>
{{ statistics.totalUvChange }} 较昨日
</span>
</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">
<span>访问统计趋势PV/UV</span>
<div style="display:flex; width:50%">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
/>
</div>
</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">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { ElRow, ElCol, ElCard, ElDatePicker } from 'element-plus';
import * as echarts from 'echarts';
import { systemOverviewApi } from '@/apis/system/overview';
import dayjs from 'dayjs';
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;
const statistics = ref({
totalUsers: 0,
totalUsersChange: '+0%',
totalResources: 0,
totalResourcesChange: '+0%',
totalPv: 0,
totalPvChange: '+0%',
totalUv: 0,
totalUvChange: '+0%'
});
onMounted(async () => {
// 默认选择最近 7 天
const now = new Date();
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
dateRange.value = [sevenDaysAgo, now];
initCharts();
await loadOverviewData();
});
onUnmounted(() => {
if (activityChartInstance) {
activityChartInstance.dispose();
}
if (pieChartInstance) {
pieChartInstance.dispose();
}
});
// 监听日期范围变化,重新加载活跃用户图表
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');
}
async function loadOverviewData() {
try {
const [statRes, activeRes, pieRes, todayRes] = await Promise.all([
systemOverviewApi.getStatistics(),
systemOverviewApi.getActiveUsersChart(formatDate(dateRange.value?.[0]!), formatDate(dateRange.value?.[1]!)),
systemOverviewApi.getResourceCategoryStats(),
systemOverviewApi.getTodayVisits()
]);
if (statRes.success && statRes.data) {
const d = statRes.data;
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%')
};
}
if (todayRes.success && todayRes.data) {
const t = todayRes.data;
// todayRes 仍用于“今日访问”区域(如后续需要),当前统计卡片只使用 statistics
}
if (activeRes.success && activeRes.data) {
updateActivityChart(activeRes.data.labels, activeRes.data.pvValues, activeRes.data.uvValues);
}
if (pieRes.success && pieRes.data) {
updatePieChart(pieRes.data.items);
}
} catch (error) {
console.error('加载系统总览数据失败:', error);
}
}
function initCharts() {
if (activityChart.value) {
activityChartInstance = echarts.init(activityChart.value);
}
if (resourcePieChart.value) {
pieChartInstance = echarts.init(resourcePieChart.value);
}
}
function updateActivityChart(labels: string[], pvValues: number[], uvValues: number[]) {
if (!activityChartInstance) return;
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
data: ['页面访问量 (PV)', '独立访客数 (UV)'],
top: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: labels
},
yAxis: {
type: 'value'
},
series: [
{
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,
type: 'line',
smooth: true,
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)' }
]
}
}
}
]
};
activityChartInstance.setOption(option);
}
function updatePieChart(items: { name: string; value: number }[]) {
if (!pieChartInstance) return;
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
type: 'pie',
radius: '50%',
data: items
}
]
};
pieChartInstance.setOption(option);
}
</script>
<style lang="scss" scoped>
.system-overview {
padding: 16px 0;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #141F38;
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: #ffffff;
padding: 20px 24px;
border-radius: 16px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 20%;
background: #f5f7ff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
img {
width: 24px;
height: 24px;
}
}
.stat-content {
flex: 1;
h3 {
font-size: 14px;
font-weight: 500;
color: #64748b;
margin-bottom: 6px;
}
> 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; // 默认上升为绿色
}
}
}
.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>