依赖升级:
- element-plus 2.2.28 -> 2.13.5
- vue 3.2.45 -> 3.5.30
- vue-router 4.0.11 -> 4.6.4
- axios 0.22 -> 1.13.6
- echarts 5.2.1 -> 5.6.0
- hls.js 1.3 -> 1.6.15
- typescript 4.5.5 -> 5.9.3
破坏性变更修复:
- axios 1.x: config.headers.token= 改为 config.headers.set()
- element-plus locale 路径: lib/ -> es/
- Article.vue: Node 改为具名导入 { Node }
- main.ts: 路由守卫参数补全显式类型声明
- SystemConfig.vue: 移除废弃的 clearValidate 包装函数
TypeScript 配置修复 (tsconfig.json):
- moduleResolution: node -> bundler (支持 exports 字段, 解决 vue-router .mts 类型文件)
- 新增 skipLibCheck: true (规避第三方库内部类型冲突)
- 新增 vuex paths 映射 (vuex exports 字段缺少 types 条件)
样式适配 (common.less):
- el-dialog 新增 padding: 0, 消除升级后产生的白边
- el-form--inline 内 el-select 补 min-width: 160px, 修复搜索栏下拉框过窄问题
清理:
- 删除 src/vuex.d.ts (已无用的 vuex store 类型扩充文件)
366 lines
13 KiB
Vue
366 lines
13 KiB
Vue
<template>
|
||
<div>
|
||
<el-form inline :model="search">
|
||
<el-form-item label="名称">
|
||
<el-input v-model="search.name" />
|
||
</el-form-item>
|
||
<el-form-item label="所属歌单">
|
||
<el-select v-model="search.lib_id" 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.ext" 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.title" />
|
||
</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>
|
||
<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="lib_id" label="所属歌单" >
|
||
<template #default="scope">
|
||
<template v-if="scope.row.isEditing && currentRow">
|
||
<el-select v-model="currentRow.lib_id">
|
||
<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.lib_id) }}
|
||
<el-button link icon="Edit" type="primary" @click="updateLib(scope.row)"></el-button>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="lyric_id" label="歌词" width="120" >
|
||
<template #default="scope">
|
||
<div style="width: 18px">
|
||
<Check v-if="scope.row.lyric_id" />
|
||
<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 ref="lyricForm" :model="lyricFormData" :rules="lyricRuleValidate" :label-width="120">
|
||
<el-form-item label="网易云ID" prop="cloud_id">
|
||
<el-input v-model="lyricFormData.cloud_id" />
|
||
</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="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 {
|
||
name?: string
|
||
ext: string[] = []
|
||
title?: string
|
||
album?: string
|
||
artist?: string
|
||
lib_id?: string[] = []
|
||
reset() {
|
||
super.reset()
|
||
this.name = undefined
|
||
this.ext = []
|
||
this.title = undefined
|
||
this.album = undefined
|
||
this.artist = undefined
|
||
this.lib_id = []
|
||
}
|
||
}
|
||
|
||
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 lyricRuleValidate = {
|
||
cloud_id: [
|
||
{ 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/v1/music/list', {params: search})
|
||
selectedIds = []
|
||
loading.value = false
|
||
total.value = data.total
|
||
musicData.value = data.data
|
||
}
|
||
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/v1/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.lyric_id) {
|
||
musicItem.lrc = `${location.origin}/api/v2/common/music/lyric/${item.lyric_id}`
|
||
}
|
||
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 () => {
|
||
const data = await http.delete<{params: {id: string}}, any>('/api/v2/music/delete', {params: {id: row._id}})
|
||
ElMessage.success(data.message)
|
||
loadData()
|
||
}).catch(() => {})
|
||
}
|
||
async function updateLyric(row: MusicModel) {
|
||
currentRow.value = { ...row }
|
||
modifyLyricModal.value = true
|
||
if (row.lyric_id) {
|
||
const data = (await http.get<any, any>('/api/v1/music/lyric/get', {params: {lyricId: row.lyric_id}}))
|
||
data.cloud_id = data.cloud_id ? data.cloud_id.toString() : null
|
||
lyricFormData.value = data
|
||
} else {
|
||
lyricFormData.value = {}
|
||
}
|
||
}
|
||
async function saveLyric() {
|
||
lyricForm.value?.validate(async (valid: boolean) => {
|
||
if (!valid) return
|
||
modalLoading.value = true
|
||
const data = await http.post<MusicLyricModel, any>(`/api/v1/music/lyric/save?musicId=${currentRow.value ? currentRow.value._id : ''}`, lyricFormData.value)
|
||
modalLoading.value = false
|
||
modifyLyricModal.value = false
|
||
ElMessage.success(data.message)
|
||
loadData()
|
||
lyricFormData.value = {}
|
||
})
|
||
}
|
||
async function saveMusicLib(row: MusicModel) {
|
||
if (!currentRow.value) return
|
||
const data = await http.post<{id: string, libId: string}, any>('/api/v2/music/updateLib', {id: currentRow.value._id, libId: currentRow.value.lib_id})
|
||
ElMessage.success(data.message)
|
||
row.lib_id = currentRow.value.lib_id
|
||
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)
|
||
}
|
||
})
|
||
}
|
||
|
||
// created
|
||
http.get<never, any>('/api/v1/music/listLibs').then(data => {
|
||
musicLibs.value = data
|
||
loadData()
|
||
})
|
||
http.get<never, any>('/api/v1/music/listExts').then(data => {
|
||
exts.value = data
|
||
})
|
||
</script> |