overview统计

This commit is contained in:
2025-11-14 18:31:39 +08:00
parent 6be3cc6abd
commit 9adc0c2058
24 changed files with 723 additions and 178 deletions

View File

@@ -4,15 +4,55 @@
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card" v-for="stat in statistics" :key="stat.label">
<div class="stat-icon" :style="{ background: stat.color }">
<i>{{ stat.icon }}</i>
</div>
<!-- 总用户 -->
<div class="stat-card">
<div class="stat-icon"><img src="@/assets/imgs/overview-user.svg" alt="用户" /></div>
<div class="stat-content">
<h3>{{ stat.value }}</h3>
<p>{{ stat.label }}</p>
<span class="stat-change" :class="stat.trend">
{{ stat.change }}
<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>
@@ -24,7 +64,7 @@
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>用户活跃度折线图</span>
<span>访问统计趋势PV/UV</span>
<el-date-picker
v-model="dateRange"
type="daterange"
@@ -49,26 +89,15 @@
</el-col>
</el-row>
<!-- 今日访问量详情 -->
<el-card class="visit-card">
<template #header>
<span>今日访问量</span>
</template>
<div class="visit-stats">
<div class="visit-item" v-for="item in visitStats" :key="item.label">
<div class="visit-label">{{ item.label }}</div>
<div class="visit-value">{{ item.value }}</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
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);
@@ -76,18 +105,23 @@ const resourcePieChart = ref<HTMLElement | null>(null);
let activityChartInstance: echarts.ECharts | null = null;
let pieChartInstance: echarts.ECharts | null = null;
const statistics = ref<{
icon: string;
label: string;
value: string | number;
change: string;
trend: 'up' | 'down';
color: string;
}[]>([]);
const visitStats = ref<{ label: string; value: string | number }[]>([]);
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();
});
@@ -101,65 +135,55 @@ onUnmounted(() => {
}
});
// 监听日期范围变化,重新加载活跃用户图表
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('2025-10-15', '2025-10-21'),
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 = [
{
icon: '👥',
label: '总用户数',
value: d.totalUsers,
change: d.totalUsersChange,
trend: d.totalUsersChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
icon: '📚',
label: '总资源数',
value: d.totalResources,
change: d.totalResourcesChange,
trend: d.totalResourcesChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
icon: '👁',
label: '今日访问量',
value: d.todayVisits,
change: d.todayVisitsChange,
trend: d.todayVisitsChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
},
{
icon: '✅',
label: '活跃用户',
value: d.activeUsers,
change: d.activeUsersChange,
trend: d.activeUsersChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'
}
];
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;
visitStats.value = [
{ label: 'UV(独立访客)', value: t.uv },
{ label: 'PV(页面浏览量)', value: t.pv },
{ label: '平均访问时长', value: t.avgVisitDuration },
{ label: '跳出率', value: t.bounceRate }
];
// todayRes 仍用于“今日访问”区域(如后续需要),当前统计卡片只使用 statistics
}
if (activeRes.success && activeRes.data) {
updateActivityChart(activeRes.data.labels, activeRes.data.values);
updateActivityChart(activeRes.data.labels, activeRes.data.pvValues, activeRes.data.uvValues);
}
if (pieRes.success && pieRes.data) {
@@ -180,14 +204,28 @@ function initCharts() {
}
}
function updateActivityChart(labels: string[], values: number[]) {
function updateActivityChart(labels: string[], pvValues: number[], uvValues: number[]) {
if (!activityChartInstance) return;
const option = {
tooltip: {
trigger: 'axis'
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: {
@@ -195,10 +233,48 @@ function updateActivityChart(labels: string[], values: number[]) {
},
series: [
{
data: values,
name: '页面访问量 (PV)',
data: pvValues,
type: 'line',
smooth: true,
areaStyle: {}
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)' }
]
}
}
}
]
};
@@ -229,6 +305,7 @@ function updatePieChart(items: { name: string; value: number }[]) {
<style lang="scss" scoped>
.system-overview {
padding: 16px 0;
}
.page-title {
@@ -241,56 +318,59 @@ function updatePieChart(items: { name: string; value: number }[]) {
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 20px;
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 8px;
background: #ffffff;
padding: 20px 24px;
border-radius: 16px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.stat-icon {
width: 64px;
height: 64px;
border-radius: 12px;
width: 48px;
height: 48px;
border-radius: 20%;
background: #f5f7ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
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: #141F38;
margin-bottom: 4px;
}
p {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
}
color: #0f172a;
.stat-change {
font-size: 13px;
&.up {
color: #4caf50;
}
&.down {
color: #f44336;
> span {
font-size: 12px;
font-weight: 400;
color: #16a34a; // 默认上升为绿色
}
}
}