Files
ReelShortWeb/src/views/common/DashboardView.vue
T

144 lines
4.2 KiB
Vue

<!-- Author: fruit.com.cn -->
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { AppIcons } from '@/constants/icons'
import { pageDramasApi } from '@/api/admin/drama'
import { getDashboardOverviewApi, getDashboardTrendApi } from '@/api/admin/dashboard'
import DashboardMetricWall from '@/views/dashboard/DashboardMetricWall.vue'
import { useRbac } from '@/stores/rbac'
import { formatCount } from '@/utils/format'
import { statusClass } from '@/utils/status'
const router = useRouter()
const { can } = useRbac()
const loading = ref(false)
const dramas = ref([])
const total = ref(0)
const overview = ref(null)
const trend = ref(null)
const totalViews = computed(() =>
dramas.value.reduce((sum, item) => sum + Number(item.views || 0), 0)
)
const reviewingCount = computed(
() => dramas.value.filter((item) => item.status === 'reviewing').length
)
const publishedCount = computed(
() => dramas.value.filter((item) => item.status === 'published').length
)
const metricFallback = computed(() => ({
'content.dramaTotal': total.value,
'content.dramaReviewing': reviewingCount.value,
'content.dramaPublished': publishedCount.value,
'playback.viewTotal': totalViews.value
}))
const recentDramas = computed(() =>
[...dramas.value]
.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')))
.slice(0, 5)
)
onMounted(async () => {
loading.value = true
try {
const [res, board, trendRes] = await Promise.all([
pageDramasApi({ pageIndex: 1, pageSize: 200 }),
getDashboardOverviewApi().catch(() => null),
getDashboardTrendApi().catch(() => null)
])
// 接口失败时 axios 拦截器会把业务错误吞成 undefined,这里不能让页面跟着崩
dramas.value = res?.data?.list || []
total.value = res?.data?.total || 0
overview.value = board?.data || null
trend.value = trendRes?.data || null
} finally {
loading.value = false
}
})
</script>
<template>
<div class="workbench">
<div class="page-heading"
><div
><div class="eyebrow">内容概览</div><h1>工作台</h1
><p>用户、设备、留存与短剧内容总览。</p></div
><el-button
v-if="can('drama:create')"
type="primary"
class="primary-button"
@click="router.push({ name: 'dramas' })"
><AppIcon :icon="AppIcons.add" />上传新短剧</el-button
></div
>
<DashboardMetricWall
v-loading="loading"
:overview="overview"
:trend="trend"
:fallback="metricFallback"
/>
<section class="panel recent-panel"
><div class="panel-heading"
><div><h2>最近更新</h2><p>短剧内容与发布状态</p></div
><el-button text class="view-all" @click="router.push({ name: 'dramas' })"
>短剧管理</el-button
></div
>
<el-table
v-loading="loading"
:data="recentDramas"
row-key="id"
empty-text="暂无短剧数据"
class="dashboard-el-table"
><el-table-column prop="title" label="短剧名称" min-width="280" /><el-table-column
prop="genre"
label="类型"
width="150" /><el-table-column label="集数" width="120"
><template #default="scope">{{ scope?.row?.episodes }} 集</template></el-table-column
><el-table-column label="播放量" width="140"
><template #default="scope">{{
formatCount(scope?.row?.views)
}}</template></el-table-column
><el-table-column label="状态" width="130"
><template #default="scope"
><el-tag
v-if="scope?.row"
:class="['status-badge', statusClass(scope.row.status)]"
effect="light"
>{{ scope.row.statusText }}</el-tag
></template
></el-table-column
><el-table-column prop="updatedAt" label="更新时间" width="180"
/></el-table>
</section>
</div>
</template>
<style scoped>
.workbench {
display: flex;
flex-direction: column;
min-width: 0;
}
.workbench .page-heading {
flex: none;
margin-bottom: 12px;
}
.workbench :deep(.screen) {
flex: 1;
min-height: 0;
}
.recent-panel {
flex: none;
max-height: 220px;
margin-top: 12px;
overflow: auto;
}
</style>