feat: 迁移自定义功能至 1.5.9.9 版本

包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
2026-09-12 19:26:42 +08:00
parent 34f0679117
commit 1106aeea6c
161 changed files with 18963 additions and 27900 deletions
+218 -88
View File
@@ -17,91 +17,114 @@
<!-- 移动端侧边栏遮罩 -->
<div
v-if="sidebarOpen"
class="fixed inset-0 bg-bg-primary-60 backdrop-blur-sm z-40 lg:hidden transition-opacity duration-300"
class="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 lg:hidden transition-opacity duration-300"
@click="closeSidebar"
/>
<!-- 主内容区域 -->
<main
class="flex-1 flex flex-col h-screen overflow-hidden lg:ml-64 relative bg-bg-primary text-text-primary"
class="flex-1 flex flex-col h-screen overflow-hidden lg:ml-64 relative bg-[#09090b] text-zinc-100"
>
<header
class="h-16 shrink-0 flex items-center justify-between px-4 md:px-8 border-b border-border-secondary bg-bg-primary-60 backdrop-blur-xl z-30"
class="admin-main-header h-16 shrink-0 flex items-center justify-between px-4 md:px-8 border-b border-zinc-800 bg-zinc-950/60 backdrop-blur-xl z-30"
>
<div class="flex items-center gap-3">
<button
class="lg:hidden p-2 text-text-tertiary hover:bg-bg-tertiary rounded-lg transition-colors"
class="admin-mobile-menu-btn lg:hidden p-2 text-zinc-400 hover:bg-zinc-800 rounded-lg transition-colors"
@click="toggleSidebar"
>
<Menu :size="20" />
</button>
<h1 class="text-xl font-bold tracking-tight">{{ getPageTitle() }}</h1>
</div>
<div class="flex items-center gap-4">
<!-- 这里可以添加顶部操作按钮 -->
<div class="flex items-center gap-2">
<button
class="px-3 py-1.5 text-xs font-semibold rounded-lg border border-zinc-700 text-zinc-300 hover:bg-zinc-800 transition-colors"
@click="openOnboardingTutorial"
>
新手引导
</button>
</div>
</header>
<div
class="flex-1 custom-scrollbar p-4 md:p-8 overflow-y-auto"
ref="contentScrollRef"
class="admin-content-area flex-1 custom-scrollbar p-4 md:p-8 overflow-y-auto"
>
<!-- 移动端返回顶部按钮 -->
<button
v-if="showBackToTop"
:aria-label="locale.backToTop"
class="fixed bottom-8 right-8 p-3 bg-primary-hover text-text-primary rounded-full shadow-lg hover:bg-primary transition-all z-50"
aria-label="返回顶部"
class="back-to-top-btn fixed bottom-8 right-8 p-3 bg-blue-600 text-white rounded-full shadow-lg hover:bg-blue-700 transition-all z-50"
@click="scrollToTop"
>
<ChevronUp :size="24" />
</button>
<!-- 数据概览 -->
<div
v-if="activeTab === 'overview' && permissions.canAccessPage('overview')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminOverviewDashboard @navigate="handleNavigate" />
</div>
<div :key="`${activeTab}-${tabRefreshKey}`">
<!-- 数据概览 -->
<div
v-if="activeTab === 'overview' && permissions.canAccessPage('overview')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminOverviewDashboard @navigate="handleNavigate" />
</div>
<!-- 歌曲管理 -->
<div
v-if="activeTab === 'songs' && permissions.canAccessPage('songs')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminSongManagement />
</div>
<!-- 工作流教程 -->
<div
v-if="activeTab === 'workflow-tutorial' && permissions.canAccessPage('workflow-tutorial')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminWorkflowTutorial />
</div>
<!-- 排期管理 -->
<div
v-if="activeTab === 'schedule' && permissions.canAccessPage('schedule')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500 h-full"
>
<LazyAdminScheduleManager />
</div>
<!-- 歌曲管理 -->
<div
v-if="activeTab === 'songs' && permissions.canAccessPage('songs')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminSongManagement />
</div>
<!-- 打印排期 -->
<div
v-if="activeTab === 'print' && permissions.canAccessPage('print')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500 h-full"
>
<LazyAdminSchedulePrinter />
</div>
<!-- 排期管理 -->
<div
v-if="activeTab === 'schedule' && permissions.canAccessPage('schedule')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500 h-full"
>
<LazyAdminScheduleManager />
</div>
<!-- 数据分析 -->
<div
v-if="activeTab === 'data-analysis' && permissions.canAccessPage('data-analysis')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminDataAnalysisPanel />
</div>
<!-- 打印排期 -->
<div
v-if="activeTab === 'print' && permissions.canAccessPage('print')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500 h-full"
>
<LazyAdminSchedulePrinter />
</div>
<!-- 用户管理 -->
<div
v-if="activeTab === 'users' && permissions.canAccessPage('users')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminUserManager />
</div>
<!-- 歌单卡片图片 -->
<div
v-if="activeTab === 'playlist-card' && permissions.canAccessPage('print')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500 h-full"
>
<LazyAdminPlaylistCardPage />
</div>
<!-- 数据分析 -->
<div
v-if="activeTab === 'data-analysis' && permissions.canAccessPage('data-analysis')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminDataAnalysisPanel />
</div>
<!-- 用户管理 -->
<div
v-if="activeTab === 'users' && permissions.canAccessPage('users')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminUserManager />
</div>
<!-- 消息管理 -->
<div
@@ -151,14 +174,6 @@
<LazyAdminBlacklistManager />
</div>
<!-- 年级班级管理 -->
<div
v-if="activeTab === 'grade-class' && permissions.canAccessPage('grade-class')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminGradeClassManager />
</div>
<!-- 站点配置 -->
<div
v-if="activeTab === 'site-config' && permissions.canAccessPage('site-config')"
@@ -167,12 +182,20 @@
<LazyAdminSiteConfigManager />
</div>
<!-- 音源控制 -->
<!-- 更新日志 -->
<div
v-if="activeTab === 'music-source' && permissions.canAccessPage('music-source')"
v-if="activeTab === 'changelog' && permissions.canAccessPage('changelog')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminMusicSourceController />
<LazyAdminChangelogManager />
</div>
<!-- 选曲日期管理 -->
<div
v-if="activeTab === 'expected-play-dates' && permissions.canAccessPage('settings')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500 h-full"
>
<LazyAdminExpectedPlayDates />
</div>
<!-- 数据库操作 -->
@@ -183,6 +206,14 @@
<LazyAdminDatabaseManager />
</div>
<!-- 权限组管理 -->
<div
v-if="activeTab === 'roles' && permissions.canAccessPage('roles')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminRolesManager />
</div>
<!-- API密钥管理 -->
<div
v-if="activeTab === 'api-keys' && permissions.canAccessPage('api-keys')"
@@ -190,14 +221,15 @@
>
<LazyAdminApiKeyManager />
</div>
<!-- 卡密管理 -->
<div
v-if="activeTab === 'card-codes' && permissions.canAccessPage('card-codes')"
class="animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<LazyAdminCardCodesManager />
</div>
</div>
</div>
<AdminOnboardingTutorial
:visible="showOnboardingTutorial"
@navigate="handleNavigate"
@close="closeOnboardingTutorial"
@complete="completeOnboardingTutorial"
/>
</main>
</div>
</ClientOnly>
@@ -209,14 +241,12 @@ import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { Menu, ChevronUp } from '@lucide/vue'
import { useAuth } from '~/composables/useAuth'
import { usePermissions } from '~/composables/usePermissions'
import { fetchRolesConfig } from '~/composables/usePermissions'
import { useSiteConfig } from '~/composables/useSiteConfig'
import { useLocale } from '~/utils/locale'
import { useScrollMemory } from '~/composables/useScrollMemory'
import AdminOnboardingTutorial from '~/components/Admin/AdminOnboardingTutorial.vue'
// 使用站点配置
const { siteTitle, initSiteConfig } = useSiteConfig()
const { pages } = useLocale()
const locale = computed(() => pages.value?.dashboard || {})
// 导入组件
@@ -227,14 +257,16 @@ definePageMeta({
// 响应式数据
const activeTab = ref('overview')
// 各面板滚动位置独立记忆与恢复
useScrollMemory(() => activeTab.value)
const currentUser = ref(null)
const sidebarOpen = ref(false)
const showBackToTop = ref(false)
const showOnboardingTutorial = ref(false)
const contentScrollRef = ref(null)
const beforeNavigateHooks = ref([])
const tabRefreshKey = ref(0)
let revisionInterval = null
let lastDataRevision = ''
// 提供注册导航拦截钩子的方法
const registerBeforeNavigate = (hook) => {
@@ -252,9 +284,57 @@ provide('registerBeforeNavigate', registerBeforeNavigate)
let auth = null
const permissions = usePermissions()
const fetchDataRevision = async () => {
try {
if (!auth || !auth.isAuthenticated.value) return ''
const authConfig = auth.getAuthConfig()
const result = await $fetch('/api/songs/revision', {
...authConfig,
query: { t: Date.now() }
})
return result?.revision || ''
} catch (e) {
console.error('获取数据修订号失败:', e)
return ''
}
}
const startRevisionPolling = async () => {
if (revisionInterval) clearInterval(revisionInterval)
lastDataRevision = await fetchDataRevision()
revisionInterval = setInterval(async () => {
const next = await fetchDataRevision()
if (!next || next === lastDataRevision) return
lastDataRevision = next
tabRefreshKey.value++
}, 5000)
}
// 方法
const getPageTitle = () => {
return locale.value.tabs[activeTab.value] || locale.value.fallbackTitle
const titles = {
overview: '数据概览',
'workflow-tutorial': '流程与教程',
'data-analysis': '数据分析',
songs: '歌曲管理',
schedule: '排期管理',
print: '打印排期',
'playlist-card': '图片歌单',
users: '用户管理',
blacklist: '黑名单管理',
'api-keys': 'API密钥管理',
notifications: '通知管理',
'smtp-config': '邮件配置',
playtimes: '播出时段',
'request-times': '投稿管理',
semesters: '学期管理',
'expected-play-dates': '选曲日期',
'site-config': '站点配置',
changelog: '更新日志',
database: '数据库操作',
password: '修改密码'
}
return titles[activeTab.value] || '管理后台'
}
// 动态页面标题
@@ -263,7 +343,7 @@ const dynamicTitle = computed(() => {
if (siteTitle && siteTitle.value) {
return `${currentPageTitle} | ${siteTitle.value}`
}
return `${currentPageTitle} | ${locale.value.defaultSiteTitle}`
return `${currentPageTitle} | 校园广播站点歌系统`
})
// 监听activeTab变化,更新页面标题
@@ -288,7 +368,13 @@ watch(
)
const getRoleDisplayName = (role) => {
return locale.value.roles[role] || role
const roleNames = {
USER: '普通用户',
SONG_ADMIN: '歌曲管理员',
ADMIN: '超级管理员',
SUPER_ADMIN: '超级管理员'
}
return roleNames[role] || role
}
const handleLogout = async () => {
@@ -323,6 +409,21 @@ const closeSidebar = () => {
sidebarOpen.value = false
}
const openOnboardingTutorial = () => {
showOnboardingTutorial.value = true
}
const closeOnboardingTutorial = () => {
showOnboardingTutorial.value = false
}
const completeOnboardingTutorial = () => {
showOnboardingTutorial.value = false
if (typeof window !== 'undefined') {
localStorage.setItem('voicehub_admin_tutorial_seen_v1', 'true')
}
}
// 监听窗口大小变化,大屏幕时自动关闭移动端侧边栏
const handleResize = () => {
if (window.innerWidth > 768) {
@@ -332,12 +433,14 @@ const handleResize = () => {
// 返回顶部功能
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
if (contentScrollRef.value) {
contentScrollRef.value.scrollTo({ top: 0, behavior: 'smooth' })
}
}
// 监听滚动事件
const handleScroll = () => {
showBackToTop.value = window.scrollY > 300
showBackToTop.value = (contentScrollRef.value?.scrollTop || 0) > 300
}
// 触摸手势支持
@@ -384,6 +487,7 @@ onMounted(async () => {
// 初始化服务
auth = useAuth()
await fetchRolesConfig()
// 检查认证状态(plugin已经初始化过了)
@@ -413,7 +517,8 @@ onMounted(async () => {
// 添加窗口大小监听器
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
contentScrollRef.value?.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
// 添加双击关闭侧边栏事件
document.addEventListener('dblclick', (e) => {
@@ -421,12 +526,21 @@ onMounted(async () => {
closeSidebar()
}
})
if (!localStorage.getItem('voicehub_admin_tutorial_seen_v1')) {
setTimeout(() => {
showOnboardingTutorial.value = true
}, 800)
}
await startRevisionPolling()
})
// 组件卸载时清理事件监听器
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
window.removeEventListener('scroll', handleScroll)
contentScrollRef.value?.removeEventListener('scroll', handleScroll)
if (revisionInterval) clearInterval(revisionInterval)
})
</script>
@@ -434,8 +548,8 @@ onUnmounted(() => {
.admin-layout {
display: flex;
min-height: 100vh;
background: var(--panel-bg-deepest);
color: var(--text-primary);
background: var(--bg-primary, #ffffff);
color: var(--text-primary, #1a2332);
position: relative;
}
@@ -447,10 +561,26 @@ onUnmounted(() => {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: var(--panel-bg-alt);
background: var(--border-primary, #cbd5e1);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--panel-bg-hover);
background: var(--border-secondary, #94a3b8);
}
@media (max-width: 768px) {
.admin-main-header {
padding-top: max(0px, env(safe-area-inset-top));
}
.admin-content-area {
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
}
.back-to-top-btn {
right: 1rem;
bottom: calc(1rem + env(safe-area-inset-bottom));
padding: 0.65rem;
}
}
</style>