feat: 音乐管理功能优化
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tweenjs/tween.js": "^23.1.2",
|
"@tweenjs/tween.js": "^23.1.2",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"axios": "^1.7.8",
|
"axios": "^1.7.8",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"zod": "^4.1.11"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^5.4.1",
|
"@antfu/eslint-config": "^6.4.1",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@eslint/js": "^9.15.0",
|
"@eslint/js": "^9.15.0",
|
||||||
"@iconify-json/ep": "^1.2.1",
|
"@iconify-json/ep": "^1.2.1",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"@typescript-eslint/parser": "^8.16.0",
|
"@typescript-eslint/parser": "^8.16.0",
|
||||||
"@vitejs/plugin-legacy": "^7.2.1",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^4.0.15",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"baseline-browser-mapping": "^2.8.32",
|
"baseline-browser-mapping": "^2.8.32",
|
||||||
|
|||||||
793
pnpm-lock.yaml
generated
793
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
src/components.d.ts
vendored
2
src/components.d.ts
vendored
@@ -11,9 +11,9 @@ declare module 'vue' {
|
|||||||
DaiysuiTable: typeof import('./components/DaiysuiTable/index.vue')['default']
|
DaiysuiTable: typeof import('./components/DaiysuiTable/index.vue')['default']
|
||||||
Dialog: typeof import('./components/Dialog/index.vue')['default']
|
Dialog: typeof import('./components/Dialog/index.vue')['default']
|
||||||
EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default']
|
EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default']
|
||||||
|
FileUpload: typeof import('./components/FileUpload/index.vue')['default']
|
||||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||||
ImageSync: typeof import('./components/ImageSync/index.vue')['default']
|
ImageSync: typeof import('./components/ImageSync/index.vue')['default']
|
||||||
ImageUpload: typeof import('./components/ImageUpload/index.vue')['default']
|
|
||||||
Loading: typeof import('./components/Loading/index.vue')['default']
|
Loading: typeof import('./components/Loading/index.vue')['default']
|
||||||
PageHeader: typeof import('./components/PageHeader/index.vue')['default']
|
PageHeader: typeof import('./components/PageHeader/index.vue')['default']
|
||||||
PlayMusic: typeof import('./components/PlayMusic/index.vue')['default']
|
PlayMusic: typeof import('./components/PlayMusic/index.vue')['default']
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import type { IFileData } from './type'
|
import type { IFileData } from './type'
|
||||||
import { FileImage, X } from 'lucide-vue-next'
|
import { ListMusic, Upload, X } from 'lucide-vue-next'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { readFileData } from '@/utils/file'
|
import { readFileData } from '@/utils/file'
|
||||||
|
|
||||||
@@ -32,13 +32,15 @@ function removeFile() {
|
|||||||
<div class="w-full h-full flex flex-col items-center mt-6">
|
<div class="w-full h-full flex flex-col items-center mt-6">
|
||||||
<input
|
<input
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
|
:disabled="fileData !== null"
|
||||||
type="file" class="w-full bg-red-400/50 max-h-52 cursor-pointer absolute" style="display: none;" :accept="limitType"
|
type="file" class="w-full bg-red-400/50 max-h-52 cursor-pointer absolute" style="display: none;" :accept="limitType"
|
||||||
@change="handleFileChange"
|
@change="handleFileChange"
|
||||||
>
|
>
|
||||||
<label for="file-upload" class="w-full h-52 cursor-pointer border-2 border-dashed flex items-center justify-center overflow-hidden">
|
<label for="file-upload" :class="fileData ? 'cursor-not-allowed' : null" class="w-full h-52 cursor-pointer border-2 border-dashed flex items-center justify-center overflow-hidden">
|
||||||
<img v-if="fileData" class="w-full object-cover stroke-0" :src="fileData.dataUrl" alt="">
|
<img v-if="fileData && fileData.type.includes('image')" class="w-full object-cover stroke-0" :src="fileData.dataUrl" alt="">
|
||||||
|
<ListMusic v-else-if="fileData && fileData.type.includes('audio')" class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
|
||||||
<div v-else class="w-full h-full flex justify-center items-center flex-col gap-4">
|
<div v-else class="w-full h-full flex justify-center items-center flex-col gap-4">
|
||||||
<FileImage class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
|
<Upload class="w-2/3 h-2/3 stroke-1 text-gray-500/50" />
|
||||||
<span class="btn btn-neutral">点击上传</span>
|
<span class="btn btn-neutral">点击上传</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -4,10 +4,19 @@ const imageDbStore = localforage.createInstance({
|
|||||||
name: 'imgStore',
|
name: 'imgStore',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const audioDbStore = localforage.createInstance({
|
||||||
|
name: 'audioStore',
|
||||||
|
})
|
||||||
|
|
||||||
async function clearImageDbStore() {
|
async function clearImageDbStore() {
|
||||||
await imageDbStore.clear()
|
await imageDbStore.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clearAudioDbStore() {
|
||||||
|
await audioDbStore.clear()
|
||||||
|
}
|
||||||
|
|
||||||
export function clearAllDbStore() {
|
export function clearAllDbStore() {
|
||||||
clearImageDbStore()
|
clearImageDbStore()
|
||||||
|
clearAudioDbStore()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import type { IFileData } from '@/components/ImageUpload/type'
|
import type { IFileData } from '@/components/FileUpload/type'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import CustomDialog from '@/components/Dialog/index.vue'
|
import CustomDialog from '@/components/Dialog/index.vue'
|
||||||
import ImageUpload from '@/components/ImageUpload/index.vue'
|
import FileUpload from '@/components/FileUpload/index.vue'
|
||||||
import useStore from '@/store'
|
import useStore from '@/store'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -103,7 +103,7 @@ watch(visible, (newVal) => {
|
|||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-col items-center gap-6 w-full px-12">
|
<div class="flex flex-col items-center gap-6 w-full px-12">
|
||||||
<ImageUpload v-if="visible" :limit-type="limitType" @upload-file="uploadFile" />
|
<FileUpload v-if="visible" :limit-type="limitType" @upload-file="uploadFile" />
|
||||||
<input v-model="fileName" :disabled="imageData === null" type="text" placeholder="图片名称" class="input w-full">
|
<input v-model="fileName" :disabled="imageData === null" type="text" placeholder="图片名称" class="input w-full">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
128
src/views/Config/Global/MusicConfig/components/UploadDialog.vue
Normal file
128
src/views/Config/Global/MusicConfig/components/UploadDialog.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import type { IFileData } from '@/components/FileUpload/type'
|
||||||
|
import localforage from 'localforage'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useToast } from 'vue-toast-notification'
|
||||||
|
import CustomDialog from '@/components/Dialog/index.vue'
|
||||||
|
import FileUpload from '@/components/FileUpload/index.vue'
|
||||||
|
import useStore from '@/store'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
const limitType = ref('audio/*')
|
||||||
|
const imgUploadToast = ref(0) // 0是不显示,1是成功,2是失败,3是不是图片
|
||||||
|
const visible = defineModel('visible', {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
const globalConfig = useStore().globalConfig
|
||||||
|
const audioDbStore = localforage.createInstance({
|
||||||
|
name: 'audioStore',
|
||||||
|
})
|
||||||
|
const audioData = ref<IFileData | null>(null)
|
||||||
|
|
||||||
|
const fileName = computed({
|
||||||
|
get() {
|
||||||
|
return audioData.value?.fileName || null
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (audioData.value && value) {
|
||||||
|
audioData.value.fileName = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const uploadDialogRef = ref()
|
||||||
|
|
||||||
|
async function uploadFile(fileData: IFileData | null) {
|
||||||
|
if (!fileData) {
|
||||||
|
audioData.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const isAudio = /audio*/.test(fileData?.type || '')
|
||||||
|
if (!isAudio) {
|
||||||
|
toast.open({
|
||||||
|
message: '不是音频文件',
|
||||||
|
type: 'error',
|
||||||
|
position: 'top-right',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioData.value = fileData
|
||||||
|
}
|
||||||
|
async function getAudioDbStore() {
|
||||||
|
const keys = await audioDbStore.keys()
|
||||||
|
if (keys.length > 0) {
|
||||||
|
audioDbStore.iterate((value: { fileName: string, dataUrl: string }, key: string) => {
|
||||||
|
globalConfig.addMusic({
|
||||||
|
id: key,
|
||||||
|
name: value.fileName,
|
||||||
|
url: 'Storage',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function submitUpload() {
|
||||||
|
if (audioData.value) {
|
||||||
|
const { dataUrl, fileName } = audioData.value
|
||||||
|
const uniqueId = uuidv4()
|
||||||
|
audioDbStore.setItem(uniqueId, {
|
||||||
|
dataUrl,
|
||||||
|
fileName,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.open({
|
||||||
|
message: '上传成功',
|
||||||
|
type: 'success',
|
||||||
|
position: 'top-right',
|
||||||
|
})
|
||||||
|
getAudioDbStore()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.open({
|
||||||
|
message: '上传失败',
|
||||||
|
type: 'error',
|
||||||
|
position: 'top-right',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch(visible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
uploadDialogRef.value.showDialog()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="toast toast-top toast-end">
|
||||||
|
<div v-if="imgUploadToast === 2" class="alert alert-error">
|
||||||
|
<span>{{ t('error.uploadFail') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="imgUploadToast === 1" class="alert alert-success">
|
||||||
|
<span>{{ t('error.uploadSuccess') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="imgUploadToast === 3" class="alert alert-error">
|
||||||
|
<span>{{ t('error.notImage') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CustomDialog
|
||||||
|
ref="uploadDialogRef"
|
||||||
|
v-model:visible="visible"
|
||||||
|
title="音乐上传"
|
||||||
|
:submit-func="submitUpload"
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex flex-col items-center gap-6 w-full px-12">
|
||||||
|
<FileUpload v-if="visible" :limit-type="limitType" @upload-file="uploadFile" />
|
||||||
|
<input v-model="fileName" :disabled="audioData === null" type="text" placeholder="图片名称" class="input w-full">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CustomDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -2,22 +2,21 @@
|
|||||||
import type { IMusic } from '@/types/storeType'
|
import type { IMusic } from '@/types/storeType'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { onMounted, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import PageHeader from '@/components/PageHeader/index.vue'
|
import PageHeader from '@/components/PageHeader/index.vue'
|
||||||
import useStore from '@/store'
|
import useStore from '@/store'
|
||||||
import { readFileData } from '@/utils/file'
|
import UploadDialog from './components/UploadDialog.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const audioUploadToast = ref(0) // 0是不显示,1是成功,2是失败,3是不是图片
|
|
||||||
const audioDbStore = localforage.createInstance({
|
const audioDbStore = localforage.createInstance({
|
||||||
name: 'audioStore',
|
name: 'audioStore',
|
||||||
})
|
})
|
||||||
const globalConfig = useStore().globalConfig
|
const globalConfig = useStore().globalConfig
|
||||||
|
|
||||||
const { getMusicList: localMusicList } = storeToRefs(globalConfig)
|
const { getMusicList: localMusicList } = storeToRefs(globalConfig)
|
||||||
const limitType = ref('audio/*')
|
|
||||||
const localMusicListValue = ref(localMusicList)
|
const localMusicListValue = ref(localMusicList)
|
||||||
|
const uploadVisible = ref(false)
|
||||||
async function play(item: IMusic) {
|
async function play(item: IMusic) {
|
||||||
globalConfig.setCurrentMusic(item, false)
|
globalConfig.setCurrentMusic(item, false)
|
||||||
}
|
}
|
||||||
@@ -37,42 +36,10 @@ function deleteAll() {
|
|||||||
globalConfig.clearMusicList()
|
globalConfig.clearMusicList()
|
||||||
audioDbStore.clear()
|
audioDbStore.clear()
|
||||||
}
|
}
|
||||||
async function getMusicDbStore() {
|
|
||||||
const keys = await audioDbStore.keys()
|
|
||||||
if (keys.length > 0) {
|
|
||||||
audioDbStore.iterate((value: string, key: string) => {
|
|
||||||
globalConfig.addMusic({
|
|
||||||
id: key + new Date().getTime().toString(),
|
|
||||||
name: key,
|
|
||||||
url: 'Storage',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function handleFileChange(e: Event) {
|
|
||||||
const isAudio = /audio*/.test(((e.target as HTMLInputElement).files as FileList)[0].type)
|
|
||||||
if (!isAudio) {
|
|
||||||
audioUploadToast.value = 3
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { dataUrl, fileName } = await readFileData(((e.target as HTMLInputElement).files as FileList)[0])
|
|
||||||
audioDbStore.setItem(`${new Date().getTime().toString()}+${fileName}`, dataUrl)
|
|
||||||
.then(() => {
|
|
||||||
audioUploadToast.value = 1
|
|
||||||
getMusicDbStore()
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
audioUploadToast.value = 2
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getMusicDbStore()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<UploadDialog v-model:visible="uploadVisible" />
|
||||||
<div>
|
<div>
|
||||||
<PageHeader title="音乐管理">
|
<PageHeader title="音乐管理">
|
||||||
<template #buttons>
|
<template #buttons>
|
||||||
@@ -81,11 +48,7 @@ onMounted(() => {
|
|||||||
{{ t('button.reset') }}
|
{{ t('button.reset') }}
|
||||||
</button>
|
</button>
|
||||||
<label for="explore">
|
<label for="explore">
|
||||||
<input
|
<span class="btn btn-primary btn-sm" @click="uploadVisible = true">{{ t('button.upload') }}</span>
|
||||||
id="explore" type="file" class="" style="display: none" :accept="limitType"
|
|
||||||
@change="handleFileChange"
|
|
||||||
>
|
|
||||||
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
|
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-error btn-sm" @click="deleteAll">
|
<button class="btn btn-error btn-sm" @click="deleteAll">
|
||||||
{{ t('button.allDelete') }}
|
{{ t('button.allDelete') }}
|
||||||
|
|||||||
Reference in New Issue
Block a user