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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="Permissions-Policy" content="interest-cohort=()">
|
<meta http-equiv="Permissions-Policy" content="interest-cohort=()">
|
||||||
<title>vtsuru.live</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>
|
<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.
|
enabled. Please enable it to continue.
|
||||||
</strong>
|
</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
16
src/Utils.ts
16
src/Utils.ts
@@ -33,3 +33,19 @@ export function objectsToCSV(arr: any[]) {
|
|||||||
})
|
})
|
||||||
.join('\n')
|
.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 { NotepadEdit20Filled, Delete24Filled, Play24Filled, SquareArrowForward24Filled, Info24Filled } from '@vicons/fluent'
|
||||||
import NeteaseIcon from '@/svgs/netease.svg'
|
import NeteaseIcon from '@/svgs/netease.svg'
|
||||||
import FiveSingIcon from '@/svgs/fivesing.svg'
|
import FiveSingIcon from '@/svgs/fivesing.svg'
|
||||||
|
import SongPlayer from './SongPlayer.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
songs: SongsInfo[]
|
songs: SongsInfo[]
|
||||||
@@ -72,12 +73,8 @@ const updateSongModel = ref<SongsInfo>({} as SongsInfo)
|
|||||||
const searchMusicKeyword = ref()
|
const searchMusicKeyword = ref()
|
||||||
const debouncedInput = refDebounced(searchMusicKeyword, 500)
|
const debouncedInput = refDebounced(searchMusicKeyword, 500)
|
||||||
|
|
||||||
const aplayerMusic = ref<{
|
const playingSong = ref<SongsInfo>()
|
||||||
title: string
|
const isLrcLoading = ref<string>()
|
||||||
artist: string
|
|
||||||
src: string
|
|
||||||
lrc: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const formRef = ref<FormInst | null>(null)
|
const formRef = ref<FormInst | null>(null)
|
||||||
const updateSongRules: FormRules = {
|
const updateSongRules: FormRules = {
|
||||||
@@ -255,7 +252,9 @@ function createColumns(): DataTableColumns<SongsInfo> {
|
|||||||
size: 'small',
|
size: 'small',
|
||||||
circle: true,
|
circle: true,
|
||||||
loading: isLrcLoading.value == data.key,
|
loading: isLrcLoading.value == data.key,
|
||||||
onClick: () => OnPlayMusic(data),
|
onClick: () => {
|
||||||
|
playingSong.value = data
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: () => h(NIcon, { component: Play24Filled }),
|
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) {
|
function GetPlayButton(song: SongsInfo) {
|
||||||
switch (song.from) {
|
switch (song.from) {
|
||||||
case SongFrom.FiveSing: {
|
case SongFrom.FiveSing: {
|
||||||
@@ -550,8 +464,8 @@ onMounted(() => {
|
|||||||
</NCard>
|
</NCard>
|
||||||
<NDivider style="margin: 5px 0 5px 0"> 共 {{ songsComputed.length }} 首 </NDivider>
|
<NDivider style="margin: 5px 0 5px 0"> 共 {{ songsComputed.length }} 首 </NDivider>
|
||||||
<Transition>
|
<Transition>
|
||||||
<div v-if="aplayerMusic" class="song-list">
|
<div v-if="playingSong" class="song-list">
|
||||||
<APlayer :music="aplayerMusic" autoplay :showLrc="aplayerMusic.lrc != null && aplayerMusic.lrc.length > 0" />
|
<SongPlayer :song="playingSong" v-model:is-lrc-loading="isLrcLoading" />
|
||||||
<NDivider style="margin: 15px 0 15px 0" />
|
<NDivider style="margin: 15px 0 15px 0" />
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</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 } }
|
} as { [key: string]: { name: string; compoent: any } }
|
||||||
export const SongListTemplateMap = {
|
export const SongListTemplateMap = {
|
||||||
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/songListTemplate/DefaultSongListTemplate.vue')) },
|
'': { 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 } }
|
} as { [key: string]: { name: string; compoent: any } }
|
||||||
export const IndexTemplateMap = {
|
export const IndexTemplateMap = {
|
||||||
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/indexTemplate/DefaultIndexTemplate.vue')) },
|
'': { name: '默认', compoent: defineAsyncComponent(() => import('@/views/view/indexTemplate/DefaultIndexTemplate.vue')) },
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ onMounted(async () => {
|
|||||||
</NLayoutContent>
|
</NLayoutContent>
|
||||||
<NLayout v-else style="height: 100vh">
|
<NLayout v-else style="height: 100vh">
|
||||||
<NLayoutHeader style="height: 50px; padding: 5px 15px 5px 15px">
|
<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>
|
<template #extra>
|
||||||
<NSpace align="center">
|
<NSpace align="center">
|
||||||
<NSwitch :default-value="!isDarkMode()" @update:value="(value: string & number & boolean) => themeType = value ? ThemeType.Light : ThemeType.Dark">
|
<NSwitch :default-value="!isDarkMode()" @update:value="(value: string & number & boolean) => themeType = value ? ThemeType.Light : ThemeType.Dark">
|
||||||
@@ -168,7 +168,9 @@ onMounted(async () => {
|
|||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
<template #title>
|
<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>
|
<NText strong style="font-size: 1.5rem; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)"> VTSURU </NText>
|
||||||
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
</NPageHeader>
|
</NPageHeader>
|
||||||
</NLayoutHeader>
|
</NLayoutHeader>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import { computed, h, onActivated, onDeactivated, onMounted, onUnmounted, ref }
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import SongRequestOBS from '../obs/SongRequestOBS.vue'
|
import SongRequestOBS from '../obs/SongRequestOBS.vue'
|
||||||
import APlayer from 'vue3-aplayer'
|
import APlayer from 'vue3-aplayer'
|
||||||
|
import SongPlayer from '@/components/SongPlayer.vue'
|
||||||
|
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
orderPrefix: '点歌',
|
orderPrefix: '点歌',
|
||||||
@@ -115,13 +116,7 @@ const settings = computed({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const aplayerMusic = ref<{
|
const selectedSong = ref<SongsInfo>()
|
||||||
title: string
|
|
||||||
artist: string
|
|
||||||
src: string
|
|
||||||
lrc: string
|
|
||||||
autoplay: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
client: DanmakuClient
|
client: DanmakuClient
|
||||||
@@ -693,96 +688,7 @@ async function updateActive() {
|
|||||||
console.error(err)
|
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('')
|
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 timer: any
|
||||||
let updateActiveTimer: any
|
let updateActiveTimer: any
|
||||||
@@ -849,16 +755,7 @@ onUnmounted(() => {
|
|||||||
</NCard>
|
</NCard>
|
||||||
<br />
|
<br />
|
||||||
<NCard>
|
<NCard>
|
||||||
<NTabs
|
<NTabs v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)" animated display-directive="show:lazy">
|
||||||
v-if="!accountInfo || accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest)"
|
|
||||||
animated
|
|
||||||
display-directive="show:lazy"
|
|
||||||
@update:value="
|
|
||||||
() => {
|
|
||||||
if (aplayerMusic) aplayerMusic.autoplay = false
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<NTabPane name="list" tab="列表">
|
<NTabPane name="list" tab="列表">
|
||||||
<NCard size="small">
|
<NCard size="small">
|
||||||
<NSpace align="center">
|
<NSpace align="center">
|
||||||
@@ -888,8 +785,8 @@ onUnmounted(() => {
|
|||||||
</NCard>
|
</NCard>
|
||||||
<NDivider> 共 {{ activeSongs.length }} 首 </NDivider>
|
<NDivider> 共 {{ activeSongs.length }} 首 </NDivider>
|
||||||
<Transition>
|
<Transition>
|
||||||
<div v-if="aplayerMusic" class="song-list">
|
<div v-if="selectedSong" class="song-list">
|
||||||
<APlayer :music="aplayerMusic" :autoplay="aplayerMusic.autoplay" :showLrc="aplayerMusic.lrc != null && aplayerMusic.lrc.length > 0" />
|
<SongPlayer :song="selectedSong" v-model:is-lrc-loading="isLrcLoading" />
|
||||||
<NDivider style="margin: 15px 0 15px 0" />
|
<NDivider style="margin: 15px 0 15px 0" />
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -945,7 +842,7 @@ onUnmounted(() => {
|
|||||||
<NSpace justify="end" align="center">
|
<NSpace justify="end" align="center">
|
||||||
<NTooltip v-if="song.song">
|
<NTooltip v-if="song.song">
|
||||||
<template #trigger>
|
<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>
|
<template #icon>
|
||||||
<NIcon :component="Play24Filled" />
|
<NIcon :component="Play24Filled" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,23 +2,23 @@
|
|||||||
<NSpin v-if="isLoading" show />
|
<NSpin v-if="isLoading" show />
|
||||||
<component
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="componentType"
|
:is="SongListTemplateMap[componentType ?? '']?.compoent"
|
||||||
:user-info="userInfo"
|
:user-info="userInfo"
|
||||||
:bili-info="biliInfo"
|
:bili-info="biliInfo"
|
||||||
:currentData="currentData"
|
:currentData="currentData"
|
||||||
:song-request-settings="settings"
|
:song-request-settings="settings"
|
||||||
:song-request-active="songs"
|
:song-request-active="songsActive"
|
||||||
@request-song="requestSong"
|
@request-song="requestSong"
|
||||||
|
v-bind="$attrs"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Setting_SongRequest, SongRequestInfo, SongsInfo } from '@/api/api-models'
|
import { Setting_SongRequest, SongRequestInfo, SongsInfo } from '@/api/api-models'
|
||||||
import DefaultSongListTemplate from '@/views/view/songListTemplate/DefaultSongListTemplate.vue'
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { UserInfo } from '@/api/api-models'
|
import { UserInfo } from '@/api/api-models'
|
||||||
import { QueryGetAPI, QueryPostAPIWithParams } from '@/api/query'
|
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 { NSpin, useMessage } from 'naive-ui'
|
||||||
import { useAccount } from '@/api/account'
|
import { useAccount } from '@/api/account'
|
||||||
|
|
||||||
@@ -33,25 +33,14 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const componentType = computed(() => {
|
const componentType = computed(() => {
|
||||||
const type = props.template ?? props.userInfo?.extra?.templateTypes['songlist']?.toLowerCase()
|
return props.template ?? props.userInfo?.extra?.templateTypes['songlist']?.toLowerCase()
|
||||||
if (props.userInfo) {
|
|
||||||
switch (type?.toLocaleLowerCase()) {
|
|
||||||
case '':
|
|
||||||
return DefaultSongListTemplate
|
|
||||||
|
|
||||||
default:
|
|
||||||
return DefaultSongListTemplate
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return DefaultSongListTemplate
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const currentData = ref<SongsInfo[]>()
|
const currentData = ref<SongsInfo[]>()
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const errMessage = ref('')
|
const errMessage = ref('')
|
||||||
const songs = ref<SongRequestInfo[]>([])
|
const songsActive = ref<SongRequestInfo[]>([])
|
||||||
const settings = ref<Setting_SongRequest>({} as Setting_SongRequest)
|
const settings = ref<Setting_SongRequest>({} as Setting_SongRequest)
|
||||||
|
|
||||||
async function getSongRequestInfo() {
|
async function getSongRequestInfo() {
|
||||||
@@ -89,7 +78,15 @@ async function getSongs() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
async function requestSong(song: SongsInfo) {
|
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 {
|
try {
|
||||||
const data = await QueryPostAPIWithParams(SONG_REQUEST_API_URL + 'add-from-web', {
|
const data = await QueryPostAPIWithParams(SONG_REQUEST_API_URL + 'add-from-web', {
|
||||||
target: props.userInfo?.id,
|
target: props.userInfo?.id,
|
||||||
@@ -106,15 +103,23 @@ async function requestSong(song: SongsInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!props.fakeData) {
|
if (!props.fakeData) {
|
||||||
|
try {
|
||||||
await getSongs()
|
await getSongs()
|
||||||
|
setTimeout(async () => {
|
||||||
const r = await getSongRequestInfo()
|
const r = await getSongRequestInfo()
|
||||||
if (r) {
|
if (r) {
|
||||||
songs.value = r.songs
|
songsActive.value = r.songs
|
||||||
settings.value = r.setting
|
settings.value = r.setting
|
||||||
}
|
}
|
||||||
|
}, 1000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
message.error('加载失败')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentData.value = props.fakeData
|
currentData.value = props.fakeData
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import SongRequestOBS from '@/views/obs/SongRequestOBS.vue'
|
|||||||
|
|
||||||
const accountInfo = useAccount()
|
const accountInfo = useAccount()
|
||||||
|
|
||||||
|
//所有模板都应该有这些
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
userInfo: UserInfo | undefined
|
userInfo: UserInfo | undefined
|
||||||
biliInfo: any | undefined
|
biliInfo: any | undefined
|
||||||
songRequestSettings: Setting_SongRequest
|
songRequestSettings: Setting_SongRequest
|
||||||
songRequestActive: SongRequestInfo[]
|
songRequestActive: SongRequestInfo[]
|
||||||
currentData: SongsInfo[] | undefined
|
currentData?: SongsInfo[] | undefined
|
||||||
}>()
|
}>()
|
||||||
const emits = defineEmits(['requestSong'])
|
const emits = defineEmits(['requestSong'])
|
||||||
|
|
||||||
@@ -38,14 +39,9 @@ const buttoms = (song: SongsInfo) => [
|
|||||||
loading: isLoading.value == song.key,
|
loading: isLoading.value == song.key,
|
||||||
disabled: !accountInfo,
|
disabled: !accountInfo,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (song.options || !props.songRequestSettings.allowFromWeb) {
|
|
||||||
navigator.clipboard.writeText(`${props.songRequestSettings.orderPrefix} ${song.name}`)
|
|
||||||
message.success('复制成功')
|
|
||||||
} else {
|
|
||||||
isLoading.value = song.key
|
isLoading.value = song.key
|
||||||
emits('requestSong', song)
|
emits('requestSong', song)
|
||||||
isLoading.value = ''
|
isLoading.value = ''
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,200 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Setting_SongRequest, SongRequestInfo, SongsInfo, UserInfo } from '@/api/api-models';
|
import { GetGuardColor } from '@/Utils'
|
||||||
import { NGridItem, NGrid } from 'naive-ui'
|
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<{
|
const props = defineProps<{
|
||||||
userInfo: UserInfo | undefined
|
userInfo: UserInfo | undefined
|
||||||
biliInfo: any | undefined
|
biliInfo: any | undefined
|
||||||
songRequestSettings: Setting_SongRequest
|
songRequestSettings: Setting_SongRequest
|
||||||
songRequretActive: SongRequestInfo[]
|
songRequestActive: SongRequestInfo[]
|
||||||
currentData: SongsInfo[] | undefined
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<NGrid>
|
<div :style="{ display: 'flex', justifyContent: 'center', flexDirection: windowSize.width.value > 900 ? 'row' : 'column', gap: '10px', width: '100%' }">
|
||||||
<NGridItem> </NGridItem>
|
<NCard size="small" :style="{ width: windowSize.width.value > 900 ? '400px' : '100%' }">
|
||||||
</NGrid>
|
<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>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user