425 lines
12 KiB
Vue
425 lines
12 KiB
Vue
<template>
|
|
<div class="dashboard-page">
|
|
<!-- 统计卡片 -->
|
|
<el-row :gutter="20" class="stat-cards">
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="hover" class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea, #764ba2)">
|
|
<el-icon :size="28"><User /></el-icon>
|
|
</div>
|
|
<div class="stat-info">
|
|
<div class="stat-value">{{ statistics.totalUsers.toLocaleString() }}</div>
|
|
<div class="stat-label">总用户数</div>
|
|
<div class="stat-footer">
|
|
<span>今日新增 {{ statistics.newUsersToday }}</span>
|
|
<span :class="statistics.userGrowthRate >= 0 ? 'up' : 'down'">
|
|
{{ statistics.userGrowthRate >= 0 ? '+' : '' }}{{ statistics.userGrowthRate }}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="hover" class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb, #f5576c)">
|
|
<el-icon :size="28"><VideoCamera /></el-icon>
|
|
</div>
|
|
<div class="stat-info">
|
|
<div class="stat-value">{{ statistics.totalVideos.toLocaleString() }}</div>
|
|
<div class="stat-label">总视频数</div>
|
|
<div class="stat-footer">
|
|
<span>今日上传 {{ statistics.newVideosToday }}</span>
|
|
<span :class="statistics.videoGrowthRate >= 0 ? 'up' : 'down'">
|
|
{{ statistics.videoGrowthRate >= 0 ? '+' : '' }}{{ statistics.videoGrowthRate }}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="hover" class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe, #00f2fe)">
|
|
<el-icon :size="28"><View /></el-icon>
|
|
</div>
|
|
<div class="stat-info">
|
|
<div class="stat-value">{{ formatNumber(statistics.totalViews) }}</div>
|
|
<div class="stat-label">总播放量</div>
|
|
<div class="stat-footer">
|
|
<span>今日播放 {{ formatNumber(statistics.viewsToday) }}</span>
|
|
<span :class="statistics.viewGrowthRate >= 0 ? 'up' : 'down'">
|
|
{{ statistics.viewGrowthRate >= 0 ? '+' : '' }}{{ statistics.viewGrowthRate }}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="12" :sm="6">
|
|
<el-card shadow="hover" class="stat-card">
|
|
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a, #fee140)">
|
|
<el-icon :size="28"><DocumentChecked /></el-icon>
|
|
</div>
|
|
<div class="stat-info">
|
|
<div class="stat-value text-warning">{{ statistics.pendingReview }}</div>
|
|
<div class="stat-label">待审核视频</div>
|
|
<div class="stat-footer">
|
|
<el-button type="primary" size="small" text @click="goToReview">
|
|
去审核
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<!-- 图表区域 -->
|
|
<el-row :gutter="20" class="chart-row">
|
|
<el-col :xs="24" :lg="16">
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>数据趋势</span>
|
|
<el-radio-group v-model="chartType" size="small">
|
|
<el-radio-button label="user">用户</el-radio-button>
|
|
<el-radio-button label="video">视频</el-radio-button>
|
|
<el-radio-button label="view">播放</el-radio-button>
|
|
</el-radio-group>
|
|
</div>
|
|
</template>
|
|
<div ref="trendChartRef" class="chart-container"></div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="24" :lg="8">
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<span>互动数据</span>
|
|
</template>
|
|
<div ref="pieChartRef" class="chart-container"></div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<!-- 列表区域 -->
|
|
<el-row :gutter="20" class="list-row">
|
|
<el-col :xs="24" :lg="12">
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>热门视频</span>
|
|
<el-button type="primary" text @click="goToVideoList">查看更多</el-button>
|
|
</div>
|
|
</template>
|
|
<el-table :data="hotVideos" style="width: 100%">
|
|
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
|
<el-table-column prop="userNickname" label="作者" width="100" />
|
|
<el-table-column prop="viewCount" label="播放" width="80">
|
|
<template #default="{ row }">
|
|
{{ formatNumber(row.viewCount) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="likeCount" label="点赞" width="80">
|
|
<template #default="{ row }">
|
|
{{ formatNumber(row.likeCount) }}
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :xs="24" :lg="12">
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>最新评论</span>
|
|
<el-button type="primary" text @click="goToCommentList">查看更多</el-button>
|
|
</div>
|
|
</template>
|
|
<el-table :data="recentComments" style="width: 100%">
|
|
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
|
|
<el-table-column prop="userNickname" label="用户" width="100" />
|
|
<el-table-column prop="createTime" label="时间" width="120">
|
|
<template #default="{ row }">
|
|
{{ formatTime(row.createTime) }}
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import * as echarts from 'echarts'
|
|
import { statisticsApi } from '@/api/statistics.ts'
|
|
import { formatNumber, formatTime } from '@/utils/format.ts'
|
|
import type { StatisticsData, PlatformVideo, PlatformComment, TrendData } from '@/types'
|
|
|
|
const router = useRouter()
|
|
|
|
// 统计数据
|
|
const statistics = ref<StatisticsData>({
|
|
totalUsers: 0,
|
|
newUsersToday: 0,
|
|
activeUsersToday: 0,
|
|
userGrowthRate: 0,
|
|
totalVideos: 0,
|
|
newVideosToday: 0,
|
|
pendingReview: 0,
|
|
videoGrowthRate: 0,
|
|
totalViews: 0,
|
|
viewsToday: 0,
|
|
viewGrowthRate: 0,
|
|
totalLikes: 0,
|
|
totalComments: 0,
|
|
totalDanmakus: 0,
|
|
userTrend: [],
|
|
videoTrend: [],
|
|
viewTrend: []
|
|
})
|
|
|
|
// 列表数据
|
|
const hotVideos = ref<PlatformVideo[]>([])
|
|
const recentComments = ref<PlatformComment[]>([])
|
|
|
|
// 图表类型
|
|
const chartType = ref<'user' | 'video' | 'view'>('user')
|
|
|
|
// 图表实例
|
|
const trendChartRef = ref<HTMLElement>()
|
|
const pieChartRef = ref<HTMLElement>()
|
|
let trendChart: echarts.ECharts | null = null
|
|
let pieChart: echarts.ECharts | null = null
|
|
|
|
// 加载数据
|
|
const loadDashboardData = async () => {
|
|
try {
|
|
const data = await statisticsApi.getDashboard()
|
|
statistics.value = data.statistics
|
|
hotVideos.value = data.hotVideos
|
|
recentComments.value = data.recentComments
|
|
|
|
// 初始化图表
|
|
await nextTick()
|
|
initCharts()
|
|
} catch (error) {
|
|
console.error('加载仪表盘数据失败:', error)
|
|
}
|
|
}
|
|
|
|
// 初始化图表
|
|
const initCharts = () => {
|
|
// 趋势图表
|
|
if (trendChartRef.value) {
|
|
trendChart = echarts.init(trendChartRef.value)
|
|
updateTrendChart()
|
|
}
|
|
|
|
// 饼图
|
|
if (pieChartRef.value) {
|
|
pieChart = echarts.init(pieChartRef.value)
|
|
pieChart.setOption({
|
|
tooltip: {
|
|
trigger: 'item'
|
|
},
|
|
legend: {
|
|
bottom: 0
|
|
},
|
|
series: [
|
|
{
|
|
type: 'pie',
|
|
radius: ['40%', '70%'],
|
|
avoidLabelOverlap: false,
|
|
itemStyle: {
|
|
borderRadius: 10,
|
|
borderColor: '#fff',
|
|
borderWidth: 2
|
|
},
|
|
label: {
|
|
show: true,
|
|
formatter: '{b}: {c}'
|
|
},
|
|
data: [
|
|
{ value: statistics.value.totalLikes, name: '点赞', itemStyle: { color: '#f56c6c' } },
|
|
{ value: statistics.value.totalComments, name: '评论', itemStyle: { color: '#e6a23c' } },
|
|
{ value: statistics.value.totalDanmakus, name: '弹幕', itemStyle: { color: '#409eff' } }
|
|
]
|
|
}
|
|
]
|
|
})
|
|
}
|
|
}
|
|
|
|
// 更新趋势图表
|
|
const updateTrendChart = () => {
|
|
if (!trendChart) return
|
|
|
|
let data: TrendData[] = []
|
|
let title = ''
|
|
|
|
switch (chartType.value) {
|
|
case 'user':
|
|
data = statistics.value.userTrend
|
|
title = '用户增长趋势'
|
|
break
|
|
case 'video':
|
|
data = statistics.value.videoTrend
|
|
title = '视频上传趋势'
|
|
break
|
|
case 'view':
|
|
data = statistics.value.viewTrend
|
|
title = '播放量趋势'
|
|
break
|
|
}
|
|
|
|
trendChart.setOption({
|
|
title: {
|
|
text: title,
|
|
left: 'center',
|
|
textStyle: {
|
|
fontSize: 14
|
|
}
|
|
},
|
|
tooltip: {
|
|
trigger: 'axis'
|
|
},
|
|
grid: {
|
|
left: '3%',
|
|
right: '4%',
|
|
bottom: '3%',
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: data.map((item) => item.date)
|
|
},
|
|
yAxis: {
|
|
type: 'value'
|
|
},
|
|
series: [
|
|
{
|
|
name: title,
|
|
type: 'line',
|
|
smooth: true,
|
|
areaStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
|
|
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
|
|
])
|
|
},
|
|
lineStyle: {
|
|
color: '#409eff'
|
|
},
|
|
itemStyle: {
|
|
color: '#409eff'
|
|
},
|
|
data: data.map((item) => item.value)
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
// 监听图表类型变化
|
|
watch(chartType, () => {
|
|
updateTrendChart()
|
|
})
|
|
|
|
// 跳转方法
|
|
const goToReview = () => router.push('/video/review')
|
|
const goToVideoList = () => router.push('/video/list')
|
|
const goToCommentList = () => router.push('/content/comment')
|
|
|
|
// 窗口大小变化时重新渲染图表
|
|
const handleResize = () => {
|
|
trendChart?.resize()
|
|
pieChart?.resize()
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadDashboardData()
|
|
window.addEventListener('resize', handleResize)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', handleResize)
|
|
trendChart?.dispose()
|
|
pieChart?.dispose()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.dashboard-page {
|
|
.stat-cards {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stat-card {
|
|
:deep(.el-card__body) {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
}
|
|
|
|
.stat-info {
|
|
flex: 1;
|
|
|
|
.stat-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: #999;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.stat-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 8px;
|
|
font-size: 12px;
|
|
color: #666;
|
|
|
|
.up {
|
|
color: #67c23a;
|
|
}
|
|
|
|
.down {
|
|
color: #f56c6c;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.chart-row {
|
|
margin-bottom: 20px;
|
|
|
|
.chart-container {
|
|
height: 300px;
|
|
}
|
|
}
|
|
|
|
.list-row {
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
}
|
|
</style>
|