From a266d444832db4121a21db30f3a7ec33996c9656 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Sat, 24 Oct 2026 17:31:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=A2=9E=E5=8A=A0=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E5=A4=A7=E5=B1=8F=E5=92=8C=E8=BD=AC=E7=A0=81?= =?UTF-8?q?=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/transcodeNotice.ts | 27 ++ src/layout/components/ToolHeader.vue | 4 +- .../components/TranscodeNoticeButton.vue | 89 ++++ src/permission.ts | 7 +- src/router/index.ts | 38 +- src/utils/transcodeNoticeSeen.ts | 51 ++ src/views/common/DashboardView.vue | 159 ++++--- src/views/common/TranscodeNoticeView.vue | 100 ++++ src/views/dashboard/BusinessDashboardView.vue | 112 +---- src/views/dashboard/DashboardMetricWall.vue | 445 ++++++++++++++++++ 10 files changed, 860 insertions(+), 172 deletions(-) create mode 100644 src/api/admin/transcodeNotice.ts create mode 100644 src/layout/components/TranscodeNoticeButton.vue create mode 100644 src/utils/transcodeNoticeSeen.ts create mode 100644 src/views/common/TranscodeNoticeView.vue create mode 100644 src/views/dashboard/DashboardMetricWall.vue diff --git a/src/api/admin/transcodeNotice.ts b/src/api/admin/transcodeNotice.ts new file mode 100644 index 0000000..423d6ae --- /dev/null +++ b/src/api/admin/transcodeNotice.ts @@ -0,0 +1,27 @@ +// Author: fruit.com.cn + +import request from '@/axios' + +export interface TranscodeNoticeItem { + id: number + status: string + dramaTitle: string + episodeNo?: number + episodeTitle?: string + progress?: number + errorMsg?: string + updatedAt?: string +} + +export interface TranscodeNotice { + running: number + success: number + failed: number + runningList: TranscodeNoticeItem[] + successList: TranscodeNoticeItem[] + failedList: TranscodeNoticeItem[] +} + +export const getTranscodeNoticesApi = () => { + return request.get({ url: '/admin/notifications/transcode' }) +} diff --git a/src/layout/components/ToolHeader.vue b/src/layout/components/ToolHeader.vue index 44ee8c8..eb17ab1 100644 --- a/src/layout/components/ToolHeader.vue +++ b/src/layout/components/ToolHeader.vue @@ -6,6 +6,7 @@ import { Collapse } from '@/components/Collapse' import { LocaleDropdown } from '@/components/LocaleDropdown' import { SizeDropdown } from '@/components/SizeDropdown' import { UserInfo } from '@/components/UserInfo' +import TranscodeNoticeButton from './TranscodeNoticeButton.vue' import { Screenfull } from '@/components/Screenfull' import { Breadcrumb } from '@/components/Breadcrumb' import { useAppStore } from '@/store/modules/app' @@ -73,6 +74,7 @@ export default defineComponent({ color="var(--top-header-text-color)" > ) : undefined} + @@ -95,7 +97,7 @@ export default defineComponent({ } } -@media (max-width: 768px) { +@media (width <= 768px) { .@{prefix-cls} { padding-right: 8px; padding-left: 8px; diff --git a/src/layout/components/TranscodeNoticeButton.vue b/src/layout/components/TranscodeNoticeButton.vue new file mode 100644 index 0000000..6bf6944 --- /dev/null +++ b/src/layout/components/TranscodeNoticeButton.vue @@ -0,0 +1,89 @@ + + + + + + + + diff --git a/src/permission.ts b/src/permission.ts index dc61a1a..557b305 100644 --- a/src/permission.ts +++ b/src/permission.ts @@ -96,7 +96,12 @@ router.beforeEach(async (to, from, next) => { const redirectPath = from.query.redirect || to.path const redirect = decodeURIComponent(redirectPath as string) - const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect } + // 只按路径重进。刷新时动态路由还没注册,先被兜底路由接住; + // 若把当时匹配到的 name 带回去,会一直停在 404。 + const nextData = + to.path === redirect + ? { path: to.path, query: to.query, hash: to.hash, replace: true } + : { path: redirect, replace: true } permissionStore.setIsAddRouters(true) next(nextData) } diff --git a/src/router/index.ts b/src/router/index.ts index 123ec04..a6f450a 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -52,6 +52,35 @@ export const constantRouterMap: AppRouteRecordRaw[] = [ title: '404', noTagsView: true } + }, + { + path: '/notifications', + component: Layout, + name: 'Notifications', + meta: { + hidden: true, + title: '通知' + }, + children: [ + { + path: 'transcode', + name: 'transcode-notices', + component: () => import('@/views/common/TranscodeNoticeView.vue'), + meta: { + title: '转码通知' + } + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'UnresolvedPath', + component: () => import('@/views/Error/404.vue'), + meta: { + hidden: true, + title: '404', + noTagsView: true + } } ] @@ -232,7 +261,14 @@ const router = createRouter({ }) export const resetRouter = (): void => { - const resetWhiteNameList = ['Redirect', 'RedirectParent', 'Login', 'NoFind', 'Root'] + const resetWhiteNameList = [ + 'Redirect', + 'RedirectParent', + 'Login', + 'NoFind', + 'Root', + 'UnresolvedPath' + ] router.getRoutes().forEach((route) => { const { name } = route if (name && !resetWhiteNameList.includes(name as string)) { diff --git a/src/utils/transcodeNoticeSeen.ts b/src/utils/transcodeNoticeSeen.ts new file mode 100644 index 0000000..ea6b829 --- /dev/null +++ b/src/utils/transcodeNoticeSeen.ts @@ -0,0 +1,51 @@ +// Author: fruit.com.cn + +const SEEN_KEY = 'transcode-notice-seen' + +export const TRANSCODE_NOTICE_SEEN = 'transcode-notice-seen' + +export interface NoticeCounts { + running: number + success: number + failed: number +} + +/** 上次打开通知页时已经看过的数量。 */ +export function readSeen(): NoticeCounts { + try { + const raw = localStorage.getItem(SEEN_KEY) + if (!raw) return { running: 0, success: 0, failed: 0 } + const data = JSON.parse(raw) as NoticeCounts + return { + running: Number(data.running || 0), + success: Number(data.success || 0), + failed: Number(data.failed || 0) + } + } catch { + return { running: 0, success: 0, failed: 0 } + } +} + +/** 比上次查看多出来的数量,看过的不再计入角标。 */ +export function unreadCounts(current: NoticeCounts): NoticeCounts { + const seen = readSeen() + return { + running: Math.max(0, current.running - seen.running), + success: Math.max(0, current.success - seen.success), + failed: Math.max(0, current.failed - seen.failed) + } +} + +/** 把当前数量记为已读,并通知顶栏收起角标。 */ +export function markTranscodeNoticeSeen(current: NoticeCounts) { + const seen = readSeen() + localStorage.setItem( + SEEN_KEY, + JSON.stringify({ + running: Math.max(seen.running, current.running), + success: Math.max(seen.success, current.success), + failed: Math.max(seen.failed, current.failed) + }) + ) + window.dispatchEvent(new Event(TRANSCODE_NOTICE_SEEN)) +} diff --git a/src/views/common/DashboardView.vue b/src/views/common/DashboardView.vue index c7e4bfa..43feaae 100644 --- a/src/views/common/DashboardView.vue +++ b/src/views/common/DashboardView.vue @@ -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 () => { + + diff --git a/src/views/common/TranscodeNoticeView.vue b/src/views/common/TranscodeNoticeView.vue new file mode 100644 index 0000000..1ba7fbf --- /dev/null +++ b/src/views/common/TranscodeNoticeView.vue @@ -0,0 +1,100 @@ + + + + + + + + diff --git a/src/views/dashboard/BusinessDashboardView.vue b/src/views/dashboard/BusinessDashboardView.vue index caa645d..1b2f9e9 100644 --- a/src/views/dashboard/BusinessDashboardView.vue +++ b/src/views/dashboard/BusinessDashboardView.vue @@ -1,8 +1,9 @@ + + + +