add simplesonglist

This commit is contained in:
2023-11-29 21:50:56 +08:00
parent cfe883e902
commit 776e5ffc1e
10 changed files with 398 additions and 256 deletions

View File

@@ -6,13 +6,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="interest-cohort=()">
<title>vtsuru.live</title>
<meta name="description" content="主包工具站" />
<meta name="description" content="为主播提供便利功能" />
<script async src="https://umami.vtsuru.live/script.js" data-website-id="05567214-d234-4076-9228-e4d69e3d202f"></script>
</head>
<body>
<noscript>
<strong>
We're sorry but Vtsuru,live doesn't work properly without JavaScript
We're sorry but Vtsuru.live doesn't work properly without JavaScript
enabled. Please enable it to continue.
</strong>
</noscript>

View File

@@ -33,3 +33,19 @@ export function objectsToCSV(arr: any[]) {
})
.join('\n')
}
export function GetGuardColor(level: number | null | undefined): string {
if (level) {
switch (level) {
case 1: {
return 'rgb(122, 4, 35)'
}
case 2: {
return 'rgb(157, 155, 255)'
}
case 3: {
return 'rgb(104, 136, 241)'
}
}
}
return ''
}

View File

@@ -40,6 +40,7 @@ import APlayer from 'vue3-aplayer'
import { NotepadEdit20Filled, Delete24Filled, Play24Filled, SquareArrowForward24Filled, Info24Filled } from '@vicons/fluent'
import NeteaseIcon from '@/svgs/netease.svg'
import FiveSingIcon from '@/svgs/fivesing.svg'
import SongPlayer from './SongPlayer.vue'
const props = defineProps<{
songs: SongsInfo[]
@@ -72,12 +73,8 @@ const updateSongModel = ref<SongsInfo>({} as SongsInfo)
const searchMusicKeyword = ref()
const debouncedInput = refDebounced(searchMusicKeyword, 500)
const aplayerMusic = ref<{
title: string
artist: string
src: string
lrc: string
}>()
const playingSong = ref<SongsInfo>()
const isLrcLoading = ref<string>()
const formRef = ref<FormInst | null>(null)
const updateSongRules: FormRules = {
@@ -255,7 +252,9 @@ function createColumns(): DataTableColumns<SongsInfo> {
size: 'small',
circle: true,
loading: isLrcLoading.value == data.key,
onClick: () => OnPlayMusic(data),
onClick: () => {
playingSong.value = data
},
},
{
icon: () => h(NIcon, { component: Play24Filled }),
@@ -319,92 +318,7 @@ function createColumns(): DataTableColumns<SongsInfo> {
},
]
}
function OnPlayMusic(song: SongsInfo) {
aplayerMusic.value = undefined
if (song.from == SongFrom.Netease) GetLyric(song)
else {
aplayerMusic.value = {
title: song.name,
artist: song.author.join('/') ?? '',
src: song.url,
lrc: '',
}
}
}
const isLrcLoading = ref('')
async function GetLyric(song: SongsInfo) {
isLrcLoading.value = song.key
QueryGetAPI<{ lyric: string; tlyric: string }>(SONG_API_URL + 'get-netease-lyric', { id: song.id })
.then((data) => {
console.log(mergeLyrics(data.data.lyric, data.data.tlyric))
if (data.code == 200) {
aplayerMusic.value = {
title: song.name,
artist: song.author.join('/') ?? '',
src: song.url,
lrc: data.data.tlyric ? mergeLyrics(data.data.lyric, data.data.tlyric) : data.data.lyric,
}
//aplayerMusic.value.lrc = data.data.lyric
}
})
.catch((err) => {
console.error(err)
aplayerMusic.value = {
title: song.name,
artist: song.author.join('/') ?? '',
src: song.url,
lrc: '',
}
})
.finally(() => {
isLrcLoading.value = ''
})
}
function mergeLyrics(originalLyrics: string, translatedLyrics: string): string {
const originalLines = originalLyrics.split('\n')
const translatedLines = translatedLyrics.split('\n')
let mergedLyrics = ''
for (let i = 0; i < originalLines.length; i++) {
const originalLine = originalLines[i]?.trim()
const originalTimeMatch = originalLine?.match(/\[(\d{2}:\d{2}\.\d{2,3})\]/) // 匹配原歌词的时间字符串
let mergedLine = originalLine
if (originalTimeMatch) {
const originalTime = originalTimeMatch[1]
const translatedLineIndex = translatedLines.findIndex((line) => line.includes(originalTime))
if (translatedLineIndex !== -1) {
const translatedLine = translatedLines[translatedLineIndex]
const translatedTimeMatch = translatedLine.match(/\[(\d{2}:\d{2}\.\d{2,3})\]/) // 匹配翻译歌词的时间字符串
if (translatedTimeMatch && translatedTimeMatch[1] === originalTime) {
const translatedText = translatedLine.slice(translatedTimeMatch[0].length).trim()
if (translatedText) {
mergedLine += ` (${translatedText})`
}
translatedLines.splice(translatedLineIndex, 1) // 从翻译歌词数组中移除已匹配的行
}
}
}
if (!mergedLine.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]$/)) {
//不是空行
mergedLyrics += `${mergedLine}\n`
}
}
// 将剩余的非空翻译歌词单独放在一行
for (const translatedLine of translatedLines) {
const translatedText = translatedLine.trim()
if (translatedText) {
mergedLyrics += `${translatedText}\n`
}
}
return mergedLyrics.trim()
}
function GetPlayButton(song: SongsInfo) {
switch (song.from) {
case SongFrom.FiveSing: {
@@ -550,8 +464,8 @@ onMounted(() => {
</NCard>
<NDivider style="margin: 5px 0 5px 0"> {{ songsComputed.length }} </NDivider>
<Transition>
<div v-if="aplayerMusic" class="song-list">
<APlayer :music="aplayerMusic" autoplay :showLrc="aplayerMusic.lrc != null && aplayerMusic.lrc.length > 0" />
<div v-if="playingSong" class="song-list">
<SongPlayer :song="playingSong" v-model:is-lrc-loading="isLrcLoading" />
<NDivider style="margin: 15px 0 15px 0" />
</div>
</Transition>

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
import { SongsInfo, SongFrom } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import { SONG_API_URL } from '@/data/constants'
import { NEmpty } from 'naive-ui'
import { computed, ref, toRef, toRefs, watch } from 'vue'
import APlayer from 'vue3-aplayer'
const props = defineProps<{
song: SongsInfo | undefined
isLrcLoading?: string
}>()
const currentSong = toRef(props, 'song')
const emits = defineEmits(['update:isLrcLoading'])
const aplayerMusic = ref({
title: '',
artist: '',
src: '',
pic: '',
lrc: '',
})
const temp = computed(() => {
if (props.song) OnPlayMusic(props.song)
return props.song
})
watch(temp, (newV) => {
if (newV) console.log('开始播放: ' + newV.name)
})
function OnPlayMusic(song: SongsInfo) {
if (song.from == SongFrom.Netease) GetLyric(song)
else {
aplayerMusic.value = {
title: song.name,
artist: song.author?.join('/') ?? '',
src: song.url,
pic: '',
lrc: '',
}
}
}
async function GetLyric(song: SongsInfo) {
emits('update:isLrcLoading', song.key)
QueryGetAPI<{ lyric: string; tlyric: string }>(SONG_API_URL + 'get-netease-lyric', { id: song.id })
.then((data) => {
console.log(mergeLyrics(data.data.lyric, data.data.tlyric))
if (data.code == 200) {
//props.song.value.lrc = data.data.tlyric ? mergeLyrics(data.data.lyric, data.data.tlyric) : data.data.lyric
aplayerMusic.value = {
title: song.name,
artist: song.author?.join('/') ?? '',
src: song.url,
pic: '',
lrc: data.data.tlyric ? mergeLyrics(data.data.lyric, data.data.tlyric) : data.data.lyric,
}
//aplayerMusic.value.lrc = data.data.lyric
} else {
aplayerMusic.value = {
title: song.name,
artist: song.author?.join('/') ?? '',
src: song.url,
pic: '',
lrc: '',
}
}
})
.catch((err) => {
console.error(err)
})
.finally(() => {
emits('update:isLrcLoading', undefined)
})
}
function mergeLyrics(originalLyrics: string, translatedLyrics: string): string {
const originalLines = originalLyrics.split('\n')
const translatedLines = translatedLyrics.split('\n')
let mergedLyrics = ''
for (let i = 0; i < originalLines.length; i++) {
const originalLine = originalLines[i]?.trim()
const originalTimeMatch = originalLine?.match(/\[(\d{2}:\d{2}\.\d{2,3})\]/) // 匹配原歌词的时间字符串
let mergedLine = originalLine
if (originalTimeMatch) {
const originalTime = originalTimeMatch[1]
const translatedLineIndex = translatedLines.findIndex((line) => line.includes(originalTime))
if (translatedLineIndex !== -1) {
const translatedLine = translatedLines[translatedLineIndex]
const translatedTimeMatch = translatedLine.match(/\[(\d{2}:\d{2}\.\d{2,3})\]/) // 匹配翻译歌词的时间字符串
if (translatedTimeMatch && translatedTimeMatch[1] === originalTime) {
const translatedText = translatedLine.slice(translatedTimeMatch[0].length).trim()
if (translatedText) {
mergedLine += ` (${translatedText})`
}
translatedLines.splice(translatedLineIndex, 1) // 从翻译歌词数组中移除已匹配的行
}
}
}
if (!mergedLine.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]$/)) {
//不是空行
mergedLyrics += `${mergedLine}\n`
}
}
// 将剩余的非空翻译歌词单独放在一行
for (const translatedLine of translatedLines) {
const translatedText = translatedLine.trim()
if (translatedText) {
mergedLyrics += `${translatedText}\n`
}
}
return mergedLyrics.trim()
}
</script>
<template>
<NEmpty v-if="!aplayerMusic.src" :description="props.isLrcLoading ? '歌词加载中...' : '暂无歌曲'" />
<APlayer v-else :music="aplayerMusic" autoplay :showLrc="aplayerMusic?.lrc && aplayerMusic.lrc.length > 0" />
</template>

View File

@@ -33,6 +33,7 @@ export const ScheduleTemplateMap = {
} as { [key: string]: { name: string; compoent: any } }
export const SongListTemplateMap = {
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue')) },
simple: { name: '简单', compoent: defineAsyncComponent(() => import('@/views/view/songListTemplate/SimpleSongListTemplate.vue')) },
} as { [key: string]: { name: string; compoent: any } }
export const IndexTemplateMap = {
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/indexTemplate/DefaultIndexTemplate.vue')) },

View File

@@ -148,7 +148,7 @@ onMounted(async () => {
</NLayoutContent>
<NLayout v-else style="height: 100vh">
<NLayoutHeader style="height: 50px; padding: 5px 15px 5px 15px">
<NPageHeader :subtitle="($route.meta.title as string) ?? ''">
<NPageHeader :subtitle="($route.meta.title as string) ?? ''" style="margin-top: 6px">
<template #extra>
<NSpace align="center">
<NSwitch :default-value="!isDarkMode()" @update:value="(value: string & number & boolean) => themeType = value ? ThemeType.Light : ThemeType.Dark">
@@ -168,7 +168,9 @@ onMounted(async () => {
</NSpace>
</template>
<template #title>
<NButton text tag="a" @click="$router.push({ name: 'index' })">
<NText strong style="font-size: 1.5rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)"> VTSURU </NText>
</NButton>
</template>
</NPageHeader>
</NLayoutHeader>

View File

@@ -62,6 +62,7 @@ import { computed, h, onActivated, onDeactivated, onMounted, onUnmounted, ref }
import { useRoute } from 'vue-router'
import SongRequestOBS from '../obs/SongRequestOBS.vue'
import APlayer from 'vue3-aplayer'
import SongPlayer from '@/components/SongPlayer.vue'
const defaultSettings = {
orderPrefix: '点歌',
@@ -115,13 +116,7 @@ const settings = computed({
}
},
})
const aplayerMusic = ref<{
title: string
artist: string
src: string
lrc: string
autoplay: boolean
}>()
const selectedSong = ref<SongsInfo>()
const props = defineProps<{
client: DanmakuClient
@@ -693,96 +688,7 @@ async function updateActive() {
console.error(err)
}
}
function playMusic(song: SongRequestInfo) {
aplayerMusic.value = undefined
if (song.song?.from == SongFrom.Netease) GetLyric(song.song)
else {
aplayerMusic.value = {
title: song.song?.name ?? '未知',
artist: song.song?.author?.join('/') ?? '',
src: song.song?.url ?? '',
lrc: '',
autoplay: true,
}
}
}
const isLrcLoading = ref('')
async function GetLyric(song: SongsInfo) {
isLrcLoading.value = song.key
QueryGetAPI<{ lyric: string; tlyric: string }>(SONG_API_URL + 'get-netease-lyric', { id: song.id })
.then((data) => {
console.log(mergeLyrics(data.data.lyric, data.data.tlyric))
if (data.code == 200) {
aplayerMusic.value = {
title: song.name,
artist: song.author.join('/') ?? '',
src: song.url,
lrc: data.data.tlyric ? mergeLyrics(data.data.lyric, data.data.tlyric) : data.data.lyric,
autoplay: true,
}
//aplayerMusic.value.lrc = data.data.lyric
}
})
.catch((err) => {
console.error(err)
aplayerMusic.value = {
title: song.name,
artist: song.author.join('/') ?? '',
src: song.url,
lrc: '',
autoplay: true,
}
})
.finally(() => {
isLrcLoading.value = ''
})
}
function mergeLyrics(originalLyrics: string, translatedLyrics: string): string {
const originalLines = originalLyrics.split('\n')
const translatedLines = translatedLyrics.split('\n')
let mergedLyrics = ''
for (let i = 0; i < originalLines.length; i++) {
const originalLine = originalLines[i]?.trim()
const originalTimeMatch = originalLine?.match(/\[(\d{2}:\d{2}\.\d{2,3})\]/) // 匹配原歌词的时间字符串
let mergedLine = originalLine
if (originalTimeMatch) {
const originalTime = originalTimeMatch[1]
const translatedLineIndex = translatedLines.findIndex((line) => line.includes(originalTime))
if (translatedLineIndex !== -1) {
const translatedLine = translatedLines[translatedLineIndex]
const translatedTimeMatch = translatedLine.match(/\[(\d{2}:\d{2}\.\d{2,3})\]/) // 匹配翻译歌词的时间字符串
if (translatedTimeMatch && translatedTimeMatch[1] === originalTime) {
const translatedText = translatedLine.slice(translatedTimeMatch[0].length).trim()
if (translatedText) {
mergedLine += ` (${translatedText})`
}
translatedLines.splice(translatedLineIndex, 1) // 从翻译歌词数组中移除已匹配的行
}
}
}
if (!mergedLine.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]$/)) {
//不是空行
mergedLyrics += `${mergedLine}\n`
}
}
// 将剩余的非空翻译歌词单独放在一行
for (const translatedLine of translatedLines) {
const translatedText = translatedLine.trim()
if (translatedText) {
mergedLyrics += `${translatedText}\n`
}
}
return mergedLyrics.trim()
}
let timer: any
let updateActiveTimer: any
@@ -849,16 +755,7 @@ onUnmounted(() => {
</NCard>
<br />
<NCard>
<NTabs
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)"
animated
display-directive="show:lazy"
@update:value="
() => {
if (aplayerMusic) aplayerMusic.autoplay = false
}
"
>
<NTabs v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)" animated display-directive="show:lazy">
<NTabPane name="list" tab="列表">
<NCard size="small">
<NSpace align="center">
@@ -888,8 +785,8 @@ onUnmounted(() => {
</NCard>
<NDivider> {{ activeSongs.length }} </NDivider>
<Transition>
<div v-if="aplayerMusic" class="song-list">
<APlayer :music="aplayerMusic" :autoplay="aplayerMusic.autoplay" :showLrc="aplayerMusic.lrc != null && aplayerMusic.lrc.length > 0" />
<div v-if="selectedSong" class="song-list">
<SongPlayer :song="selectedSong" v-model:is-lrc-loading="isLrcLoading" />
<NDivider style="margin: 15px 0 15px 0" />
</div>
</Transition>
@@ -945,7 +842,7 @@ onUnmounted(() => {
<NSpace justify="end" align="center">
<NTooltip v-if="song.song">
<template #trigger>
<NButton circle type="success" style="height: 30px; width: 30px" :loading="isLrcLoading == song?.song?.key" @click="playMusic(song)">
<NButton circle type="success" style="height: 30px; width: 30px" :loading="isLrcLoading == song?.song?.key" @click="selectedSong = song.song">
<template #icon>
<NIcon :component="Play24Filled" />
</template>

View File

@@ -2,23 +2,23 @@
<NSpin v-if="isLoading" show />
<component
v-else
:is="componentType"
:is="SongListTemplateMap[componentType ?? '']?.compoent"
:user-info="userInfo"
:bili-info="biliInfo"
:currentData="currentData"
:song-request-settings="settings"
:song-request-active="songs"
:song-request-active="songsActive"
@request-song="requestSong"
v-bind="$attrs"
/>
</template>
<script lang="ts" setup>
import { Setting_SongRequest, SongRequestInfo, SongsInfo } from '@/api/api-models'
import DefaultSongListTemplate from '@/views/view/songListTemplate/DefaultSongListTemplate.vue'
import { computed, onMounted, ref } from 'vue'
import { UserInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPIWithParams } from '@/api/query'
import { SONG_API_URL, SONG_REQUEST_API_URL } from '@/data/constants'
import { SONG_API_URL, SONG_REQUEST_API_URL, SongListTemplateMap } from '@/data/constants'
import { NSpin, useMessage } from 'naive-ui'
import { useAccount } from '@/api/account'
@@ -33,25 +33,14 @@ const props = defineProps<{
}>()
const componentType = computed(() => {
const type = props.template ?? props.userInfo?.extra?.templateTypes['songlist']?.toLowerCase()
if (props.userInfo) {
switch (type?.toLocaleLowerCase()) {
case '':
return DefaultSongListTemplate
default:
return DefaultSongListTemplate
}
} else {
return DefaultSongListTemplate
}
return props.template ?? props.userInfo?.extra?.templateTypes['songlist']?.toLowerCase()
})
const currentData = ref<SongsInfo[]>()
const isLoading = ref(true)
const message = useMessage()
const errMessage = ref('')
const songs = ref<SongRequestInfo[]>([])
const songsActive = ref<SongRequestInfo[]>([])
const settings = ref<Setting_SongRequest>({} as Setting_SongRequest)
async function getSongRequestInfo() {
@@ -89,7 +78,15 @@ async function getSongs() {
})
}
async function requestSong(song: SongsInfo) {
if (props.userInfo && accountInfo.value?.id != props.userInfo?.id) {
if (song.options || !settings.value.allowFromWeb) {
navigator.clipboard.writeText(`${settings.value.orderPrefix} ${song.name}`)
if (!accountInfo.value) {
message.warning('要从网页点歌请先登录, 点歌弹幕已复制到剪切板')
} else {
message.success('复制成功')
}
} else {
if (props.userInfo) {
try {
const data = await QueryPostAPIWithParams(SONG_REQUEST_API_URL + 'add-from-web', {
target: props.userInfo?.id,
@@ -106,15 +103,23 @@ async function requestSong(song: SongsInfo) {
}
}
}
}
onMounted(async () => {
if (!props.fakeData) {
try {
await getSongs()
setTimeout(async () => {
const r = await getSongRequestInfo()
if (r) {
songs.value = r.songs
songsActive.value = r.songs
settings.value = r.setting
}
}, 1000)
} catch (err) {
console.error(err)
message.error('加载失败')
}
} else {
currentData.value = props.fakeData
isLoading.value = false

View File

@@ -10,12 +10,13 @@ import SongRequestOBS from '@/views/obs/SongRequestOBS.vue'
const accountInfo = useAccount()
//所有模板都应该有这些
const props = defineProps<{
userInfo: UserInfo | undefined
biliInfo: any | undefined
songRequestSettings: Setting_SongRequest
songRequestActive: SongRequestInfo[]
currentData: SongsInfo[] | undefined
currentData?: SongsInfo[] | undefined
}>()
const emits = defineEmits(['requestSong'])
@@ -38,14 +39,9 @@ const buttoms = (song: SongsInfo) => [
loading: isLoading.value == song.key,
disabled: !accountInfo,
onClick: () => {
if (song.options || !props.songRequestSettings.allowFromWeb) {
navigator.clipboard.writeText(`${props.songRequestSettings.orderPrefix} ${song.name}`)
message.success('复制成功')
} else {
isLoading.value = song.key
emits('requestSong', song)
isLoading.value = ''
}
},
},
{

View File

@@ -1,17 +1,200 @@
<script setup lang="ts">
import { Setting_SongRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models';
import { NGridItem, NGrid } from 'naive-ui'
import { GetGuardColor } from '@/Utils'
import { useAccount } from '@/api/account'
import { FunctionTypes, Setting_SongRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models'
import SongPlayer from '@/components/SongPlayer.vue'
import SongRequestOBS from '@/views/obs/SongRequestOBS.vue'
import { CloudAdd20Filled, Play24Filled } from '@vicons/fluent'
import { useDebounceFn, useElementSize, useInfiniteScroll, useWindowSize } from '@vueuse/core'
import { debounce, throttle } from 'lodash'
import { NGridItem, NGrid, NCard, NSpace, NDivider, NButton, NCollapseTransition, NInput, NText, NEllipsis, NSelect, NEmpty, NIcon, NTag, NScrollbar, NTooltip } from 'naive-ui'
import { computed, ref } from 'vue'
const props = defineProps<{
userInfo: UserInfo | undefined
biliInfo: any | undefined
songRequestSettings: Setting_SongRequest
songRequretActive: SongRequestInfo[]
songRequestActive: SongRequestInfo[]
currentData: SongsInfo[] | undefined
}>()
const emits = defineEmits(['requestSong'])
const windowSize = useWindowSize()
const container = ref()
const index = ref(20)
const accountInfo = useAccount()
const selectedTag = ref('')
const selectedSong = ref<SongsInfo>()
const searchKeyword = ref('')
const selectedAuthor = ref<string>()
const isLrcLoading = ref('')
const isLoading = ref('')
const tags = computed(() => {
if (props.currentData) {
return [
...new Set(
props.currentData
.map((item) => {
return item.tags ?? []
})
.reduce((prev, curr) => [...prev, ...curr], [])
),
]
}
return []
})
const authors = computed(() => {
if (props.currentData) {
return [
...new Set(
props.currentData
.map((item) => {
return item.author ?? []
})
.reduce((prev, curr) => [...prev, ...curr], [])
),
]
}
return []
})
const songs = computed(() => {
if (props.currentData) {
return props.currentData
.filter((item) => {
return (
(!selectedTag.value || item.tags?.includes(selectedTag.value)) &&
(!searchKeyword.value || item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())) &&
(!selectedAuthor.value || item.author?.includes(selectedAuthor.value) == true)
)
})
.slice(0, index.value)
}
})
const onScroll = throttle((e: UIEvent) => {
const container = e.target as HTMLDivElement
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 20) {
loadMore()
}
}, 100)
function loadMore() {
if (props.currentData) {
index.value += props.currentData.length > 20 + index.value ? 20 : props.currentData.length - index.value
}
}
</script>
<template>
<NGrid>
<NGridItem> </NGridItem>
</NGrid>
<div :style="{ display: 'flex', justifyContent: 'center', flexDirection: windowSize.width.value > 900 ? 'row' : 'column', gap: '10px', width: '100%' }">
<NCard size="small" :style="{ width: windowSize.width.value > 900 ? '400px' : '100%' }">
<NCollapseTransition>
<SongPlayer v-if="selectedSong" :song="selectedSong" v-model:is-lrc-loading="isLrcLoading" />
</NCollapseTransition>
<NDivider> 标签 </NDivider>
<NSpace>
<NButton v-for="tag in tags" size="small" secondary :type="selectedTag == tag ? 'primary' : 'default'" @click="selectedTag == tag ? (selectedTag = '') : (selectedTag = tag)">
{{ tag }}
</NButton>
</NSpace>
<NDivider> 搜索歌曲 </NDivider>
<NSpace vertical>
<NInput v-model:value="searchKeyword" placeholder="歌名" clearable />
<NSelect
v-model:value="selectedAuthor"
:options="
authors.map((a) => {
return { label: a, value: a }
})
"
placeholder="选择歌手"
clearable
/>
<NDivider />
<SongRequestOBS v-if="userInfo?.extra?.enableFunctions.includes(FunctionTypes.SongRequest)" :id="userInfo?.id" />
</NSpace>
</NCard>
<NEmpty v-if="!currentData || songs?.length == 0" description="暂无曲目" style="max-width: 0 auto" />
<div v-else ref="container" :style="{ flexGrow: 1, height: windowSize.width.value > 900 ? '90vh' : '800px', overflowY: 'auto', overflowX: 'hidden' }" @scroll="onScroll">
<NGrid cols="1 600:2 900:3 1200:4" x-gap="10" y-gap="10" responsive="self">
<NGridItem v-for="item in songs" :key="item.key">
<NCard size="small" style="height: 200px; min-width: 300px">
<template #header>
<NSpace :wrap="false" align="center">
<div :style="`border-radius: 4px; background-color: ${item.options ? '#bd5757' : '#577fb8'}; width: 7px; height: 20px`"></div>
<NEllipsis>
{{ item.name }}
</NEllipsis>
</NSpace>
</template>
<NText depth="3">
<NSpace v-if="(item.author?.length ?? 0) > 0" :size="0">
<div v-for="(author, index) in item.author" v-bind:key="author">
<NButton size="small" text @click="selectedAuthor == author ? (selectedAuthor = undefined) : (selectedAuthor = author)">
<NText depth="3" :style="{ color: selectedAuthor == author ? '#82bcd3' : '' }">
{{ author }}
</NText>
<NDivider v-if="index < (item.author?.length ?? 0) - 1" vertical />
</NButton>
</div>
</NSpace>
</NText>
<template #footer>
<NEllipsis>
{{ item.description }}
</NEllipsis>
<template v-if="item.options">
<NSpace>
<NTag v-if="item.options?.scMinPrice" size="small" type="error" :bordered="false"> SC | {{ item.options?.scMinPrice }}</NTag>
<NTag v-if="item.options?.fanMedalMinLevel" size="small" type="info" :bordered="false"> 粉丝牌 | {{ item.options?.fanMedalMinLevel }}</NTag>
<NTag v-if="item.options?.needZongdu" size="small" :color="{ color: GetGuardColor(1) }"> 总督 </NTag>
<NTag v-if="item.options?.needTidu" size="small" :color="{ color: GetGuardColor(2) }"> 提督 </NTag>
<NTag v-if="item.options?.needJianzhang" size="small" :color="{ color: GetGuardColor(3) }"> 舰长 </NTag>
</NSpace>
</template>
</template>
<template #action>
<NSpace>
<NTooltip v-if="item.url">
<template #trigger>
<NButton size="small" @click="selectedSong = item" type="success" :loading="isLrcLoading == item.key">
<template #icon>
<NIcon :component="Play24Filled" />
</template>
</NButton>
</template>
试听
</NTooltip>
<NTooltip>
<template #trigger>
<NButton
size="small"
@click="
() => {
isLoading = item.key
emits('requestSong', item)
isLoading = ''
}
"
:type="!songRequestSettings.allowFromWeb || item.options ? 'warning' : 'info'"
:loading="isLoading == item.key"
>
<template #icon>
<NIcon :component="CloudAdd20Filled" />
</template>
</NButton>
</template>
{{ !songRequestSettings.allowFromWeb || item.options ? '点歌 | 用户或此歌曲不允许从网页点歌, 点击后将复制点歌内容到剪切板' : !accountInfo ? '点歌 | 你需要登录后才能点歌' : '点歌' }}
</NTooltip>
</NSpace>
</template>
</NCard>
</NGridItem>
</NGrid>
<NDivider />
<NSpace justify="center">
<NButton v-if="currentData.length > index" @click="loadMore"> 加载更多 </NButton>
</NSpace>
</div>
</div>
</template>