This commit is contained in:
Megghy
2023-06-12 14:49:05 +08:00
parent 1b6bb7c248
commit 64a6bece5c
6 changed files with 463 additions and 109 deletions

View File

@@ -1,12 +1,14 @@
<template>
<NMessageProvider>
<NConfigProvider :theme-overrides="themeOverrides" style="height: 100vh">
<ViewerLayout v-if="layout == 'viewer'" />
<ManageLayout v-else-if="layout == 'manage'" />
<template v-else>
<RouterView />
</template>
</NConfigProvider>
<NNotificationProvider>
<NConfigProvider :theme-overrides="themeOverrides" :locale="zhCN" style="height: 100vh">
<ViewerLayout v-if="layout == 'viewer'" />
<ManageLayout v-else-if="layout == 'manage'" />
<template v-else>
<RouterView />
</template>
</NConfigProvider>
</NNotificationProvider>
</NMessageProvider>
</template>
@@ -14,7 +16,7 @@
import ViewerLayout from '@/views/ViewerLayout.vue'
import ManageLayout from '@/views/ManageLayout.vue'
import { useRoute } from 'vue-router'
import { NConfigProvider, NMessageProvider } from 'naive-ui'
import { NConfigProvider, NMessageProvider, NNotificationProvider, zhCN } from 'naive-ui'
import { computed } from 'vue'
const route = useRoute()

View File

