|
|
@@ -0,0 +1,373 @@
|
|
|
+<template>
|
|
|
+ <div class="table-box">
|
|
|
+ <div class="card table-search">
|
|
|
+ <el-form ref="elSearchFormRef" :inline="true" size="small" :model="searchForm" class="demo-form-inline" @keyup.enter="onSubmit">
|
|
|
+ <el-form-item label="歌曲名称">
|
|
|
+ <el-input v-model="searchForm.title" placeholder="歌曲名称" style="width:150px"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="歌手">
|
|
|
+ <el-input v-model="searchForm.artist" placeholder="歌手" style="width:120px"></el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="歌单">
|
|
|
+ <el-select v-model="searchForm.groupId" placeholder="选择歌单" clearable style="width:150px">
|
|
|
+ <el-option v-for="g in groupOptions" :key="g.id" :label="g.name" :value="g.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-select v-model="searchForm.status" placeholder="状态" clearable style="width:100px">
|
|
|
+ <el-option label="启用" :value="1" />
|
|
|
+ <el-option label="禁用" :value="0" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <div class="operation">
|
|
|
+ <el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
|
|
+ <el-button icon="refresh" @click="onResetSearch">重置</el-button>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card table-main">
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="header-button-lf">
|
|
|
+ <el-button type="primary" icon="Plus" @click="openDialog()">新增歌曲</el-button>
|
|
|
+ </div>
|
|
|
+ <div class="header-button-ri">
|
|
|
+ <el-button :icon="Refresh" circle @click="refresh" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table ref="myTable" :data="tableData" border size="small">
|
|
|
+ <el-table-column prop="id" label="ID" align="center" width="80" />
|
|
|
+ <el-table-column prop="title" label="歌曲名称" align="center" min-width="150" />
|
|
|
+ <el-table-column prop="artist" label="歌手" align="center" width="120" />
|
|
|
+ <el-table-column label="所属歌单" align="center" width="120">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ getGroupName(row.groupId) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="试听" align="center" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button v-if="row.url" type="primary" link size="small" @click="playAudio(row.url)">
|
|
|
+ <el-icon><VideoPlay /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="duration" label="时长(秒)" align="center" width="80" />
|
|
|
+ <el-table-column prop="sort" label="排序" align="center" width="70" />
|
|
|
+ <el-table-column prop="status" label="状态" align="center" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.status === 1 ? 'success' : 'info'">
|
|
|
+ {{ row.status === 1 ? '启用' : '禁用' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createdAt" label="创建时间" align="center" min-width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span v-if="row.createdAt">{{ formatUnix(row.createdAt) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column fixed="right" label="操作" align="center" width="150">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button type="primary" link @click="openDialog(scope.row)">编辑</el-button>
|
|
|
+ <el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <Pagination :pageable="pageable" @handleCurrent="handleCurrent" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 编辑弹窗 -->
|
|
|
+ <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑歌曲' : '新增歌曲'" width="600px" center destroy-on-close>
|
|
|
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
|
|
+ <el-form-item label="所属歌单" prop="groupId">
|
|
|
+ <el-select v-model="formData.groupId" placeholder="请选择歌单" style="width: 100%">
|
|
|
+ <el-option v-for="g in groupOptions" :key="g.id" :label="g.name" :value="g.id" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="歌曲名称" prop="title">
|
|
|
+ <el-input v-model="formData.title" placeholder="请输入歌曲名称" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="歌手" prop="artist">
|
|
|
+ <el-input v-model="formData.artist" placeholder="请输入歌手名称" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="音频文件" prop="url">
|
|
|
+ <div class="upload-audio-wrapper">
|
|
|
+ <div v-if="formData.url" class="audio-preview">
|
|
|
+ <audio :src="audioPreviewUrl || formData.url" controls style="width: 100%"></audio>
|
|
|
+ <div class="audio-actions">
|
|
|
+ <el-button type="primary" size="small" @click="triggerAudioUpload">重新上传</el-button>
|
|
|
+ <el-button type="danger" size="small" @click="clearAudio">删除</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-upload
|
|
|
+ v-else
|
|
|
+ ref="audioUploadRef"
|
|
|
+ class="audio-uploader"
|
|
|
+ drag
|
|
|
+ action="#"
|
|
|
+ :auto-upload="false"
|
|
|
+ :on-change="handleAudioChange"
|
|
|
+ :show-file-list="false"
|
|
|
+ accept=".mp3"
|
|
|
+ >
|
|
|
+ <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
|
|
+ <div class="el-upload__text">拖拽 MP3 文件到此处 或 <em>点击上传</em></div>
|
|
|
+ </el-upload>
|
|
|
+ <el-upload
|
|
|
+ v-show="false"
|
|
|
+ ref="audioReuploadRef"
|
|
|
+ action="#"
|
|
|
+ :auto-upload="false"
|
|
|
+ :on-change="handleAudioChange"
|
|
|
+ :show-file-list="false"
|
|
|
+ accept=".mp3"
|
|
|
+ />
|
|
|
+ <div v-if="audioUploading" class="upload-progress">
|
|
|
+ <el-progress :percentage="audioUploadProgress" :status="audioUploadProgress === 100 ? 'success' : undefined" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="排序" prop="sort">
|
|
|
+ <el-input-number v-model="formData.sort" :min="0" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态" prop="status">
|
|
|
+ <el-radio-group v-model="formData.status">
|
|
|
+ <el-radio :value="1">启用</el-radio>
|
|
|
+ <el-radio :value="0">禁用</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 试听弹窗 -->
|
|
|
+ <el-dialog v-model="playerVisible" title="试听" width="400px" center destroy-on-close>
|
|
|
+ <audio :src="playerUrl" controls autoplay style="width: 100%"></audio>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted } from "vue";
|
|
|
+import dayjs from "dayjs";
|
|
|
+import { Refresh, UploadFilled, VideoPlay } from "@element-plus/icons-vue";
|
|
|
+import { ElNotification, ElMessageBox } from "element-plus";
|
|
|
+import Pagination from "@/components/Pangination/Pagination.vue";
|
|
|
+import { getMusicList, createMusic, updateMusic, deleteMusic, getMusicGroupList } from "@/api/modules/daytask.js";
|
|
|
+import { uploadImg } from "@/api/modules/upload.js";
|
|
|
+
|
|
|
+const formatUnix = (ts) => ts ? dayjs.unix(ts).format("YYYY-MM-DD HH:mm:ss") : "";
|
|
|
+
|
|
|
+// 歌单选项
|
|
|
+const groupOptions = ref([]);
|
|
|
+const loadGroupOptions = async () => {
|
|
|
+ const res = await getMusicGroupList({ pageSize: 100 });
|
|
|
+ if (res.data) groupOptions.value = res.data.list || [];
|
|
|
+};
|
|
|
+const getGroupName = (id) => {
|
|
|
+ const g = groupOptions.value.find(g => g.id === id);
|
|
|
+ return g ? g.name : "-";
|
|
|
+};
|
|
|
+
|
|
|
+// 试听
|
|
|
+const playerVisible = ref(false);
|
|
|
+const playerUrl = ref("");
|
|
|
+const playAudio = (url) => {
|
|
|
+ playerUrl.value = url;
|
|
|
+ playerVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 搜索
|
|
|
+const searchForm = reactive({ title: "", artist: "", groupId: null, status: null });
|
|
|
+const tableData = ref([]);
|
|
|
+const pageable = reactive({ current: 1, pageSize: 20, total: 0 });
|
|
|
+
|
|
|
+const getList = async () => {
|
|
|
+ const params = { current: pageable.current, pageSize: pageable.pageSize, ...searchForm };
|
|
|
+ Object.keys(params).forEach(k => { if (params[k] === "" || params[k] === null) delete params[k]; });
|
|
|
+ const res = await getMusicList(params);
|
|
|
+ if (res.data) {
|
|
|
+ tableData.value = res.data.list || [];
|
|
|
+ pageable.total = res.data.paging?.total || 0;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const onSubmit = () => { pageable.current = 1; getList(); };
|
|
|
+const onResetSearch = () => { searchForm.title = ""; searchForm.artist = ""; searchForm.groupId = null; searchForm.status = null; onSubmit(); };
|
|
|
+const handleCurrent = (val) => { pageable.current = val; getList(); };
|
|
|
+const refresh = () => getList();
|
|
|
+
|
|
|
+// 弹窗
|
|
|
+const dialogVisible = ref(false);
|
|
|
+const isEdit = ref(false);
|
|
|
+const formRef = ref(null);
|
|
|
+const submitLoading = ref(false);
|
|
|
+const formData = reactive({ id: null, groupId: null, title: "", artist: "", url: "", duration: 0, sort: 0, status: 1 });
|
|
|
+const formRules = {
|
|
|
+ groupId: [{ required: true, message: "请选择歌单", trigger: "change" }],
|
|
|
+ title: [{ required: true, message: "请输入歌曲名称", trigger: "blur" }],
|
|
|
+ url: [{ required: true, message: "请上传音频文件", trigger: "change" }],
|
|
|
+};
|
|
|
+
|
|
|
+// 音频上传
|
|
|
+const audioUploadRef = ref(null);
|
|
|
+const audioReuploadRef = ref(null);
|
|
|
+const audioPreviewUrl = ref("");
|
|
|
+const audioUploading = ref(false);
|
|
|
+const audioUploadProgress = ref(0);
|
|
|
+
|
|
|
+const handleAudioChange = async (file) => {
|
|
|
+ const rawFile = file.raw;
|
|
|
+ audioPreviewUrl.value = URL.createObjectURL(rawFile);
|
|
|
+
|
|
|
+ // 自动获取音频时长
|
|
|
+ const audio = new Audio(audioPreviewUrl.value);
|
|
|
+ audio.addEventListener("loadedmetadata", () => {
|
|
|
+ formData.duration = Math.round(audio.duration);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 自动填充歌曲名称(如果为空)
|
|
|
+ if (!formData.title) {
|
|
|
+ formData.title = rawFile.name.replace(/\.[^/.]+$/, "");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传
|
|
|
+ audioUploading.value = true;
|
|
|
+ audioUploadProgress.value = 0;
|
|
|
+ try {
|
|
|
+ const fd = new FormData();
|
|
|
+ fd.append("file", rawFile);
|
|
|
+ fd.append("autoSave", "false");
|
|
|
+
|
|
|
+ const progressInterval = setInterval(() => {
|
|
|
+ if (audioUploadProgress.value < 90) audioUploadProgress.value += 10;
|
|
|
+ }, 100);
|
|
|
+
|
|
|
+ const res = await uploadImg(fd);
|
|
|
+ clearInterval(progressInterval);
|
|
|
+
|
|
|
+ if (res.code === 200) {
|
|
|
+ audioUploadProgress.value = 100;
|
|
|
+ formData.url = res.data.path;
|
|
|
+ if (formRef.value) formRef.value.clearValidate("url");
|
|
|
+ ElNotification.success("音频上传成功");
|
|
|
+ } else {
|
|
|
+ ElNotification.error(res.msg || "音频上传失败");
|
|
|
+ clearAudio();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ ElNotification.error("音频上传失败");
|
|
|
+ clearAudio();
|
|
|
+ } finally {
|
|
|
+ audioUploading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const triggerAudioUpload = () => {
|
|
|
+ audioReuploadRef.value?.$el.querySelector("input")?.click();
|
|
|
+};
|
|
|
+
|
|
|
+const clearAudio = () => {
|
|
|
+ formData.url = "";
|
|
|
+ formData.duration = 0;
|
|
|
+ audioPreviewUrl.value = "";
|
|
|
+};
|
|
|
+
|
|
|
+const openDialog = (row) => {
|
|
|
+ isEdit.value = !!row;
|
|
|
+ audioPreviewUrl.value = "";
|
|
|
+ if (row) {
|
|
|
+ Object.assign(formData, {
|
|
|
+ id: row.id,
|
|
|
+ groupId: row.groupId,
|
|
|
+ title: row.title,
|
|
|
+ artist: row.artist || "",
|
|
|
+ url: row.url,
|
|
|
+ duration: row.duration || 0,
|
|
|
+ sort: row.sort || 0,
|
|
|
+ status: row.status,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ Object.assign(formData, { id: null, groupId: null, title: "", artist: "", url: "", duration: 0, sort: 0, status: 1 });
|
|
|
+ }
|
|
|
+ dialogVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ await formRef.value?.validate();
|
|
|
+ submitLoading.value = true;
|
|
|
+ try {
|
|
|
+ // 后端期望格式: { id, data: { ...fields } }
|
|
|
+ const { id, ...fields } = formData;
|
|
|
+ if (isEdit.value) {
|
|
|
+ await updateMusic({ id, data: fields });
|
|
|
+ } else {
|
|
|
+ await createMusic({ data: fields });
|
|
|
+ }
|
|
|
+ ElNotification.success({ title: "成功", message: isEdit.value ? "编辑成功" : "新增成功" });
|
|
|
+ dialogVisible.value = false;
|
|
|
+ getList();
|
|
|
+ } catch (e) {
|
|
|
+ ElNotification.error({ title: "错误", message: e.message || "操作失败" });
|
|
|
+ } finally {
|
|
|
+ submitLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleDelete = (row) => {
|
|
|
+ ElMessageBox.confirm("确定删除该歌曲吗?", "提示", { type: "warning" }).then(async () => {
|
|
|
+ await deleteMusic({ id: row.id });
|
|
|
+ ElNotification.success({ title: "成功", message: "删除成功" });
|
|
|
+ getList();
|
|
|
+ }).catch(() => {});
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => { loadGroupOptions(); getList(); });
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.upload-audio-wrapper {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-uploader {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-uploader :deep(.el-upload) {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-uploader :deep(.el-upload-dragger) {
|
|
|
+ width: 100%;
|
|
|
+ height: auto;
|
|
|
+ min-height: 120px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-preview {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-progress {
|
|
|
+ margin-top: 10px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+</style>
|