2026-04-01 10:25:14 +08:00

429 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-wrapper">
<el-form inline :model="search">
<el-form-item label="名称/标题">
<el-input v-model="search.title" />
</el-form-item>
<el-form-item label="所属歌单">
<el-select v-model="search.libIds" multiple collapse-tags>
<el-option v-for="musicLib in musicLibs" :key="musicLib._id" :value="musicLib._id" :label="musicLib.name" />
</el-select>
</el-form-item>
<el-form-item label="文件类型">
<el-select v-model="search.exts" multiple >
<el-option v-for="ext in exts" :key="ext" :value="ext" :label="ext" />
</el-select>
</el-form-item>
<el-form-item label="唱片集">
<el-input v-model="search.album" />
</el-form-item>
<el-form-item label="艺术家">
<el-input v-model="search.artist" />
</el-form-item>
</el-form>
<div class="btn-container">
<el-button type="success" plain icon="VideoPlay" @click="playMusic">播放</el-button>
<el-button type="primary" icon="Upload" @click="openUploadModal">上传音乐</el-button>
<el-button type="warning" plain icon="Notebook" @click="libDrawerVisible = true">歌单管理</el-button>
<div class="search-btn">
<el-button type="primary" @click="loadDataBase(true)" icon="Search">搜索</el-button>
<el-button @click="reset" icon="RefreshLeft">重置</el-button>
</div>
</div>
<div class="table-container">
<el-table :data="musicData" v-loading="loading" stripe @selection-change="dataSelect">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="名称" show-overflow-tooltip role=""/>
<el-table-column prop="ext" label="类型" width="100" />
<el-table-column prop="size" label="文件大小" width="150">
<template #default="scope">
{{ prettyBytes(scope.row.size) }}
</template>
</el-table-column>
<el-table-column prop="title" label="标题" show-overflow-tooltip />
<el-table-column prop="album" label="唱片集" />
<el-table-column prop="artist" label="艺术家" />
<el-table-column prop="libId" label="所属歌单" >
<template #default="scope">
<template v-if="scope.row.isEditing && currentRow">
<el-select v-model="currentRow.libId">
<el-option v-for="musicLib in musicLibs" :key="musicLib._id" :value="musicLib._id" :label="musicLib.name" />
</el-select>
<el-button link icon="Check" type="primary" @click="saveMusicLib(scope.row)"></el-button>
<el-button link icon="Close" type="primary" @click="scope.row.isEditing = false"></el-button>
</template>
<template v-else>
{{ findMusicLib(scope.row.libId) }}
<el-button link icon="Edit" type="primary" @click="updateLib(scope.row)"></el-button>
</template>
</template>
</el-table-column>
<el-table-column prop="lyricId" label="歌词" width="120" >
<template #default="scope">
<div style="width: 18px">
<Check v-if="scope.row.lyricId" />
<Close v-else/>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="230" >
<template #default="scope">
<el-button link icon="Document" @click="updateLyric(scope.row)" title="歌词"></el-button>
<el-button link icon="Download" @click="download(scope.row)" title="下载"></el-button>
<el-button link type="danger" icon="Delete" @click="remove(scope.row)" title="删除"></el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="page-container">
<el-pagination background
:page-sizes="store.state.pageSizeOpts"
:layout="store.state.pageLayout"
:current-page="search.pageNum"
:total="total"
@size-change="pageSizeChange"
@current-change="pageChange">
</el-pagination>
</div>
<el-dialog v-model="modifyLyricModal" title="编辑歌词" :width="600" >
<el-form v-loading="lyricLoading" ref="lyricForm" :model="lyricFormData" :rules="lyricRuleValidate" :label-width="120">
<el-form-item label="网易云ID" prop="cloudId">
<el-input v-model="lyricFormData.cloudId" />
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="lyricFormData.name" />
</el-form-item>
<el-form-item label="歌词" prop="lyric">
<el-input v-model="lyricFormData.lyric" type="textarea" :rows="4"/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="modifyLyricModal = false">取消</el-button>
<el-button type="primary" @click="saveLyric" :loading="modalLoading">确定</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="uploadModal" title="上传音乐" :width="550" @closed="uploadModalClosed" >
<el-form :label-width="80">
<el-form-item label="歌单">
<el-select v-model="libIdSelected">
<el-option v-for="musicLib in musicLibs" :key="musicLib._id" :value="musicLib._id" :label="musicLib.name" />
</el-select>
</el-form-item>
<el-upload
ref="musicUpload"
action="/api/v2/music/upload"
name="file"
accept=".mp3,.flac"
:headers="{token: store.state.loginInfo.token}"
:on-success="uploadSuccess"
:on-error="uploadError"
:auto-upload="false"
multiple
drag
:data="{libId: libIdSelected}">
<div class="el-upload__text">
拖拽到此处或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
可选择 mp3/flac 文件
</div>
</template>
</el-upload>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="uploadModal = false">取消</el-button>
<el-button type="primary" @click="uploadMusic" >开始上传</el-button>
</span>
</template>
</el-dialog>
<el-drawer v-model="libDrawerVisible" title="歌单管理" size="900px">
<el-button type="primary" icon="Plus" @click="addLibRow" style="margin-bottom: 12px">添加歌单</el-button>
<el-table :data="libTableData" stripe>
<el-table-column prop="name" label="名称">
<template #default="scope">
<el-input v-if="scope.row._isNew" v-model="scope.row.name" placeholder="歌单名称" />
<span v-else>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="path" label="路径">
<template #default="scope">
<el-input v-if="scope.row._isNew" v-model="scope.row.path" placeholder="music/歌单名/" />
<span v-else>{{ scope.row.path }}</span>
</template>
</el-table-column>
<el-table-column prop="musicCount" label="歌曲数量" width="100" />
<el-table-column label="操作" width="160">
<template #default="scope">
<el-button v-if="scope.row._isNew" link type="primary" icon="Check" @click="saveLib(scope.row)" :loading="libSaving">保存</el-button>
<el-button v-if="scope.row._isNew" link icon="Close" @click="cancelAddLib">取消</el-button>
<el-button v-if="!scope.row._isNew" link type="danger" icon="Delete" @click="removeLib(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-drawer>
<el-drawer v-model="musicPlaying" :close-on-click-modal="false" size="40%" title="播放音乐">
<a-player v-if="musicPlaying" ref="player" autoplay showLrc :list="musicList" v-model:music="currentMusic" @play="musicPlay"/>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useStore } from 'vuex'
import { useBaseList } from '@/model/baselist'
import { MsgResult, Page } from '@/model/common.dto'
import { UploadInstance, ElMessage, ElMessageBox } from 'element-plus'
import { MusicModel, MusicLibModel, MusicLyricModel, MusicPlayerItem } from '@/model/api/music'
import APlayer from './aplayer/vue-aplayer.vue'
import prettyBytes from 'pretty-bytes'
import type { VForm } from '@/types'
import http from '@/utils/http'
class MusicPage extends Page {
exts: string[] = []
title?: string
album?: string
artist?: string
libIds?: string[] = []
reset() {
super.reset()
this.exts = []
this.title = undefined
this.album = undefined
this.artist = undefined
this.libIds = []
}
}
const store = useStore()
const { loading, modalLoading, total, search, setLoadData, loadDataBase, reset, pageChange, pageSizeChange } = useBaseList(new MusicPage())
const currentRow = ref<MusicModel | null>(null)
const libIdSelected = ref<string | null>(null)
const exts = ref<string[]>([])
const musicLibs = ref<MusicLibModel[]>([])
const musicData = ref<MusicModel[]>([])
const uploadModal = ref(false)
const modifyLyricModal = ref(false)
const lyricFormData = ref<MusicLyricModel>({})
const musicPlaying = ref(false)
const musicList = ref<MusicPlayerItem[]>([])
const currentMusic = ref<MusicPlayerItem>()
const lyricForm = ref<VForm>()
const musicUpload = ref<UploadInstance>()
const player = ref<any>()
const lyricLoading = ref(false)
const libDrawerVisible = ref(false)
const libTableData = ref<(MusicLibModel & { _isNew?: boolean })[]>([])
const libSaving = ref(false)
const lyricRuleValidate = {
cloudId: [
{ required: true, message: '请输入网易云ID', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入名称', trigger: 'blur' }
],
lyric: [
{ required: true, message: '请输入歌词正文', trigger: 'blur' }
],
}
let selectedIds: string[] = []
async function loadData() {
loading.value = true
const data = await http.get<MusicPage, any>('/api/v2/music/list', {params: search})
selectedIds = []
loading.value = false
total.value = data.total
musicData.value = data.list
}
setLoadData(loadData)
function dataSelect(selection: MusicModel[]) {
selectedIds = selection.map(item => item._id)
}
function findMusicLib(value: string): string | null {
const musicLib = musicLibs.value.find(item => item._id === value)
return musicLib ? musicLib.name : null
}
async function playMusic() {
try {
const data = await http.get<any, any>('/api/v2/music/list/all', {params: selectedIds.length ? {ids: selectedIds} : search})
musicList.value = data.map((item: MusicModel, index: number) => {
const musicItem: MusicPlayerItem = {
id: index,
title: item.title || item.name,
artist: item.artist,
album: item.album,
src: `/api/v2/common/music/load/${item._id}`,
pic: `/api/v2/common/music/album/${item._id}`,
}
if (item.lyricId) {
musicItem.lrc = `${location.origin}/api/v2/common/music/lyric/${item.lyricId}`
}
return musicItem
})
currentMusic.value = musicList.value[0]
musicPlaying.value = true
} catch (err) {
console.error(err)
ElMessage.error('获取播放列表失败')
}
}
function updateLib(row: MusicModel) {
currentRow.value = { ...row }
row.isEditing = true
}
function download(row: MusicModel) {
const link = document.createElement('a')
link.setAttribute('href', `/api/v2/common/music/load/${row._id}`)
link.setAttribute('download', row.name)
link.setAttribute('target', '_blank')
link.click()
}
function remove(row: MusicModel) {
ElMessageBox.confirm(`是否确认删除 ${row.name} `, '确认删除', {type: 'warning'}).then(async () => {
await http.delete<{params: {id: string}}, any>('/api/v2/music/delete', {params: {id: row._id}})
ElMessage.success("删除成功")
loadData()
}).catch(() => {})
}
async function updateLyric(row: MusicModel) {
currentRow.value = { ...row }
lyricFormData.value = {}
modifyLyricModal.value = true
if (row.lyricId) {
lyricLoading.value = true
try {
const data = await http.get<any, any>('/api/v2/music/lyric/get', {params: {lyricId: row.lyricId}})
data.cloudId = data.cloudId ? data.cloudId.toString() : null
lyricFormData.value = data
} finally {
lyricLoading.value = false
}
}
}
async function saveLyric() {
lyricForm.value?.validate(async (valid: boolean) => {
if (!valid) return
modalLoading.value = true
await http.post<MusicLyricModel, any>(`/api/v2/music/lyric/save?musicId=${currentRow.value ? currentRow.value._id : ''}`, lyricFormData.value)
modalLoading.value = false
modifyLyricModal.value = false
ElMessage.success("歌词保存成功")
loadData()
lyricFormData.value = {}
})
}
async function saveMusicLib(row: MusicModel) {
if (!currentRow.value) return
await http.post<{id: string, libId: string}, any>('/api/v2/music/lib/update', {id: currentRow.value._id, libId: currentRow.value.libId})
ElMessage.success("歌单更新成功")
row.libId = currentRow.value.libId
row.isEditing = false
}
function openUploadModal() {
uploadModal.value = true
libIdSelected.value = null
}
async function uploadMusic() {
if (!libIdSelected.value) {
ElMessage.warning('请选择歌单')
return
}
musicUpload.value?.submit()
}
function uploadSuccess(response: MsgResult) {
if (response.code === 0) {
ElMessage.success(response.message)
loadData()
} else {
ElMessage.warning(response.message)
}
}
function uploadError(error: Error) {
ElMessage.error(error.message)
}
function uploadModalClosed() {
musicUpload.value?.clearFiles()
}
function musicPlay() {
if (!('mediaSession' in window.navigator) || !currentMusic.value) return
const currentId = currentMusic.value.id
navigator.mediaSession.metadata = new MediaMetadata({
title: currentMusic.value.title,
artist: currentMusic.value.artist,
album: currentMusic.value.album,
artwork: [{src: location.origin + currentMusic.value.pic}]
})
navigator.mediaSession.setActionHandler('play', () => {
player.value.play()
})
navigator.mediaSession.setActionHandler('pause', () => {
player.value.pause()
})
navigator.mediaSession.setActionHandler('previoustrack', () => {
if (currentId === 0) {
player.value.switch(musicList.value.length - 1)
} else {
player.value.switch(currentId - 1)
}
})
navigator.mediaSession.setActionHandler('nexttrack', () => {
if (currentId === musicList.value.length - 1) {
player.value.switch(0)
} else {
player.value.switch(currentId + 1)
}
})
}
function loadLibs() {
return http.get<never, any>('/api/v2/music/lib/list').then(data => {
musicLibs.value = data
libTableData.value = data.map((item: MusicLibModel) => ({ ...item }))
})
}
function addLibRow() {
if (libTableData.value.some(item => (item as any)._isNew)) return
libTableData.value.unshift({ _id: '', name: '', path: '', musicCount: 0, _isNew: true })
}
function cancelAddLib() {
libTableData.value = libTableData.value.filter(item => !(item as any)._isNew)
}
async function saveLib(row: MusicLibModel & { _isNew?: boolean }) {
if (!row.name || !row.path) {
ElMessage.warning('请输入歌单名称和路径')
return
}
libSaving.value = true
try {
await http.post<any, any>('/api/v2/music/lib/add', { name: row.name, path: row.path })
ElMessage.success('歌单创建成功')
await loadLibs()
} finally {
libSaving.value = false
}
}
function removeLib(row: MusicLibModel) {
ElMessageBox.confirm(`是否确认删除歌单「${row.name}」?`, '确认删除', { type: 'warning' }).then(async () => {
await http.delete<any, any>('/api/v2/music/lib/delete', { params: { id: row._id } })
ElMessage.success('歌单删除成功')
await loadLibs()
}).catch(() => {})
}
// created
loadLibs().then(() => {
loadData()
})
http.get<never, any>('/api/v2/music/listExts').then(data => {
exts.value = data
})
</script>