@@ -24,14 +24,31 @@ export interface AccountInfo extends UserInfo {
biliVerifyCode?: string
emailVerifyUrl?: string
}
export interface SongsInfo {
id: string
export interface SongAuthorInfo {
name: string
author: string
url: string
cover: string
from: string
language: string
desc: string
tags: string[]
id: number
}
export enum SongFrom {
Custom,
Netease,
FiveSing,
}
export interface SongsInfo {
id: number
key: string
name: string
author: string[]
url: string
from: SongFrom
language: SongLanguage[]
description?: string
tags?: string[]
}
export enum SongLanguage {
Chinese, // 中文
English, // 英文
Japanese, // 日文
Spanish, // 西班牙文
French, // 法文
Other, //其他
}

View File

@@ -1,14 +1,58 @@
<script setup lang="ts">
import { SongsInfo } from '@/api/api-models'
import { DataTableColumns, NAvatar, NButton, NCollapseTransition, NDataTable, NInput, NList, NListItem, NSpace } from 'naive-ui'
import { onMounted, h, ref } from 'vue'
import { SongAuthorInfo, SongFrom, SongLanguage, SongsInfo } from '@/api/api-models'
import { QueryPostAPI } from '@/api/query'
import { SONG_API_URL } from '@/data/constants'
import {
DataTableColumns,
FormInst,
FormRules,
NAvatar,
NButton,
NCollapseTransition,
NDataTable,
NDivider,
NEllipsis,
NForm,
NFormItem,
NInput,
NList,
NListItem,
NModal,
NSelect,
NSpace,
NTag,
NText,
useMessage,
} from 'naive-ui'
import { onMounted, h, ref, watch } from 'vue'
import APlayer from 'vue3-aplayer'
const props = defineProps<{
songs: SongsInfo[]
canEdit?: boolean
}>()
const songsInternal = ref<{ [id: string]: SongsInfo }>({})
watch(
() => props.songs,
(newV) => {
let map = new Map()
newV.forEach((s) => {
s.tags?.forEach((t) => map.set(t, t))
})
map.forEach((tag) => {
tagsSelectOption.value.push({
label: tag,
value: tag,
})
})
songsInternal.value = newV
}
)
const songsInternal = ref(props.songs)
const message = useMessage()
const showModal = ref(false)
const updateSongModel = ref<SongsInfo>({} as SongsInfo)
const columns = ref<DataTableColumns<SongsInfo>>()
const aplayerMusic = ref<{
title: string
@@ -17,91 +61,115 @@ const aplayerMusic = ref<{
pic: string
}>()
const createColumns = (): DataTableColumns<SongsInfo> => [
{
title: '',
key: 'cover',
resizable: false,
width: 50,
render(data) {
return h(NAvatar, {
src: data.cover,
imgProps: {},
})
const formRef = ref<FormInst | null>(null)
const updateSongRules: FormRules = {
name: [
{
required: true,
message: '请输入歌曲名称',
},
],
password: [
{
required: true,
message: '请输入密码',
},
],
}
const songSelectOption = [
{
label: '中文',
value: SongLanguage.Chinese,
},
{
label: '日语',
value: SongLanguage.Japanese,
},
{
label: '英语',
value: SongLanguage.English,
},
{
label: '法语',
value: SongLanguage.French,
},
{
label: '西语',
value: SongLanguage.Spanish,
},
{
label: '其他',
value: SongLanguage.Other,
},
]
const tagsSelectOption = ref<{ label: string; value: string }[]>([])
const createColumns = (): DataTableColumns<SongsInfo> => [
{
title: '名称',
key: 'name',
resizable: true,
minWidth: 100,
sorter: 'default',
render(data) {
return props.canEdit
? h(NInput, {
value: data.name,
onUpdateValue(v) {
songsInternal.value[data.id].name = v
},
})
: h('span', data.name)
return h('span', data.name)
},
},
{
title: '作者',
key: 'author',
key: 'artist',
resizable: true,
render(data) {
return props.canEdit
? h(NInput, {
value: data.author,
onUpdateValue(v) {
songsInternal.value[data.id].author = v
},
})
: h('span', data.author)
return h(NSpace, { size: 5 }, () => data.author.map((a) => h(NTag, { bordered: false, size: 'small', type: 'info' }, () => a)))
},
},
{
title: '描述',
key: 'description',
resizable: true,
minWidth: 75,
render(data) {
return props.canEdit
? h(NInput, {
value: data.desc,
onUpdateValue(v) {
songsInternal.value[data.id].desc = v
},
})
: h('span', data.desc)
return h(NEllipsis, () => data.description)
},
},
{
title: '标签',
key: 'tags',
resizable: true,
render(data) {
return (data.tags?.length ?? 0) > 0 ? h(NSpace, { size: 5 }, () => data.tags?.map((a) => h(NTag, { bordered: false, size: 'small' }, () => a))) : null
},
},
{
title: '操作',
key: 'manage',
minWidth: 75,
disabled: () => !props.canEdit,
width: 200,
render(data) {
return h(NSpace, [
return h(NSpace, () => [
h(
NButton,
{
onClick: () => console.log(1),
size:'small',
onClick: () => {
updateSongModel.value = JSON.parse(JSON.stringify(data))
showModal.value = true
},
},
{
default: () => '保存',
default: () => '修改',
}
),
h(
NButton,
{
type: 'primary',
size:'small',
onClick: () => {
aplayerMusic.value = {
title: data.name,
artist: data.author,
artist: data.author.join('/') ?? '',
src: data.url,
pic: data.cover,
pic: '',
}
},
},
@@ -109,22 +177,85 @@ const createColumns = (): DataTableColumns<SongsInfo> => [
default: () => '播放',
}
),
h(
NButton,
{
type: 'error',
size:'small',
onClick: () => {
aplayerMusic.value = {
title: data.name,
artist: data.author.join('/') ?? '',
src: data.url,
pic: '',
}
},
},
{
default: () => '删除',
}
),
])
},
},
]
onMounted(() => {
props.songs.forEach((song) => {
songsInternal.value[song.id] = song
function renderCell(value: string | number) {
if (!value) {
return h(NText, { depth: 3 }, { default: () => '未填写' })
}
return value
}
async function updateSong() {
await QueryPostAPI<SongsInfo>(SONG_API_URL + 'update', {
key: updateSongModel.value.key,
song: updateSongModel.value,
}).then((data) => {
if (data.code == 200) {
const index = songsInternal.value.findIndex((s) => s.key == data.data.key)
songsInternal.value.splice(index, 1, data.data)
message.success('已更新歌曲信息')
} else {
message.error('未能更新歌曲信息: ' + data.message)
}
})
}
onMounted(() => {
songsInternal.value = props.songs
columns.value = createColumns()
})
</script>
<template>
歌单 {{ songs.length }}
歌单 {{ songsInternal.length }}
<Transition>
<APlayer v-if="aplayerMusic" :music="aplayerMusic" />
</Transition>
<NDataTable :columns="columns" :data="songs"> </NDataTable>
<NDataTable :columns="columns" :data="songsInternal"> </NDataTable>
<NModal v-model:show="showModal" style="max-width: 600px" preset="card">
<template #header> 修改信息 </template>
<NForm ref="formRef" :rules="updateSongRules" :model="updateSongModel" :render-cell="renderCell">
<NFormItem path="name" label="名称">
<NInput v-model:value="updateSongModel.name" autosize style="min-width: 200px" placeholder="就是歌曲名称" />
</NFormItem>
<NFormItem path="author" label="作者">
<NSelect v-model:value="updateSongModel.author" filterable multiple tag placeholder="输入,按回车确认" :show-arrow="false" :show="false" />
</NFormItem>
<NFormItem path="description" label="备注">
<NInput v-model:value="updateSongModel.description" placeholder="可选" :maxlength="250" show-count autosize style="min-width: 300px" clearable />
</NFormItem>
<NFormItem path="language" label="语言">
<NSelect v-model:value="updateSongModel.language" multiple :options="songSelectOption" placeholder="可选" />
</NFormItem>
<NFormItem path="tags" label="标签">
<NSelect v-model:value="updateSongModel.tags" filterable multiple tag placeholder="可选,按回车确认" :show-arrow="false" :show="false" :options="tagsSelectOption" />
</NFormItem>
<NFormItem path="url" label="链接">
<NInput v-model:value="updateSongModel.url" placeholder="可选, 后缀为mp3、wav、ogg时将会尝试播放, 否则会在新页面打开" :disabled="updateSongModel.from != SongFrom.Custom" />
</NFormItem>
</NForm>
<NDivider style="margin: 10px" />
<NButton @click="updateSong"> 更新 </NButton>
</NModal>
</template>

View File

@@ -8,3 +8,4 @@ export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0'
export const USER_API_URL = `${BASE_API}user/`
export const ACCOUNT_API_URL = `${BASE_API}account/`
export const BILI_API_URL = `${BASE_API}bili/`
export const SONG_API_URL = `${BASE_API}song-list/`

View File

@@ -1,11 +1,241 @@
<script setup lang="ts">
import { SongsInfo } from '@/api/api-models'
import { useAccount } from '@/api/account'
import { SongFrom, SongLanguage, SongsInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import SongList from '@/components/SongList.vue'
import { ref } from 'vue'
import { SONG_API_URL } from '@/data/constants'
import { ca } from 'date-fns/locale'
import { FormInst, FormRules, NButton, NDivider, NForm, NFormItem, NInput, NInputGroup, NInputGroupLabel, NModal, NSelect, NSpace, NSpin, NTabPane, NTabs, NTag, NTransfer, useMessage } from 'naive-ui'
import { Option } from 'naive-ui/es/transfer/src/interface'
import { computed, onMounted, ref } from 'vue'
const message = useMessage()
const accountInfo = useAccount()
const showModal = ref(false)
const neteaseIdInput = ref()
const isModalLoading = ref(false)
const neteaseSongListId = computed(() => {
try {
const url = new URL(neteaseIdInput.value)
console.log(url)
if (url.host == 'music.163.com') {
let regex = /id=(\d+)/
// 使用exec方法在链接中查找匹配项
let match = regex.exec(neteaseIdInput.value)
// 如果找到了匹配项那么match[1]就是分组1的值也就是id的值
if (match) {
return Number(match[1])
}
}
} catch (err) {}
try {
return Number(neteaseIdInput.value)
} catch {}
return null
})
const songs = ref<SongsInfo[]>([])
const neteaseSongs = ref<SongsInfo[]>([])
const neteaseSongsOptions = ref<Option[]>([])
const selectedNeteaseSongs = ref<string[]>([])
const formRef = ref<FormInst | null>(null)
const addSongModel = ref<SongsInfo>({} as SongsInfo)
const addSongRules: FormRules = {
name: [
{
required: true,
message: '请输入歌曲名称',
},
],
password: [
{
required: true,
message: '请输入密码',
},
],
}
const songSelectOption = [
{
label: '中文',
value: SongLanguage.Chinese,
},
{
label: '日语',
value: SongLanguage.Japanese,
},
{
label: '英语',
value: SongLanguage.English,
},
{
label: '法语',
value: SongLanguage.French,
},
{
label: '西语',
value: SongLanguage.Spanish,
},
{
label: '其他',
value: SongLanguage.Other,
},
]
async function addCustomSong() {
isModalLoading.value = true
formRef.value
?.validate()
.then(async () => {
await addSongs([addSongModel.value], SongFrom.Custom)
.then((data) => {
if (data.code == 200) {
if (data.data.length == 1) {
message.success('成功添加歌曲: ' + addSongModel.value.name)
songs.value.push(data.data[0])
addSongModel.value = {} as SongsInfo
} else {
message.error('未能添加歌曲, 已存在相同名称的曲目')
}
} else {
message.error('添加失败: ' + data.message)
}
})
.catch((err) => {
message.error('添加失败')
console.error(err)
})
})
.finally(() => {
isModalLoading.value = false
})
}
async function addNeteaseSongs() {
isModalLoading.value = true
const selected = neteaseSongs.value.filter((s) => selectedNeteaseSongs.value.find((select) => s.key == select))
await addSongs(selected, SongFrom.Netease)
.then((data) => {
if (data.code == 200) {
message.success(`已添加 ${data.data.length} 首歌曲`)
songs.value.push(...data.data)
neteaseSongsOptions.value = neteaseSongs.value.map((s) => ({
label: `${s.name} - ${s.author.join('/')}`,
value: s.key,
disabled: songs.value.findIndex((exist) => exist.id == s.id) > -1 || data.data.findIndex((add) => add.id == s.id) > -1,
}))
} else {
message.error('添加失败: ' + data.message)
}
})
.catch((err) => {
message.error('添加失败')
console.error(err)
})
.finally(() => {
isModalLoading.value = false
})
}
async function addSongs(songsShoudAdd: SongsInfo[], from: SongFrom) {
return QueryPostAPI<SongsInfo[]>(
SONG_API_URL + 'add',
songsShoudAdd.map((s) => ({
Name: s.name,
Id: from == SongFrom.Custom ? -1 : s.id,
From: from,
Author: s.author,
Url: s.url,
Description: s.description,
}))
)
}
async function getNeteaseSongList() {
isModalLoading.value = true
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get-netease-list', {
id: neteaseSongListId.value,
})
.then((data) => {
if (data.code == 200) {
neteaseSongs.value = data.data
neteaseSongsOptions.value = data.data.map((s) => ({
label: `${s.name} - ${s.author.join('/')}`,
value: s.key,
disabled: songs.value.findIndex((exist) => exist.id == s.id) > -1,
}))
message.success(`成功获取歌曲信息, 共 ${data.data.length} 条, 歌单中已存在 ${neteaseSongsOptions.value.filter((s) => s.disabled).length}`)
}
})
.catch((err) => {
console.error(err)
message.error(err)
})
.finally(() => {
isModalLoading.value = false
})
}
async function getSongs() {
await QueryGetAPI<any>(SONG_API_URL + 'get', {
uId: accountInfo.value?.biliId,
}).then((data) => {
if (data.code == 200) {
songs.value = data.data
}
})
}
onMounted(async () => {
await getSongs()
})
</script>
<template>
<NButton @click="showModal = true"> 添加歌曲 </NButton>
<NModal v-model:show="showModal" style="max-width: 600px" preset="card">
<template #header> 添加歌曲 </template>
<NSpin :show="isModalLoading">
<NTabs default-value="custom" animated>
<NTabPane name="custom" tab="手动录入">
<NForm ref="formRef" :rules="addSongRules" :model="addSongModel">
<NFormItem path="name" label="名称">
<NInput v-model:value="addSongModel.name" autosize style="min-width: 200px" placeholder="就是歌曲名称" />
</NFormItem>
<NFormItem path="author" label="作者">
<NSelect v-model:value="addSongModel.author" filterable multiple tag placeholder="输入,按回车确认" :show-arrow="false" :show="false" />
</NFormItem>
<NFormItem path="description" label="备注">
<NInput v-model:value="addSongModel.description" placeholder="可选" :maxlength="250" show-count autosize style="min-width: 300px" clearable />
</NFormItem>
<NFormItem path="language" label="语言">
<NSelect v-model:value="addSongModel.language" multiple :options="songSelectOption" placeholder="可选" />
</NFormItem>
<NFormItem path="url" label="链接">
<NInput v-model:value="addSongModel.url" placeholder="可选, 后缀为mp3、wav、ogg时将会尝试播放, 否则会在新页面打开" />
</NFormItem>
</NForm>
<NButton type="primary" @click="addCustomSong"> 添加 </NButton>
</NTabPane>
<NTabPane name="netease" tab="从网易云歌单导入">
<NInput clearable style="width: 100%" autosize :status="neteaseSongListId ? 'success' : 'error'" v-model:value="neteaseIdInput" placeholder="直接输入歌单Id或者网页链接">
<template #suffix>
<NTag v-if="neteaseSongListId" type="success" size="small"> 歌单Id: {{ neteaseSongListId }} </NTag>
</template>
</NInput>
<NDivider style="margin: 10px" />
<NButton type="primary" @click="getNeteaseSongList"> 获取 </NButton>
<template v-if="neteaseSongsOptions.length > 0">
<NDivider style="margin: 10px" />
<NTransfer style="height: 500px" ref="transfer" v-model:value="selectedNeteaseSongs" :options="neteaseSongsOptions" source-filterable />
<NDivider style="margin: 10px" />
<NButton type="primary" @click="addNeteaseSongs"> 添加到歌单 | {{ selectedNeteaseSongs.length }} </NButton>
</template>
</NTabPane>
<NTabPane name="5sing" tab="从5sing搜索"> </NTabPane>
</NTabs>
</NSpin>
</NModal>
<SongList :songs="songs" />
</template>

View File

@@ -1,55 +1,28 @@
<script setup lang="ts">
import { SongsInfo } from '@/api/api-models'
import { QueryGetPaginationAPI } from '@/api/query'
import { QueryGetAPI, QueryGetPaginationAPI } from '@/api/query'
import SongList from '@/components/SongList.vue'
import { USER_API_URL } from '@/data/constants'
import { SONG_API_URL, USER_API_URL } from '@/data/constants'
import { onMounted, ref } from 'vue'
import { useRouteParams } from '@vueuse/router'
import { useAccount } from '@/api/account'
const accountInfo = useAccount()
const songs = ref<SongsInfo[]>()
const uId = useRouteParams('id', '-1', { transform: Number })
async function RequestData() {
songs.value = [
{
id: '1',
name: 'test',
author: '雪路',
url: 'https://music.163.com/song/media/outer/url?id=1995844771.mp3',
cover: 'https://ukamnads.icu/file/components.png',
from: '网易云',
language: '中文',
desc: 'xuelu',
tags: ['hao'],
},
{
id: '2',
name: 'test2',
author: '雪路2',
url: 'https://music.163.com/song/media/outer/url?id=1995844771.mp3',
cover: 'https://ukamnads.icu/file/components.png',
from: '网易云2',
language: '中文2',
desc: 'xuelu',
tags: ['hao'],
},
]
await QueryGetPaginationAPI<SongsInfo[]>(`${USER_API_URL}info`, {
async function getSongs() {
await QueryGetAPI<SongsInfo[]>(SONG_API_URL + 'get', {
uId: uId.value,
}).then((data) => {
if (data.code == 200) {
songs.value = data.data
}
})
.then((result) => {
if (result.code == 200) {
songs.value = result.data.datas
} else {
}
})
.catch((err) => {
console.error(err)
})
}
onMounted(async () => {
await RequestData()
await getSongs()
})
</script>