overview统计
This commit is contained in:
@@ -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; // 默认上升为绿色
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user