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>
|
|
|
|
|
|
|