feat(admin): 增加工作台大屏和转码通知

This commit is contained in:
Zhou Weidong committed 2026-10-24 17:31:16 +08:00
1 parent 2b2c6bd708
commit a266d44483
10 files changed
+860 -172

No files matched your search

+93 -66
View File
@@ -5,6 +5,8 @@ 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'
@@ -15,6 +17,8 @@ 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)
@@ -25,6 +29,12 @@ const reviewingCount = computed(
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 || '')))
@@ -34,10 +44,16 @@ const recentDramas = computed(() =>
onMounted(async () => {
loading.value = true
try {
const res = await pageDramasApi({ pageIndex: 1, pageSize: 200 })
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
}
@@ -45,72 +61,83 @@ onMounted(async () => {
</script>
<template>
<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
>
<div v-loading="loading" class="stats-grid">
<div class="stat-card"
><div class="stat-top"
><span>短剧总数</span
><span class="stat-icon"><AppIcon :icon="AppIcons.content" /></span></div
><div class="stat-value">{{ total }} <small>部</small></div></div
>
<div class="stat-card"
><div class="stat-top"
><span>累计播放量</span
><span class="stat-icon"><AppIcon :icon="AppIcons.videoPlay" /></span></div
><div class="stat-value">{{ formatCount(totalViews) }}</div></div
>
<div class="stat-card"
><div class="stat-top"
><span>待审核内容</span
><span class="stat-icon"><AppIcon :icon="AppIcons.clock" /></span></div
><div class="stat-value">{{ reviewingCount }} <small>项</small></div></div
>
<div class="stat-card"
><div class="stat-top"
><span>已上线</span
><span class="stat-icon"><AppIcon :icon="AppIcons.checkCircle" /></span></div
><div class="stat-value">{{ publishedCount }} <small>部</small></div></div
>
</div>
<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 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
>
<el-table
<DashboardMetricWall
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>
: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>
+100
View File
@@ -0,0 +1,100 @@
<!-- Author: fruit.com.cn -->
<!-- 转码通知页:按转码中、成功、失败查看短剧和集数。 -->
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import PageHeading from '@/views/components/PageHeading.vue'
import {
getTranscodeNoticesApi,
type TranscodeNotice,
type TranscodeNoticeItem
} from '@/api/admin/transcodeNotice'
import { formatDateTime } from '@/utils/format'
import { markTranscodeNoticeSeen } from '@/utils/transcodeNoticeSeen'
const loading = ref(false)
const tab = ref('RUNNING')
const notice = ref<TranscodeNotice | null>(null)
const rows = computed<TranscodeNoticeItem[]>(() => {
if (tab.value === 'SUCCESS') return notice.value?.successList || []
if (tab.value === 'FAILED') return notice.value?.failedList || []
return notice.value?.runningList || []
})
function episodeText(row: TranscodeNoticeItem) {
const no = row.episodeNo ? `第${row.episodeNo}集` : ''
if (!row.episodeTitle || row.episodeTitle === no) return no || '集数未知'
return no ? `${no} ${row.episodeTitle}` : row.episodeTitle
}
async function load() {
loading.value = true
try {
const res = await getTranscodeNoticesApi()
notice.value = res.data || null
if (notice.value) {
markTranscodeNoticeSeen({
running: Number(notice.value.running || 0),
success: Number(notice.value.success || 0),
failed: Number(notice.value.failed || 0)
})
}
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<PageHeading
eyebrow="通知"
title="转码通知"
description="查看转码中、转码成功和转码失败的短剧与集数。"
>
<el-button :loading="loading" @click="load">刷新</el-button>
</PageHeading>
<section class="panel notice-panel">
<el-tabs v-model="tab">
<el-tab-pane label="转码中" name="RUNNING" />
<el-tab-pane label="转码成功" name="SUCCESS" />
<el-tab-pane label="转码失败" name="FAILED" />
</el-tabs>
<el-table
v-loading="loading"
:data="rows"
row-key="id"
border
class="drama-el-table"
empty-text="暂无通知"
>
<el-table-column prop="dramaTitle" label="短剧" min-width="220" show-overflow-tooltip />
<el-table-column label="集数" min-width="180" show-overflow-tooltip>
<template #default="{ row }">{{ episodeText(row) }}</template>
</el-table-column>
<el-table-column label="进度" width="100">
<template #default="{ row }">{{ row.progress ?? 0 }}%</template>
</el-table-column>
<el-table-column
v-if="tab === 'FAILED'"
prop="errorMsg"
label="失败原因"
min-width="220"
show-overflow-tooltip
/>
<el-table-column label="更新时间" width="180">
<template #default="{ row }">{{ formatDateTime(row.updatedAt) }}</template>
</el-table-column>
</el-table>
</section>
</template>
<style scoped>
.notice-panel {
padding: 8px 16px 16px;
}
</style>