mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
add simplesonglist
This commit is contained in:
@@ -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>
|
||||
|
||||
16
src/Utils.ts
16
src/Utils.ts
@@ -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 ''
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
127
src/components/SongPlayer.vue
Normal file
127
src/components/SongPlayer.vue
Normal 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>
|
||||
@@ -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')) },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
@@ -105,16 +102,24 @@ async function requestSong(song: SongsInfo) {
|
||||
message.error('点歌失败: ' + err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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 = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user