mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
1018
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
import { SongAuthorInfo, SongFrom, SongLanguage, SongsInfo, UserInfo } from '@/api/api-models'
|
import { SongAuthorInfo, SongFrom, SongLanguage, SongsInfo, UserInfo } from '@/api/api-models'
|
||||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||||
import { SONG_API_URL } from '@/data/constants'
|
import { SONG_API_URL } from '@/data/constants'
|
||||||
import { refDebounced, useDebounceFn } from '@vueuse/core'
|
import { refDebounced, useDebounceFn, useLocalStorage } from '@vueuse/core'
|
||||||
import { List } from 'linqts'
|
import { List } from 'linqts'
|
||||||
import {
|
import {
|
||||||
DataTableBaseColumn,
|
DataTableBaseColumn,
|
||||||
@@ -51,6 +51,7 @@ watch(
|
|||||||
}, 1)
|
}, 1)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const volume = useLocalStorage('Settings.AplayerVolume', 0.8)
|
||||||
const songsInternal = ref(props.songs)
|
const songsInternal = ref(props.songs)
|
||||||
const songsComputed = computed(() => {
|
const songsComputed = computed(() => {
|
||||||
if (debouncedInput.value) {
|
if (debouncedInput.value) {
|
||||||
@@ -70,7 +71,7 @@ const aplayerMusic = ref<{
|
|||||||
title: string
|
title: string
|
||||||
artist: string
|
artist: string
|
||||||
src: string
|
src: string
|
||||||
pic: string
|
lrc: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const formRef = ref<FormInst | null>(null)
|
const formRef = ref<FormInst | null>(null)
|
||||||
@@ -152,8 +153,7 @@ const authorColumn = ref<DataTableBaseColumn<SongsInfo>>({
|
|||||||
const onAuthorClick = (author: string) => {
|
const onAuthorClick = (author: string) => {
|
||||||
if (authorColumn.value.filterOptionValue == author) {
|
if (authorColumn.value.filterOptionValue == author) {
|
||||||
authorColumn.value.filterOptionValue = undefined
|
authorColumn.value.filterOptionValue = undefined
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
authorColumn.value.filterOptionValue = author
|
authorColumn.value.filterOptionValue = author
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,11 +218,11 @@ function createColumns(): DataTableColumns<SongsInfo> {
|
|||||||
NSpace,
|
NSpace,
|
||||||
{
|
{
|
||||||
justify: 'end',
|
justify: 'end',
|
||||||
size: 10
|
size: 10,
|
||||||
},
|
},
|
||||||
() => [
|
() => [
|
||||||
GetPlayButton(data),
|
GetPlayButton(data),
|
||||||
data.url
|
data.url?.endsWith('mp3') || data.url?.endsWith('flac') || data.url?.endsWith('ogg') || data.url?.endsWith('wav') || data.url?.endsWith('m4a')
|
||||||
? h(NTooltip, null, {
|
? h(NTooltip, null, {
|
||||||
trigger: () =>
|
trigger: () =>
|
||||||
h(
|
h(
|
||||||
@@ -231,14 +231,7 @@ function createColumns(): DataTableColumns<SongsInfo> {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
size: 'small',
|
size: 'small',
|
||||||
circle: true,
|
circle: true,
|
||||||
onClick: () => {
|
onClick: () => OnPlayMusic(data),
|
||||||
aplayerMusic.value = {
|
|
||||||
title: data.name,
|
|
||||||
artist: data.author.join('/') ?? '',
|
|
||||||
src: data.url,
|
|
||||||
pic: '',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: () => h(NIcon, { component: Play24Filled }),
|
icon: () => h(NIcon, { component: Play24Filled }),
|
||||||
@@ -301,6 +294,87 @@ 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: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function GetLyric(song: SongsInfo) {
|
||||||
|
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: '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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: {
|
||||||
@@ -411,12 +485,16 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NCard embedded size="small">
|
<NCard embedded size="small">
|
||||||
<NInput placeholder="搜索歌曲" v-model:value="searchMusicKeyword" size="small" style="max-width: 150px" />
|
<NSpace>
|
||||||
|
<NInput placeholder="搜索歌曲" v-model:value="searchMusicKeyword" size="small" style="width: 150px" />
|
||||||
|
<NSelect placeholder="选择歌手" v-model:value="authorColumn.filterOptionValue" :options="authorsOptions" clearable filterable size="small" style="width: 150px" />
|
||||||
|
<NButton v-if="authorColumn.filterOptionValue" type="error" @click="authorColumn.filterOptionValue = null" size="small"> 清除歌手选择 </NButton>
|
||||||
|
</NSpace>
|
||||||
</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">
|
<div v-if="aplayerMusic" class="song-list">
|
||||||
<APlayer :music="aplayerMusic" autoplay />
|
<APlayer :music="aplayerMusic" autoplay :showLrc="aplayerMusic.lrc != null && aplayerMusic.lrc.length > 0"/>
|
||||||
<NDivider style="margin: 15px 0 15px 0" />
|
<NDivider style="margin: 15px 0 15px 0" />
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ onMounted(() => {
|
|||||||
<NIcon :component="Moon" />
|
<NIcon :component="Moon" />
|
||||||
</template>
|
</template>
|
||||||
</NSwitch>
|
</NSwitch>
|
||||||
<NButton size="small" style="right: 0px; position: relative" type="primary" @click="$router.push({ name: 'user-index', params: { id: accountInfo.id } })"> 回到主页 </NButton>
|
<NButton size="small" style="right: 0px; position: relative" type="primary" @click="$router.push({ name: 'user-index', params: { id: accountInfo.name } })"> 回到主页 </NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</template>
|
</template>
|
||||||
</NPageHeader>
|
</NPageHeader>
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ const message = useMessage()
|
|||||||
|
|
||||||
const fansHistory = ref<{ time: number; count: number }[]>()
|
const fansHistory = ref<{ time: number; count: number }[]>()
|
||||||
const guardHistory = ref<{ time: number; count: number }[]>()
|
const guardHistory = ref<{ time: number; count: number }[]>()
|
||||||
|
const upstatHistory = ref<{ time: number; stats: { views: number; likes: number } }[]>()
|
||||||
const fansOption = ref()
|
const fansOption = ref()
|
||||||
const guardsOption = ref()
|
const guardsOption = ref()
|
||||||
|
const upstatViewOption = ref()
|
||||||
|
const upstatLikeOption = ref()
|
||||||
|
|
||||||
async function getFansHistory() {
|
async function getFansHistory() {
|
||||||
await QueryGetAPI<
|
await QueryGetAPI<
|
||||||
@@ -27,9 +30,7 @@ async function getFansHistory() {
|
|||||||
time: number
|
time: number
|
||||||
count: number
|
count: number
|
||||||
}[]
|
}[]
|
||||||
>(HISTORY_API_URL + 'fans', {
|
>(HISTORY_API_URL + 'fans')
|
||||||
id: accountInfo.value?.id,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
fansHistory.value = data.data
|
fansHistory.value = data.data
|
||||||
@@ -48,9 +49,7 @@ async function getGuardsHistory() {
|
|||||||
time: number
|
time: number
|
||||||
count: number
|
count: number
|
||||||
}[]
|
}[]
|
||||||
>(HISTORY_API_URL + 'guards', {
|
>(HISTORY_API_URL + 'guards')
|
||||||
id: accountInfo.value?.id,
|
|
||||||
})
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.code == 200) {
|
if (data.code == 200) {
|
||||||
guardHistory.value = data.data
|
guardHistory.value = data.data
|
||||||
@@ -63,6 +62,28 @@ async function getGuardsHistory() {
|
|||||||
message.error('加载失败')
|
message.error('加载失败')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
async function getUpstatHistory() {
|
||||||
|
await QueryGetAPI<
|
||||||
|
{
|
||||||
|
time: number
|
||||||
|
stats: {
|
||||||
|
views: number
|
||||||
|
likes: number
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
>(HISTORY_API_URL + 'upstat')
|
||||||
|
.then((data) => {
|
||||||
|
if (data.code == 200) {
|
||||||
|
upstatHistory.value = data.data
|
||||||
|
} else {
|
||||||
|
message.error('加载失败: ' + data.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
message.error('加载失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
function isSameDay(time1: number, time2: number) {
|
function isSameDay(time1: number, time2: number) {
|
||||||
const time1Date = new Date(time1)
|
const time1Date = new Date(time1)
|
||||||
const time2Date = new Date(time2)
|
const time2Date = new Date(time2)
|
||||||
@@ -100,6 +121,25 @@ function getOptions() {
|
|||||||
lastDayGuards = g.count
|
lastDayGuards = g.count
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
let upstatViewIncreace: { time: number; value: number }[] = []
|
||||||
|
let upstatLikeIncreace: { time: number; value: number }[] = []
|
||||||
|
if (upstatHistory.value && upstatHistory.value.length > 0) {
|
||||||
|
let lastUpstatView = upstatHistory.value[0].stats.views
|
||||||
|
let lastUpstatLike = upstatHistory.value[0].stats.likes
|
||||||
|
|
||||||
|
upstatHistory.value?.forEach((u) => {
|
||||||
|
upstatViewIncreace.push({
|
||||||
|
time: u.time,
|
||||||
|
value: u.stats.views - lastUpstatView,
|
||||||
|
})
|
||||||
|
lastUpstatView = u.stats.views
|
||||||
|
upstatLikeIncreace.push({
|
||||||
|
time: u.time,
|
||||||
|
value: u.stats.likes - lastUpstatLike,
|
||||||
|
})
|
||||||
|
lastUpstatLike = u.stats.likes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fansOption.value = {
|
fansOption.value = {
|
||||||
title: {
|
title: {
|
||||||
@@ -152,8 +192,6 @@ function getOptions() {
|
|||||||
{
|
{
|
||||||
name: '粉丝数',
|
name: '粉丝数',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
xAxisIndex: 1,
|
|
||||||
yAxisIndex: 1,
|
|
||||||
emphasis: {
|
emphasis: {
|
||||||
focus: 'series',
|
focus: 'series',
|
||||||
},
|
},
|
||||||
@@ -162,6 +200,7 @@ function getOptions() {
|
|||||||
{
|
{
|
||||||
name: '增量 /日',
|
name: '增量 /日',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
yAxisIndex: 1,
|
||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
emphasis: {
|
emphasis: {
|
||||||
@@ -193,9 +232,6 @@ function getOptions() {
|
|||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'value',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
xAxis: [
|
xAxis: [
|
||||||
{
|
{
|
||||||
@@ -234,11 +270,139 @@ function getOptions() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
upstatViewOption.value = {
|
||||||
|
title: {
|
||||||
|
text: '投稿播放数',
|
||||||
|
left: 'left',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
legend: {},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
onZero: false,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#EE6666',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// prettier-ignore
|
||||||
|
data: upstatHistory.value?.map((f) => format(f.time, 'yyyy-MM-dd')),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '播放数',
|
||||||
|
type: 'line',
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
},
|
||||||
|
data: upstatHistory.value?.map((f) => f.stats.views),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '日增',
|
||||||
|
type: 'line',
|
||||||
|
step: 'start',
|
||||||
|
yAxisIndex: 1,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
},
|
||||||
|
data: upstatViewIncreace.map((f) => f.value),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
realtime: true,
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
xAxisIndex: [0, 1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
upstatLikeOption.value = {
|
||||||
|
title: {
|
||||||
|
text: '投稿点赞数',
|
||||||
|
left: 'left',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
legend: {},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
onZero: false,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#EE6666',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// prettier-ignore
|
||||||
|
data: upstatHistory.value?.map((f) => format(f.time, 'yyyy-MM-dd')),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '点赞数',
|
||||||
|
type: 'line',
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
},
|
||||||
|
data: upstatHistory.value?.map((f) => f.stats.likes),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '日增',
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 1,
|
||||||
|
step: 'start',
|
||||||
|
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series',
|
||||||
|
},
|
||||||
|
data: upstatLikeIncreace.map((f) => f.value),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
show: true,
|
||||||
|
realtime: true,
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
xAxisIndex: [0, 1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getFansHistory()
|
await getFansHistory()
|
||||||
await getGuardsHistory()
|
await getGuardsHistory()
|
||||||
|
await getUpstatHistory()
|
||||||
getOptions()
|
getOptions()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -247,5 +411,7 @@ onMounted(async () => {
|
|||||||
<NCard size="small">
|
<NCard size="small">
|
||||||
<VChart :option="fansOption" style="height: 200px" />
|
<VChart :option="fansOption" style="height: 200px" />
|
||||||
<VChart :option="guardsOption" style="height: 200px" />
|
<VChart :option="guardsOption" style="height: 200px" />
|
||||||
|
<VChart :option="upstatViewOption" style="height: 200px" />
|
||||||
|
<VChart :option="upstatLikeOption" style="height: 200px" />
|
||||||
</NCard>
|
</NCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -284,7 +284,6 @@ function reset() {
|
|||||||
switch (currentType.value) {
|
switch (currentType.value) {
|
||||||
case 'comment': {
|
case 'comment': {
|
||||||
commentUsers.value = JSON.parse(JSON.stringify(originCommentUsers.value))
|
commentUsers.value = JSON.parse(JSON.stringify(originCommentUsers.value))
|
||||||
console.log(originCommentUsers.value)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'forward': {
|
case 'forward': {
|
||||||
|
|||||||
Reference in New Issue
Block a user