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