feat: 音乐管理功能优化

This commit is contained in:
log1997
2025-12-04 15:27:19 +08:00
parent cdd4972870
commit 6546a17427
9 changed files with 674 additions and 327 deletions

View File

@@ -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",

789
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
src/components.d.ts vendored
View File

@@ -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']

View File

@@ -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>

View File

@@ -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()
} }

View File

@@ -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>

View 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>

View File

@@ -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') }}