feat: 优化 SongList 组件,增强歌曲管理功能

- 增加了歌曲搜索和筛选功能,支持按语言、标签和作者筛选。
- 改进了歌曲编辑和删除操作的用户体验,添加了确认提示。
- 更新了表格列定义,确保动态生成筛选选项。
- 优化了歌曲播放器的状态管理,增强了播放体验。
- 规范化了代码注释,提升了可读性和维护性。
This commit is contained in:
2025-04-20 14:32:10 +08:00
parent febfa132c8
commit 94a315a906
4 changed files with 1130 additions and 767 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ const props = defineProps<{
song: SongsInfo | undefined song: SongsInfo | undefined
isLrcLoading?: string isLrcLoading?: string
}>() }>()
const emits = defineEmits(['update:isLrcLoading']) const emits = defineEmits(['update:isLrcLoading', 'update:close', 'update:volume'])
const aplayerMusic = ref({ const aplayerMusic = ref({
title: '', title: '',

View File

@@ -5,6 +5,8 @@ import { onMounted, onUnmounted, ref } from 'vue'
const timer = ref<any>() const timer = ref<any>()
const visible = ref(true) const visible = ref(true)
const active = ref(true) const active = ref(true)
const originalBackgroundColor = ref('')
onMounted(() => { onMounted(() => {
timer.value = setInterval(() => { timer.value = setInterval(() => {
if (!visible.value || !active.value) return if (!visible.value || !active.value) return
@@ -22,9 +24,21 @@ onMounted(() => {
active.value = a active.value = a
} }
} }
// 使 .n-layout-content 背景透明
const layoutContent = document.querySelector('.n-layout-content');
if (layoutContent instanceof HTMLElement) {
originalBackgroundColor.value = layoutContent.style.backgroundColor
layoutContent.style.setProperty('background-color', 'transparent');
}
}) })
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer.value) clearInterval(timer.value)
// 还原 .n-layout-content 背景颜色
const layoutContent = document.querySelector('.n-layout-content');
if (layoutContent instanceof HTMLElement) {
layoutContent.style.setProperty('background-color', originalBackgroundColor.value);
}
}) })
</script> </script>
@@ -46,9 +60,3 @@ onUnmounted(() => {
</RouterView> </RouterView>
</div> </div>
</template> </template>
<style>
.body,html,.n-element,.n-layout-content {
background-color: transparent !important;
}
</style>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { isDarkMode } from '@/Utils' import { isDarkMode } from '@/Utils' // 引入暗黑模式判断工具
import { ThemeType } from '@/api/api-models' import { ThemeType } from '@/api/api-models' // 引入主题类型枚举
import { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient' import { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient' // 引入开放平台认证信息类型
import { useDanmakuClient } from '@/store/useDanmakuClient'; import { useDanmakuClient } from '@/store/useDanmakuClient'; // 引入弹幕客户端状态管理
import { Lottery24Filled, PeopleQueue24Filled, TabletSpeaker24Filled } from '@vicons/fluent' import { Lottery24Filled, PeopleQueue24Filled, TabletSpeaker24Filled } from '@vicons/fluent' // 引入 Fluent UI 图标
import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5' import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5' // 引入 Ionicons 图标
import { useElementSize, useStorage } from '@vueuse/core' import { useElementSize, useStorage } from '@vueuse/core' // 引入 VueUse 组合式函数
import { import {
NAlert, NAlert,
NAvatar, NAvatar,
@@ -27,21 +27,28 @@ import {
NTag, NTag,
NText, NText,
useMessage, useMessage,
} from 'naive-ui' } from 'naive-ui' // 引入 Naive UI 组件
import { h, onMounted, onUnmounted, ref } from 'vue' import { h, onMounted, onUnmounted, ref, computed } from 'vue' // 引入 Vue 相关 API
import { RouterLink, useRoute } from 'vue-router' import { RouterLink, useRoute, useRouter } from 'vue-router' // 引入 Vue Router 相关 API
const route = useRoute() // -- 基本状态和工具 --
const message = useMessage() const route = useRoute() // 获取当前路由信息
const themeType = useStorage('Settings.Theme', ThemeType.Auto) const router = useRouter() // 获取路由实例
const message = useMessage() // 获取 Naive UI 消息提示 API
const themeType = useStorage('Settings.Theme', ThemeType.Auto) // 使用 useStorage 持久化主题设置 (默认自动)
const danmakuClient = useDanmakuClient(); // 获取弹幕客户端实例
const sider = ref() // -- 侧边栏状态 --
const { width } = useElementSize(sider) const sider = ref<HTMLElement | null>(null) // 侧边栏 DOM 引用
const { width: siderWidth } = useElementSize(sider) // 实时获取侧边栏宽度
const authInfo = ref<AuthInfo>() // -- 认证与连接状态 --
const danmakuClient = await useDanmakuClient().initOpenlive(); const authInfo = ref<AuthInfo>() // 存储从路由查询参数获取的认证信息
const danmakuClientError = ref<string>() // 存储弹幕客户端初始化错误信息
const menuOptions = [ // -- 菜单配置 --
// 定义菜单项, 使用 h 函数渲染 RouterLink 以实现路由跳转
const menuOptions = computed(() => [ // 改为 computed 以便将来可能动态修改
{ {
label: () => label: () =>
h( h(
@@ -49,10 +56,10 @@ const menuOptions = [
{ {
to: { to: {
name: 'open-live-lottery', name: 'open-live-lottery',
query: route.query, query: route.query, // 保留查询参数
}, },
}, },
{ default: () => '抽奖' }, { default: () => '弹幕抽奖' },
), ),
key: 'open-live-lottery', key: 'open-live-lottery',
icon: renderIcon(Lottery24Filled), icon: renderIcon(Lottery24Filled),
@@ -67,7 +74,7 @@ const menuOptions = [
query: route.query, query: route.query,
}, },
}, },
{ default: () => '点歌' }, { default: () => '弹幕点歌' }, // 优化名称
), ),
key: 'open-live-live-request', key: 'open-live-live-request',
icon: renderIcon(MusicalNote), icon: renderIcon(MusicalNote),
@@ -82,7 +89,7 @@ const menuOptions = [
query: route.query, query: route.query,
}, },
}, },
{ default: () => '排队' }, { default: () => '弹幕排队' }, // 优化名称
), ),
key: 'open-live-queue', key: 'open-live-queue',
icon: renderIcon(PeopleQueue24Filled), icon: renderIcon(PeopleQueue24Filled),
@@ -97,37 +104,74 @@ const menuOptions = [
query: route.query, query: route.query,
}, },
}, },
{ default: () => '弹幕' }, { default: () => '弹幕朗读' }, // 优化名称
), ),
key: 'open-live-speech', key: 'open-live-speech',
icon: renderIcon(TabletSpeaker24Filled), icon: renderIcon(TabletSpeaker24Filled),
}, },
] ])
// -- 工具函数 --
/**
* 渲染 Naive UI 图标的辅助函数
* @param icon 图标组件
*/
function renderIcon(icon: unknown) { function renderIcon(icon: unknown) {
return () => h(NIcon, null, { default: () => h(icon as any) }) return () => h(NIcon, null, { default: () => h(icon as any) })
} }
const danmakuClientError = ref<string>()
// -- 主题切换逻辑 --
const isDarkValue = computed({
get: () => themeType.value === ThemeType.Dark || (themeType.value === ThemeType.Auto && isDarkMode.value),
set: (value) => {
themeType.value = value ? ThemeType.Dark : ThemeType.Light;
}
});
// -- 生命周期钩子 --
onMounted(async () => { onMounted(async () => {
// 1. 从路由查询参数解析认证信息
authInfo.value = route.query as unknown as AuthInfo authInfo.value = route.query as unknown as AuthInfo
// 2. 检查是否存在必要的 Code 参数
if (authInfo.value?.Code) { if (authInfo.value?.Code) {
danmakuClient.initOpenlive(authInfo.value) try {
// 3. 初始化开放平台弹幕客户端
await danmakuClient.initOpenlive(authInfo.value) // 改为 await 处理可能的异步初始化
// 可选: 初始化成功提示
// message.success('弹幕客户端连接中...')
} catch (error: any) {
// 4. 处理初始化错误
console.error("Danmaku client initialization failed:", error);
danmakuClientError.value = `弹幕客户端初始化失败: ${error.message || '未知错误'}`;
message.error(danmakuClientError.value);
}
} else { } else {
message.error('你不是从幻星平台访问此页面, 或未提供对应参数, 无法使用此功能') // 5. 如果缺少 Code, 显示错误信息
message.error('无效访问: 缺少必要的认证参数 (Code)。请通过幻星平台获取链接。')
// authInfo 清空, 触发 v-if 显示错误页
authInfo.value = undefined;
} }
}) })
onUnmounted(() => {
}) // onUnmounted 清理 (如果需要)
// onUnmounted(() => {
// danmakuClient.dispose(); // 示例: 如果有清理逻辑
// })
</script> </script>
<template> <template>
<!-- 情况一: 缺少认证信息, 显示错误提示页 -->
<NLayoutContent <NLayoutContent
v-if="!authInfo?.Code" v-if="!authInfo?.Code"
style="height: 100vh" style="height: 100vh; display: flex; align-items: center; justify-content: center;"
content-style="padding: 24px;"
> >
<NResult <NResult
status="error" status="error"
title="无效访问" title="无效访问"
description="请确保您是通过正确的幻星平台 H5 插件链接访问此页面。"
> >
<template #footer> <template #footer>
请前往 请前往
@@ -140,177 +184,288 @@ onUnmounted(() => {
> >
幻星平台 | VTsuru 幻星平台 | VTsuru
</NButton> </NButton>
并点击 获取 , 再点击 获取 H5 插件链接来获取可用链接 获取 H5 插件链接
<br>
或者直接在那个页面用也可以, 虽然并不推荐
</template> </template>
</NResult> </NResult>
</NLayoutContent> </NLayoutContent>
<!-- 情况二: 存在认证信息, 显示主布局 -->
<NLayout <NLayout
v-else v-else
style="height: 100vh" style="height: 100vh"
:native-scrollbar="false"
> >
<!-- 顶部导航栏 -->
<NLayoutHeader <NLayoutHeader
style="height: 45px; padding: 5px 15px 5px 15px" style="height: 60px; display: flex; align-items: center; padding: 0 20px;"
bordered bordered
> >
<NPageHeader :subtitle="($route.meta.title as string) ?? ''"> <!-- 使用 NPageHeader 增强语义和结构 -->
<template #extra> <NPageHeader style="width: 100%;">
<NSpace align="center"> <!-- 标题区域 -->
<NTag :type="danmakuClient.connected ? 'success' : 'warning'"> <template #title>
{{ danmakuClient.connected ? `已连接 | ${danmakuClient.authInfo?.anchor_info?.uname}` : '未连接' }} <NButton
</NTag> text
<NSwitch style="text-decoration: none;"
:default-value="!isDarkMode" @click="router.push({ name: 'open-live-index', query: route.query })"
@update:value="(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark) >
" <NText
strong
style="font-size: 1.5rem; line-height: 1;"
type="primary"
> >
<!-- 网站/应用 Logo 或名称 -->
<img
src="/favicon.ico"
alt="VTsuru Logo"
style="height: 24px; vertical-align: middle; margin-right: 8px;"
> <!-- 可选: 添加 Logo -->
VTsuru 开放平台
</NText>
</NButton>
</template>
<!-- 副标题/当前页面信息 -->
<template #subtitle>
<NText depth="3">
{{ $route.meta.title as string ?? '功能模块' }}
</NText>
</template>
<!-- 右侧额外操作区域 -->
<template #extra>
<NSpace
align="center"
:size="20"
>
<!-- 连接状态指示 -->
<NTag
:type="danmakuClient.connected ? 'success' : 'warning'"
round
size="small"
>
<template #icon>
<NIcon :component="danmakuClient.connected ? Sunny : Moon" /> <!-- 示例图标 -->
</template>
{{ danmakuClient.connected ? `已连接: ${danmakuClient.authInfo?.anchor_info?.uname ?? '主播'}` : '连接中...' }}
</NTag>
<!-- 主题切换开关 -->
<NSwitch v-model:value="isDarkValue">
<template #checked> <template #checked>
<NIcon :component="Sunny" /> <NIcon :component="Moon" />
</template> </template>
<template #unchecked> <template #unchecked>
<NIcon :component="Moon" /> <NIcon :component="Sunny" />
</template> </template>
</NSwitch> </NSwitch>
</NSpace> </NSpace>
</template> </template>
<template #title>
<NButton
text
tag="a"
@click="$router.push({ name: 'open-live-index', query: $route.query })"
>
<NText
strong
style="font-size: 1.4rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); text-justify: auto"
>
VTSURU | 开放平台
</NText>
</NButton>
</template>
</NPageHeader> </NPageHeader>
</NLayoutHeader> </NLayoutHeader>
<!-- 主体内容区域 (包含侧边栏和内容) -->
<NLayout <NLayout
has-sider has-sider
style="height: calc(100vh - 45px - 30px)" style="height: calc(100vh - 60px - 40px)"
> >
<!-- 左侧导航栏 -->
<NLayoutSider <NLayoutSider
ref="sider" ref="sider"
bordered bordered
show-trigger show-trigger="arrow-circle"
default-collapsed
collapse-mode="width" collapse-mode="width"
:collapsed-width="64" :collapsed-width="64"
:width="180" :width="200"
:native-scrollbar="false" :native-scrollbar="false"
style="height: 100%" style="height: 100%;"
default-collapsed
> >
<Transition> <!-- 主播信息区域 (优化样式和过渡) -->
<Transition name="fade">
<!-- 添加简单的淡入淡出效果 -->
<div <div
v-if="danmakuClient.authInfo" v-if="danmakuClient.authInfo?.anchor_info && siderWidth > 64"
style="margin-top: 8px" style="padding: 20px 10px; text-align: center;"
> >
<NSpace <NAvatar
vertical :src="danmakuClient.authInfo.anchor_info.uface"
justify="center" :img-props="{ referrerpolicy: 'no-referrer' }"
align="center" round
bordered
size="large"
:style="{
marginBottom: '10px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}"
/>
<!-- 限制最大宽度并居中 -->
<NEllipsis
style="max-width: 160px; margin: 0 auto; font-weight: bold;"
:tooltip="{ placement: 'bottom' }"
> >
<NAvatar {{ danmakuClient.authInfo.anchor_info.uname }}
:src="danmakuClient.authInfo?.anchor_info?.uface" </NEllipsis>
:img-props="{ referrerpolicy: 'no-referrer' }" </div>
round <!-- 折叠时显示小头像 (可选) -->
bordered <div
:style="{ v-else-if="danmakuClient.authInfo?.anchor_info && siderWidth <= 64"
boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)', style="padding: 15px 0; text-align: center;"
}" >
/> <NAvatar
<NEllipsis :src="danmakuClient.authInfo.anchor_info.uface"
v-if="width > 100" :img-props="{ referrerpolicy: 'no-referrer' }"
style="max-width: 100%" round
> size="medium"
<NText strong> :style="{ boxShadow: '0 1px 4px rgba(0, 0, 0, 0.1)' }"
{{ danmakuClient.authInfo?.anchor_info?.uname }} />
</NText>
</NEllipsis>
</NSpace>
</div> </div>
</Transition> </Transition>
<!-- 导航菜单 -->
<NMenu <NMenu
:default-value="$route.name?.toString()" :value="route.name?.toString()"
:collapsed-width="64" :collapsed-width="64"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:options="menuOptions" :options="menuOptions"
:indent="24"
style="margin-top: 10px;"
/> />
<NSpace justify="center">
<!-- 侧边栏底部提示/链接 (优化样式和显示逻辑) -->
<NSpace
v-if="siderWidth > 150"
vertical
align="center"
style="position: absolute; bottom: 20px; left: 0; right: 0; padding: 0 15px;"
>
<NText <NText
v-if="width > 150"
depth="3" depth="3"
style="font-size: 12px; text-align: center;"
> >
有更多功能建议请 遇到问题或有建议?
<NButton <NButton
text text
type="info" type="info"
@click="$router.push({ name: 'about' })" size="tiny"
@click="router.push({ name: 'about' })"
> >
反馈 点此反馈
</NButton> </NButton>
</NText> </NText>
</NSpace> </NSpace>
</NLayoutSider> </NLayoutSider>
<!-- 右侧主内容区域 -->
<NLayoutContent <NLayoutContent
style="height: 100%; padding: 10px" style="height: 100%;"
content-style="padding: 15px; height: 100%;"
:native-scrollbar="false" :native-scrollbar="false"
> >
<!-- 弹幕客户端错误提示 -->
<NAlert <NAlert
v-if="danmakuClientError" v-if="danmakuClientError"
type="error" type="error"
title="无法启动弹幕客户端" title="弹幕客户端错误"
closable
style="margin-bottom: 15px;"
@close="danmakuClientError = undefined"
> >
{{ danmakuClientError }} {{ danmakuClientError }}
</NAlert> </NAlert>
<RouterView
v-if="danmakuClient.authInfo" <!-- 路由视图: 根据认证状态显示不同内容 -->
v-slot="{ Component }" <RouterView v-slot="{ Component }">
> <!-- 情况一: 认证信息加载中或连接中 -->
<KeepAlive> <div
v-if="!danmakuClient.authInfo && !danmakuClientError"
style="display: flex; justify-content: center; align-items: center; height: 80%;"
>
<NSpin size="large">
<template #description>
正在加载主播信息并连接服务...
</template>
</NSpin>
</div>
<!-- 情况二: 加载/连接成功, 渲染对应页面 -->
<KeepAlive v-else-if="Component && danmakuClient.authInfo">
<component <component
:is="Component" :is="Component"
:key="route.fullPath"
:room-info="danmakuClient.authInfo" :room-info="danmakuClient.authInfo"
:code="authInfo.Code" :code="authInfo.Code"
/> />
</KeepAlive> </KeepAlive>
<!-- 情况三: 组件无法渲染或其他错误 (理论上不应发生, 但作为后备) -->
<NResult
v-else-if="!danmakuClientError"
status="warning"
title="页面加载失败"
description="无法加载当前功能模块,请尝试刷新或联系开发者。"
/>
</RouterView> </RouterView>
<template v-else>
{{ }} <!-- 返回顶部按钮 -->
<NAlert <NBackTop
type="info" :right="40"
title="正在请求弹幕客户端认证信息..." :bottom="60"
> />
<NSpin show />
</NAlert>
</template>
<NBackTop />
</NLayoutContent> </NLayoutContent>
</NLayout> </NLayout>
<!-- 底部信息栏 -->
<NLayoutFooter <NLayoutFooter
style="height: 30px" style="height: 40px; display: flex; align-items: center; justify-content: center; padding: 0 20px;"
bordered bordered
> >
<NSpace <NText
justify="center" depth="3"
align="center" style="font-size: 12px;"
style="height: 100%"
> >
© {{ new Date().getFullYear() }} <!-- 动态年份 -->
<NButton <NButton
text text
tag="a" tag="a"
href="/" href="https://vtsuru.live"
target="_blank" target="_blank"
type="info" type="primary"
style="margin-left: 5px;"
> >
vtsuru.live vtsuru.live
</NButton> </NButton>
</NSpace> - VTsuru 提供支持
</NText>
</NLayoutFooter> </NLayoutFooter>
</NLayout> </NLayout>
</template> </template>
<style scoped>
/* 添加过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.n-pageheader-wrapper {
width: 100% !important;
}
/* 优化 NPageHeader 在窄屏幕下的表现 (可选) */
@media (max-width: 768px) {
.n-page-header-wrapper {
padding: 0 10px !important; /* 减少内边距 */
}
.n-page-header__title {
font-size: 1.2rem !important; /* 缩小标题字号 */
}
}
/* 确保 NLayoutContent 的内边距生效 */
:deep(.n-layout-scroll-container) {
display: flex;
flex-direction: column;
}
</style>