refactor(project): Reorganize frontend and backend directories
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user