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

View File

@@ -5,6 +5,8 @@ import { onMounted, onUnmounted, ref } from 'vue'
const timer = ref<any>()
const visible = ref(true)
const active = ref(true)
const originalBackgroundColor = ref('')
onMounted(() => {
timer.value = setInterval(() => {
if (!visible.value || !active.value) return
@@ -22,9 +24,21 @@ onMounted(() => {
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(() => {
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>
@@ -46,9 +60,3 @@ onUnmounted(() => {
</RouterView>
</div>
</template>
<style>
.body,html,.n-element,.n-layout-content {
background-color: transparent !important;
}
</style>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import { isDarkMode } from '@/Utils'
import { ThemeType } from '@/api/api-models'
import { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient'
import { useDanmakuClient } from '@/store/useDanmakuClient';
import { Lottery24Filled, PeopleQueue24Filled, TabletSpeaker24Filled } from '@vicons/fluent'
import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5'
import { useElementSize, useStorage } from '@vueuse/core'
import { isDarkMode } from '@/Utils' // 引入暗黑模式判断工具
import { ThemeType } from '@/api/api-models' // 引入主题类型枚举
import { AuthInfo } from '@/data/DanmakuClients/OpenLiveClient' // 引入开放平台认证信息类型
import { useDanmakuClient } from '@/store/useDanmakuClient'; // 引入弹幕客户端状态管理
import { Lottery24Filled, PeopleQueue24Filled, TabletSpeaker24Filled } from '@vicons/fluent' // 引入 Fluent UI 图标
import { Moon, MusicalNote, Sunny } from '@vicons/ionicons5' // 引入 Ionicons 图标
import { useElementSize, useStorage } from '@vueuse/core' // 引入 VueUse 组合式函数
import {
NAlert,
NAvatar,
@@ -27,21 +27,28 @@ import {
NTag,
NText,
useMessage,
} from 'naive-ui'
import { h, onMounted, onUnmounted, ref } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
} from 'naive-ui' // 引入 Naive UI 组件
import { h, onMounted, onUnmounted, ref, computed } from 'vue' // 引入 Vue 相关 API
import { RouterLink, useRoute, useRouter } from 'vue-router' // 引入 Vue Router 相关 API
const route = useRoute()
const message = useMessage()
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
// -- 基本状态和工具 --
const route = useRoute() // 获取当前路由信息
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: () =>
h(
@@ -49,10 +56,10 @@ const menuOptions = [
{
to: {
name: 'open-live-lottery',
query: route.query,
query: route.query, // 保留查询参数
},
},
{ default: () => '抽奖' },
{ default: () => '弹幕抽奖' },
),
key: 'open-live-lottery',
icon: renderIcon(Lottery24Filled),
@@ -67,7 +74,7 @@ const menuOptions = [
query: route.query,
},
},
{ default: () => '点歌' },
{ default: () => '弹幕点歌' }, // 优化名称
),
key: 'open-live-live-request',
icon: renderIcon(MusicalNote),
@@ -82,7 +89,7 @@ const menuOptions = [
query: route.query,
},
},
{ default: () => '排队' },
{ default: () => '弹幕排队' }, // 优化名称
),
key: 'open-live-queue',
icon: renderIcon(PeopleQueue24Filled),
@@ -97,37 +104,74 @@ const menuOptions = [
query: route.query,
},
},
{ default: () => '弹幕' },
{ default: () => '弹幕朗读' }, // 优化名称
),
key: 'open-live-speech',
icon: renderIcon(TabletSpeaker24Filled),
},
]
])
// -- 工具函数 --
/**
* 渲染 Naive UI 图标的辅助函数
* @param icon 图标组件
*/
function renderIcon(icon: unknown) {
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 () => {
// 1. 从路由查询参数解析认证信息
authInfo.value = route.query as unknown as AuthInfo
// 2. 检查是否存在必要的 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 {
message.error('你不是从幻星平台访问此页面, 或未提供对应参数, 无法使用此功能')
// 5. 如果缺少 Code, 显示错误信息
message.error('无效访问: 缺少必要的认证参数 (Code)。请通过幻星平台获取链接。')
// authInfo 清空, 触发 v-if 显示错误页
authInfo.value = undefined;
}
})
onUnmounted(() => {
})
// onUnmounted 清理 (如果需要)
// onUnmounted(() => {
// danmakuClient.dispose(); // 示例: 如果有清理逻辑
// })
</script>
<template>
<!-- 情况一: 缺少认证信息, 显示错误提示页 -->
<NLayoutContent
v-if="!authInfo?.Code"
style="height: 100vh"
style="height: 100vh; display: flex; align-items: center; justify-content: center;"
content-style="padding: 24px;"
>
<NResult
status="error"
title="无效访问"
description="请确保您是通过正确的幻星平台 H5 插件链接访问此页面。"
>
<template #footer>
请前往
@@ -140,177 +184,288 @@ onUnmounted(() => {
>
幻星平台 | VTsuru
</NButton>
并点击 获取 , 再点击 获取 H5 插件链接来获取可用链接
<br>
或者直接在那个页面用也可以, 虽然并不推荐
获取 H5 插件链接
</template>
</NResult>
</NLayoutContent>
<!-- 情况二: 存在认证信息, 显示主布局 -->
<NLayout
v-else
style="height: 100vh"
:native-scrollbar="false"
>
<!-- 顶部导航栏 -->
<NLayoutHeader
style="height: 45px; padding: 5px 15px 5px 15px"
style="height: 60px; display: flex; align-items: center; padding: 0 20px;"
bordered
>
<NPageHeader :subtitle="($route.meta.title as string) ?? ''">
<template #extra>
<NSpace align="center">
<NTag :type="danmakuClient.connected ? 'success' : 'warning'">
{{ danmakuClient.connected ? `已连接 | ${danmakuClient.authInfo?.anchor_info?.uname}` : '未连接' }}
</NTag>
<NSwitch
:default-value="!isDarkMode"
@update:value="(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark)
"
<!-- 使用 NPageHeader 增强语义和结构 -->
<NPageHeader style="width: 100%;">
<!-- 标题区域 -->
<template #title>
<NButton
text
style="text-decoration: none;"
@click="router.push({ name: 'open-live-index', query: route.query })"
>
<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>
<NIcon :component="Sunny" />
<NIcon :component="Moon" />
</template>
<template #unchecked>
<NIcon :component="Moon" />
<NIcon :component="Sunny" />
</template>
</NSwitch>
</NSpace>
</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>
</NLayoutHeader>
<!-- 主体内容区域 (包含侧边栏和内容) -->
<NLayout
has-sider
style="height: calc(100vh - 45px - 30px)"
style="height: calc(100vh - 60px - 40px)"
>
<!-- 左侧导航栏 -->
<NLayoutSider
ref="sider"
bordered
show-trigger
default-collapsed
show-trigger="arrow-circle"
collapse-mode="width"
:collapsed-width="64"
:width="180"
:width="200"
:native-scrollbar="false"
style="height: 100%"
style="height: 100%;"
default-collapsed
>
<Transition>
<!-- 主播信息区域 (优化样式和过渡) -->
<Transition name="fade">
<!-- 添加简单的淡入淡出效果 -->
<div
v-if="danmakuClient.authInfo"
style="margin-top: 8px"
v-if="danmakuClient.authInfo?.anchor_info && siderWidth > 64"
style="padding: 20px 10px; text-align: center;"
>
<NSpace
vertical
justify="center"
align="center"
<NAvatar
:src="danmakuClient.authInfo.anchor_info.uface"
:img-props="{ referrerpolicy: 'no-referrer' }"
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
:src="danmakuClient.authInfo?.anchor_info?.uface"
:img-props="{ referrerpolicy: 'no-referrer' }"
round
bordered
:style="{
boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)',
}"
/>
<NEllipsis
v-if="width > 100"
style="max-width: 100%"
>
<NText strong>
{{ danmakuClient.authInfo?.anchor_info?.uname }}
</NText>
</NEllipsis>
</NSpace>
{{ danmakuClient.authInfo.anchor_info.uname }}
</NEllipsis>
</div>
<!-- 折叠时显示小头像 (可选) -->
<div
v-else-if="danmakuClient.authInfo?.anchor_info && siderWidth <= 64"
style="padding: 15px 0; text-align: center;"
>
<NAvatar
:src="danmakuClient.authInfo.anchor_info.uface"
:img-props="{ referrerpolicy: 'no-referrer' }"
round
size="medium"
:style="{ boxShadow: '0 1px 4px rgba(0, 0, 0, 0.1)' }"
/>
</div>
</Transition>
<!-- 导航菜单 -->
<NMenu
:default-value="$route.name?.toString()"
:value="route.name?.toString()"
:collapsed-width="64"
:collapsed-icon-size="22"
: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
v-if="width > 150"
depth="3"
style="font-size: 12px; text-align: center;"
>
有更多功能建议请
遇到问题或有建议?
<NButton
text
type="info"
@click="$router.push({ name: 'about' })"
size="tiny"
@click="router.push({ name: 'about' })"
>
反馈
点此反馈
</NButton>
</NText>
</NSpace>
</NLayoutSider>
<!-- 右侧主内容区域 -->
<NLayoutContent
style="height: 100%; padding: 10px"
style="height: 100%;"
content-style="padding: 15px; height: 100%;"
:native-scrollbar="false"
>
<!-- 弹幕客户端错误提示 -->
<NAlert
v-if="danmakuClientError"
type="error"
title="无法启动弹幕客户端"
title="弹幕客户端错误"
closable
style="margin-bottom: 15px;"
@close="danmakuClientError = undefined"
>
{{ danmakuClientError }}
</NAlert>
<RouterView
v-if="danmakuClient.authInfo"
v-slot="{ Component }"
>
<KeepAlive>
<!-- 路由视图: 根据认证状态显示不同内容 -->
<RouterView v-slot="{ Component }">
<!-- 情况一: 认证信息加载中或连接中 -->
<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
:is="Component"
:key="route.fullPath"
:room-info="danmakuClient.authInfo"
:code="authInfo.Code"
/>
</KeepAlive>
<!-- 情况三: 组件无法渲染或其他错误 (理论上不应发生, 但作为后备) -->
<NResult
v-else-if="!danmakuClientError"
status="warning"
title="页面加载失败"
description="无法加载当前功能模块,请尝试刷新或联系开发者。"
/>
</RouterView>
<template v-else>
{{ }}
<NAlert
type="info"
title="正在请求弹幕客户端认证信息..."
>
<NSpin show />
</NAlert>
</template>
<NBackTop />
<!-- 返回顶部按钮 -->
<NBackTop
:right="40"
:bottom="60"
/>
</NLayoutContent>
</NLayout>
<!-- 底部信息栏 -->
<NLayoutFooter
style="height: 30px"
style="height: 40px; display: flex; align-items: center; justify-content: center; padding: 0 20px;"
bordered
>
<NSpace
justify="center"
align="center"
style="height: 100%"
<NText
depth="3"
style="font-size: 12px;"
>
© {{ new Date().getFullYear() }} <!-- 动态年份 -->
<NButton
text
tag="a"
href="/"
href="https://vtsuru.live"
target="_blank"
type="info"
type="primary"
style="margin-left: 5px;"
>
vtsuru.live
</NButton>
</NSpace>
- VTsuru 提供支持
</NText>
</NLayoutFooter>
</NLayout>
</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>