Files
HAOVP/frontend/video-frontend-admin/src/views/dashboard/index.vue
T

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>