431 lines
9.8 KiB
Vue
431 lines
9.8 KiB
Vue
<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>
|
||
|