灌糖包子 90a6153776
升级依赖 & 修复 TS 类型解析 & 适配 element-plus 样式变化
依赖升级:
- 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 类型扩充文件)
2026-03-20 00:33:09 +08:00

366 lines
13 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>
<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>