diff --git a/.claude/commands/refactor.md b/.claude/commands/refactor.md new file mode 100644 index 00000000..951fefa0 --- /dev/null +++ b/.claude/commands/refactor.md @@ -0,0 +1,5 @@ +重构当前页面代码 + +1. 把当前页面 JS 代码全部抽离,然后是采用 Pinia 重构 +2. 把当前页面 CSS 代码全部抽离,如果是 stylus 语法代码,则需要改成 SCSS 语法代码 +3. 尽量做到代码的复用性,不要重复造轮子 diff --git a/web/src/assets/css/suno.scss b/web/src/assets/css/suno.scss index 16fe1913..fe450f63 100644 --- a/web/src/assets/css/suno.scss +++ b/web/src/assets/css/suno.scss @@ -1,20 +1,15 @@ .page-suno { display: flex; height: 100%; - // background-color: #0E0808; + background-color: #f8fafc; overflow: auto; - .item-group { - scrollbar-width: auto !important; /* 恢复滚动条(Firefox) */ - -ms-overflow-style: auto !important; /* 恢复滚动条(IE、Edge) */ - ::-webkit-scrollbar { - display: block !important; - } - } .left-bar { - max-width: 340px; - min-width: 340px; - padding: 20px 30px; + max-width: 400px; + min-width: 400px; + padding: 20px; + background-color: #f8fafc; + overflow-y: auto; .bar-top { display: flex; @@ -63,7 +58,7 @@ .create-btn { margin: 20px 0; - background-image: url("~@/assets/img/suno-create-bg.svg"); + background-image: url('~@/assets/img/suno-create-bg.svg'); background-size: cover; background-position: 50% 50%; transition: background 1s ease-in-out; @@ -178,6 +173,8 @@ padding: 3px 6px; cursor: pointer; font-size: 13px; + border: none; + outline: none; &:hover { color: var(--el-color-primary); } @@ -188,12 +185,13 @@ } .right-box { width: 100%; - color: rgb(250 247 245); + color: #374151; overflow: auto; - background: var(--chat-bg); + background: #f8fafc; + padding: 20px; .list-box { - padding: 20px; + padding: 0; .item { display: flex; flex-flow: row; @@ -201,10 +199,6 @@ cursor: pointer; margin-bottom: 10px; - &:hover { - background: rgba(188, 149, 236, 0.08); - } - .left { .container { width: 60px; @@ -421,4 +415,22 @@ .el-button { width: 200px; } -} \ No newline at end of file +} + +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.form { + .form-item { + margin-bottom: 20px; + + .label { + margin-bottom: 8px; + font-weight: 500; + color: var(--el-text-color-primary); + } + } +} diff --git a/web/src/components/ui/BlackInput.vue b/web/src/components/ui/BlackInput.vue deleted file mode 100644 index 52912551..00000000 --- a/web/src/components/ui/BlackInput.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - - - diff --git a/web/src/router.js b/web/src/router.js index 4c1b006d..61245693 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -359,20 +359,20 @@ const routes = [ }, { meta: { title: 'Suno音乐创作' }, - path: '/mobile/suno-create', - name: 'mobile-suno-create', + path: '/mobile/suno', + name: 'mobile-suno', component: () => import('@/views/mobile/pages/SunoCreate.vue'), }, { meta: { title: '视频生成' }, - path: '/mobile/video-create', - name: 'mobile-video-create', + path: '/mobile/video', + name: 'mobile-video', component: () => import('@/views/mobile/pages/VideoCreate.vue'), }, { meta: { title: '即梦AI' }, - path: '/mobile/jimeng-create', - name: 'mobile-jimeng-create', + path: '/mobile/jimeng', + name: 'mobile-jimeng', component: () => import('@/views/mobile/pages/JimengCreate.vue'), }, ], diff --git a/web/src/store/suno.js b/web/src/store/suno.js new file mode 100644 index 00000000..fe8673f6 --- /dev/null +++ b/web/src/store/suno.js @@ -0,0 +1,417 @@ +import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog' +import { httpDownload, httpGet, httpPost } from '@/utils/http' +import { replaceImg } from '@/utils/libs' +import Compressor from 'compressorjs' +import { ElMessage, ElMessageBox } from 'element-plus' +import { compact } from 'lodash' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +export const useSunoStore = defineStore('suno', () => { + // 响应式数据 + const custom = ref(false) + const models = ref([ + { label: 'v3.0', value: 'chirp-v3-0' }, + { label: 'v3.5', value: 'chirp-v3-5' }, + { label: 'v4.0', value: 'chirp-v4' }, + { label: 'v4.5', value: 'chirp-auk' }, + ]) + + const tags = ref([ + { label: '女声', value: 'female vocals' }, + { label: '男声', value: 'male vocals' }, + { label: '流行', value: 'pop' }, + { label: '摇滚', value: 'rock' }, + { label: '硬摇滚', value: 'hard rock' }, + { label: '电音', value: 'electronic' }, + { label: '金属', value: 'metal' }, + { label: '重金属', value: 'heavy metal' }, + { label: '节拍', value: 'beat' }, + { label: '弱拍', value: 'upbeat' }, + { label: '合成器', value: 'synth' }, + { label: '吉他', value: 'guitar' }, + { label: '钢琴', value: 'piano' }, + { label: '小提琴', value: 'violin' }, + { label: '贝斯', value: 'bass' }, + { label: '嘻哈', value: 'hip hop' }, + ]) + + const data = ref({ + model: 'chirp-auk', + tags: '', + lyrics: '', + prompt: '', + title: '', + instrumental: false, + ref_task_id: '', + extend_secs: 0, + ref_song_id: '', + }) + + const loading = ref(false) + const noData = ref(true) + const playList = ref([]) + const showPlayer = ref(false) + const list = ref([]) + const taskPulling = ref(true) + const btnText = ref('开始创作') + const refSong = ref(null) + const showDialog = ref(false) + const editData = ref({ title: '', cover: '', id: 0 }) + const promptPlaceholder = ref('请在这里输入你自己写的歌词...') + const isGenerating = ref(false) + + // 分页相关 + const page = ref(1) + const pageSize = ref(10) + const total = ref(0) + + // 定时器引用 + let tastPullHandler = null + + // 计算属性 + const hasRefSong = computed(() => refSong.value !== null) + + // 方法 + const fetchData = async (_page) => { + if (_page) { + page.value = _page + } + loading.value = true + + try { + const res = await httpGet('/api/suno/list', { + page: page.value, + page_size: pageSize.value, + }) + + total.value = res.data.total + let needPull = false + const items = [] + + for (let v of res.data.items) { + if (v.progress === 100) { + v.major_model_version = v['raw_data']['major_model_version'] + } + if (v.progress === 0 || v.progress === 102) { + needPull = true + } + items.push(v) + } + + loading.value = false + taskPulling.value = needPull + + // 如果任务有变化,则刷新任务列表 + if (JSON.stringify(list.value) !== JSON.stringify(items)) { + list.value = items + } + noData.value = list.value.length === 0 + } catch (e) { + loading.value = false + noData.value = true + showMessageError('获取作品列表失败:' + e.message) + } + } + + const create = async () => { + data.value.type = custom.value ? 2 : 1 + data.value.ref_task_id = refSong.value ? refSong.value.task_id : '' + data.value.ref_song_id = refSong.value ? refSong.value.song_id : '' + data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0 + + // 验证输入 + if (refSong.value) { + if (data.value.extend_secs > refSong.value.duration) { + return showMessageError('续写开始时间不能超过原歌曲长度') + } + } else if (custom.value) { + if (data.value.lyrics === '') { + return showMessageError('请输入歌词') + } + if (data.value.title === '') { + return showMessageError('请输入歌曲标题') + } + } else { + if (data.value.prompt === '') { + return showMessageError('请输入歌曲描述') + } + } + + try { + await httpPost('/api/suno/create', data.value) + await fetchData(1) + taskPulling.value = true + showMessageOK('创建任务成功') + } catch (e) { + showMessageError('创建任务失败:' + e.message) + } + } + + const merge = async (item) => { + try { + await httpPost('/api/suno/create', { song_id: item.song_id, type: 3 }) + await fetchData(1) + taskPulling.value = true + showMessageOK('创建任务成功') + } catch (e) { + showMessageError('合并歌曲失败:' + e.message) + } + } + + const download = async (item) => { + const url = replaceImg(item.audio_url) + const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}` + const urlObj = new URL(url) + const fileName = urlObj.pathname.split('/').pop() + + item.downloading = true + + try { + const response = await httpDownload(downloadURL) + const blob = new Blob([response.data]) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = fileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(link.href) + item.downloading = false + } catch (error) { + showMessageError('下载失败') + item.downloading = false + } + } + + const uploadAudio = async (file) => { + const formData = new FormData() + formData.append('file', file.file, file.name) + showLoading('正在上传文件...') + + try { + const res = await httpPost('/api/upload', formData) + await httpPost('/api/suno/create', { + audio_url: res.data.url, + title: res.data.name, + type: 4, + }) + await fetchData(1) + showMessageOK('歌曲上传成功') + closeLoading() + removeRefSong() + ElMessage.success({ message: '上传成功', duration: 500 }) + } catch (e) { + showMessageError('歌曲上传失败:' + e.message) + closeLoading() + } + } + + const extend = (item) => { + refSong.value = item + refSong.value.extend_secs = item.duration + data.value.title = item.title + custom.value = true + btnText.value = '续写歌曲' + promptPlaceholder.value = '输入额外的歌词,根据您之前的歌词来扩展歌曲...' + } + + const update = (item) => { + showDialog.value = true + editData.value.title = item.title + editData.value.cover = item.cover_url + editData.value.id = item.id + } + + const updateSong = async () => { + if (editData.value.title === '' || editData.value.cover === '') { + return showMessageError('歌曲标题和封面不能为空') + } + + try { + await httpPost('/api/suno/update', editData.value) + showMessageOK('更新歌曲成功') + showDialog.value = false + await fetchData() + } catch (e) { + showMessageError('更新歌曲失败:' + e.message) + } + } + + const removeRefSong = () => { + refSong.value = null + btnText.value = '开始创作' + promptPlaceholder.value = '请在这里输入你自己写的歌词...' + } + + const selectTag = (tag) => { + const currentTags = data.value.tags.trim() + const newTagLength = tag.value.length + + if (currentTags.length + newTagLength >= 119) { + return + } + + const tagArray = currentTags + ? currentTags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + : [] + const newTags = compact([...tagArray, tag.value]) + data.value.tags = newTags.join(',') + } + + const removeJob = async (item) => { + try { + await ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', { + confirmButtonText: '确认', + cancelButtonText: '取消', + type: 'warning', + }) + + await httpGet('/api/suno/remove', { id: item.id }) + ElMessage.success('任务删除成功') + await fetchData() + } catch (e) { + if (e !== 'cancel') { + ElMessage.error('任务删除失败:' + e.message) + } + } + } + + const publishJob = async (item) => { + try { + await httpGet('/api/suno/publish', { id: item.id, publish: item.publish }) + ElMessage.success('操作成功') + } catch (e) { + ElMessage.error('操作失败:' + e.message) + } + } + + const getShareURL = (item) => { + return `${location.protocol}//${location.host}/song/${item.song_id}` + } + + const uploadCover = (file) => { + new Compressor(file.file, { + quality: 0.6, + success(result) { + const formData = new FormData() + formData.append('file', result, result.name) + showLoading('图片上传中...') + + httpPost('/api/upload', formData) + .then((res) => { + editData.value.cover = res.data.url + ElMessage.success({ message: '上传成功', duration: 500 }) + closeLoading() + }) + .catch((e) => { + ElMessage.error('图片上传失败:' + e.message) + closeLoading() + }) + }, + error(err) { + console.log(err.message) + }, + }) + } + + const createLyric = async () => { + if (data.value.lyrics === '') { + return showMessageError('请输入歌词描述') + } + + isGenerating.value = true + + try { + const res = await httpPost('/api/prompt/lyric', { prompt: data.value.lyrics }) + const lines = res.data.split('\n') + data.value.title = lines.shift().replace(/\*/g, '') + lines.shift() + data.value.lyrics = lines.join('\n') + isGenerating.value = false + } catch (e) { + showMessageError('歌词生成失败:' + e.message) + isGenerating.value = false + } + } + + const startTaskPolling = () => { + tastPullHandler = setInterval(() => { + if (taskPulling.value) { + fetchData(1) + } + }, 5000) + } + + const stopTaskPolling = () => { + if (tastPullHandler) { + clearInterval(tastPullHandler) + tastPullHandler = null + } + } + + const resetData = () => { + data.value = { + model: 'chirp-auk', + tags: '', + lyrics: '', + prompt: '', + title: '', + instrumental: false, + ref_task_id: '', + extend_secs: 0, + ref_song_id: '', + } + custom.value = false + refSong.value = null + btnText.value = '开始创作' + promptPlaceholder.value = '请在这里输入你自己写的歌词...' + } + + return { + // 状态 + custom, + models, + tags, + data, + loading, + noData, + playList, + showPlayer, + list, + taskPulling, + btnText, + refSong, + showDialog, + editData, + promptPlaceholder, + isGenerating, + page, + pageSize, + total, + hasRefSong, + + // 方法 + fetchData, + create, + merge, + download, + uploadAudio, + extend, + update, + updateSong, + removeRefSong, + selectTag, + removeJob, + publishJob, + getShareURL, + uploadCover, + createLyric, + startTaskPolling, + stopTaskPolling, + resetData, + } +}) diff --git a/web/src/views/Suno.vue b/web/src/views/Suno.vue index cafa4383..e9450849 100644 --- a/web/src/views/Suno.vue +++ b/web/src/views/Suno.vue @@ -1,46 +1,83 @@ @@ -359,383 +657,214 @@ import nodata from '@/assets/img/no-data.png' import MusicPlayer from '@/components/MusicPlayer.vue' -import BlackDialog from '@/components/ui/BlackDialog.vue' -import BlackInput from '@/components/ui/BlackInput.vue' -import BlackSelect from '@/components/ui/BlackSelect.vue' -import BlackSwitch from '@/components/ui/BlackSwitch.vue' -import Generating from '@/components/ui/Generating.vue' import { checkSession } from '@/store/cache' -import { useSharedStore } from '@/store/sharedata' -import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog' -import { httpDownload, httpGet, httpPost } from '@/utils/http' -import { formatTime, replaceImg } from '@/utils/libs' -import { Delete, InfoFilled } from '@element-plus/icons-vue' +import { useSunoStore } from '@/store/suno' +import { InfoFilled } from '@element-plus/icons-vue' import Clipboard from 'clipboard' -import Compressor from 'compressorjs' -import { ElMessage, ElMessageBox } from 'element-plus' -import { compact } from 'lodash' +import { ElMessage } from 'element-plus' import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue' -const custom = ref(false) -const models = ref([ - { label: 'v3.0', value: 'chirp-v3-0' }, - { label: 'v3.5', value: 'chirp-v3-5' }, - { label: 'v4.0', value: 'chirp-v4' }, - { label: 'v4.5', value: 'chirp-auk' }, -]) -const tags = ref([ - { label: '女声', value: 'female vocals' }, - { label: '男声', value: 'male vocals' }, - { label: '流行', value: 'pop' }, - { label: '摇滚', value: 'rock' }, - { label: '硬摇滚', value: 'hard rock' }, - { label: '电音', value: 'electronic' }, - { label: '金属', value: 'metal' }, - { label: '重金属', value: 'heavy metal' }, - { label: '节拍', value: 'beat' }, - { label: '弱拍', value: 'upbeat' }, - { label: '合成器', value: 'synth' }, - { label: '吉他', value: 'guitar' }, - { label: '钢琴', value: 'piano' }, - { label: '小提琴', value: 'violin' }, - { label: '贝斯', value: 'bass' }, - { label: '嘻哈', value: 'hip hop' }, -]) -const data = ref({ - model: 'chirp-auk', - tags: '', - lyrics: '', - prompt: '', - title: '', - instrumental: false, - ref_task_id: '', - extend_secs: 0, - ref_song_id: '', -}) -const loading = ref(false) -const noData = ref(true) -const playList = ref([]) +// 使用 Pinia store +const store = useSunoStore() + +// 组件内部状态 const playerRef = ref(null) -const showPlayer = ref(false) -const list = ref([]) -const taskPulling = ref(true) -const tastPullHandler = ref(null) -const btnText = ref('开始创作') -const refSong = ref(null) -const showDialog = ref(false) -const editData = ref({ title: '', cover: '', id: 0 }) -const promptPlaceholder = ref('请在这里输入你自己写的歌词...') -const store = useSharedStore() const clipboard = ref(null) +const fileInput = ref(null) +const uploading = ref(false) +const uploadProgress = ref(0) +const uploadedFile = ref(null) +const uploadedAudioUrl = ref('') + +// 播放音乐 +const play = (item) => { + store.playList = [item] + store.showPlayer = true + nextTick(() => playerRef.value.play()) +} + +// 文件上传相关方法 +const triggerFileInput = () => { + fileInput.value?.click() +} + +const formatFileSize = (bytes) => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +const handleFileSelect = (event) => { + const file = event.target.files[0] + if (file) { + validateAndUploadFile(file) + } +} + +const handleDrop = (event) => { + event.preventDefault() + const files = event.dataTransfer.files + if (files.length > 0) { + validateAndUploadFile(files[0]) + } +} + +const validateAndUploadFile = (file) => { + // 验证文件类型 + const allowedTypes = ['audio/wav', 'audio/mp3', 'audio/mpeg'] + if (!allowedTypes.includes(file.type)) { + ElMessage.error('只支持 WAV 和 MP3 格式的音频文件') + return + } + + // 验证文件大小 (50MB) + const maxSize = 50 * 1024 * 1024 + if (file.size > maxSize) { + ElMessage.error('文件大小不能超过 50MB') + return + } + + uploadedFile.value = file + uploadFile(file) +} + +const uploadFile = async (file) => { + uploading.value = true + uploadProgress.value = 0 + + try { + const formData = new FormData() + formData.append('file', file) + + // 模拟上传进度 + const progressInterval = setInterval(() => { + if (uploadProgress.value < 90) { + uploadProgress.value += Math.random() * 10 + } + }, 200) + + // 只上传文件,不创建任务 + const { httpPost } = await import('@/utils/http') + const res = await httpPost('/api/upload', formData) + + clearInterval(progressInterval) + uploadProgress.value = 100 + + // 保存上传的音频URL + uploadedAudioUrl.value = res.data.url + + ElMessage.success('文件上传成功') + + // 延迟重置状态 + setTimeout(() => { + uploading.value = false + uploadProgress.value = 0 + }, 1000) + } catch (error) { + uploading.value = false + uploadProgress.value = 0 + uploadedFile.value = null + ElMessage.error('文件上传失败:' + error.message) + } +} + +const playUploadedFile = () => { + if (uploadedAudioUrl.value) { + const audio = new Audio(uploadedAudioUrl.value) + audio.play() + } +} + +const removeUploadedFile = () => { + uploadedFile.value = null + uploadedAudioUrl.value = '' + if (fileInput.value) { + fileInput.value.value = '' + } +} + +// 监听器 +watch( + () => store.custom, + (newValue) => { + if (!newValue) { + store.removeRefSong() + } + } +) + +// 生命周期 onMounted(() => { + // 初始化剪贴板 clipboard.value = new Clipboard('.copy-link') clipboard.value.on('success', () => { ElMessage.success('复制歌曲链接成功!') }) - clipboard.value.on('error', () => { ElMessage.error('复制失败!') }) + // 检查会话并初始化数据 checkSession() .then(() => { - fetchData(1) - tastPullHandler.value = setInterval(() => { - if (taskPulling.value) { - fetchData(1) - } - }, 5000) + store.fetchData(1) + store.startTaskPolling() }) .catch(() => {}) }) onUnmounted(() => { - clipboard.value.destroy() - if (tastPullHandler.value) { - clearInterval(tastPullHandler.value) + // 清理资源 + if (clipboard.value) { + clipboard.value.destroy() } + store.stopTaskPolling() }) - -const page = ref(1) -const pageSize = ref(10) -const total = ref(0) -const fetchData = (_page) => { - if (_page) { - page.value = _page - } - loading.value = true - httpGet('/api/suno/list', { page: page.value, page_size: pageSize.value }) - .then((res) => { - total.value = res.data.total - let needPull = false - const items = [] - for (let v of res.data.items) { - if (v.progress === 100) { - v.major_model_version = v['raw_data']['major_model_version'] - } - if (v.progress === 0 || v.progress === 102) { - needPull = true - } - items.push(v) - } - loading.value = false - taskPulling.value = needPull - // 如果任务有变化,则刷新任务列表 - if (JSON.stringify(list.value) !== JSON.stringify(items)) { - list.value = items - } - noData.value = list.value.length === 0 - }) - .catch((e) => { - loading.value = false - noData.value = true - showMessageError('获取作品列表失败:' + e.message) - }) -} - -// 创建新的歌曲 -const create = () => { - data.value.type = custom.value ? 2 : 1 - data.value.ref_task_id = refSong.value ? refSong.value.task_id : '' - data.value.ref_song_id = refSong.value ? refSong.value.song_id : '' - data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0 - if (refSong.value) { - if (data.value.extend_secs > refSong.value.duration) { - return showMessageError('续写开始时间不能超过原歌曲长度') - } - } else if (custom.value) { - if (data.value.lyrics === '') { - return showMessageError('请输入歌词') - } - if (data.value.title === '') { - return showMessageError('请输入歌曲标题') - } - } else { - if (data.value.prompt === '') { - return showMessageError('请输入歌曲描述') - } - } - - httpPost('/api/suno/create', data.value) - .then(() => { - fetchData(1) - taskPulling.value = true - showMessageOK('创建任务成功') - }) - .catch((e) => { - showMessageError('创建任务失败:' + e.message) - }) -} - -// 拼接歌曲 -const merge = (item) => { - httpPost('/api/suno/create', { song_id: item.song_id, type: 3 }) - .then(() => { - fetchData(1) - taskPulling.value = true - showMessageOK('创建任务成功') - }) - .catch((e) => { - showMessageError('合并歌曲失败:' + e.message) - }) -} - -// 下载歌曲 -const download = (item) => { - const url = replaceImg(item.audio_url) - const downloadURL = `${import.meta.env.VITE_API_HOST}/api/download?url=${url}` - // parse filename - const urlObj = new URL(url) - const fileName = urlObj.pathname.split('/').pop() - item.downloading = true - httpDownload(downloadURL) - .then((response) => { - const blob = new Blob([response.data]) - const link = document.createElement('a') - link.href = URL.createObjectURL(blob) - link.download = fileName - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(link.href) - item.downloading = false - }) - .catch(() => { - showMessageError('下载失败') - item.downloading = false - }) -} - -const uploadAudio = (file) => { - const formData = new FormData() - formData.append('file', file.file, file.name) - showLoading('正在上传文件...') - // 执行上传操作 - httpPost('/api/upload', formData) - .then((res) => { - httpPost('/api/suno/create', { - audio_url: res.data.url, - title: res.data.name, - type: 4, - }) - .then(() => { - fetchData(1) - showMessageOK('歌曲上传成功') - closeLoading() - }) - .catch((e) => { - showMessageError('歌曲上传失败:' + e.message) - closeLoading() - }) - removeRefSong() - ElMessage.success({ message: '上传成功', duration: 500 }) - }) - .catch((e) => { - ElMessage.error('文件传失败:' + e.message) - }) -} - -// 续写歌曲 -const extend = (item) => { - refSong.value = item - refSong.value.extend_secs = item.duration - data.value.title = item.title - custom.value = true - btnText.value = '续写歌曲' - promptPlaceholder.value = '输入额外的歌词,根据您之前的歌词来扩展歌曲...' -} - -// 更细歌曲 -const update = (item) => { - showDialog.value = true - editData.value.title = item.title - editData.value.cover = item.cover_url - editData.value.id = item.id -} - -const updateSong = () => { - if (editData.value.title === '' || editData.value.cover === '') { - return showMessageError('歌曲标题和封面不能为空') - } - httpPost('/api/suno/update', editData.value) - .then(() => { - showMessageOK('更新歌曲成功') - showDialog.value = false - fetchData() - }) - .catch((e) => { - showMessageError('更新歌曲失败:' + e.message) - }) -} - -watch( - () => custom.value, - (newValue) => { - if (!newValue) { - removeRefSong() - } - } -) - -const removeRefSong = () => { - refSong.value = null - btnText.value = '开始创作' - promptPlaceholder.value = '请在这里输入你自己写的歌词...' -} - -const play = (item) => { - playList.value = [item] - showPlayer.value = true - nextTick(() => playerRef.value.play()) -} - -const selectTag = (tag) => { - if (data.value.tags.length + tag.value.length >= 119) { - return - } - data.value.tags = compact([...data.value.tags.split(','), tag.value]).join(',') -} - -const removeJob = (item) => { - ElMessageBox.confirm('此操作将会删除任务相关文件,继续操作码?', '删除提示', { - confirmButtonText: '确认', - cancelButtonText: '取消', - type: 'warning', - }) - .then(() => { - httpGet('/api/suno/remove', { id: item.id }) - .then(() => { - ElMessage.success('任务删除成功') - fetchData() - }) - .catch((e) => { - ElMessage.error('任务删除失败:' + e.message) - }) - }) - .catch(() => {}) -} - -const publishJob = (item) => { - httpGet('/api/suno/publish', { id: item.id, publish: item.publish }) - .then(() => { - ElMessage.success('操作成功') - }) - .catch((e) => { - ElMessage.error('操作失败:' + e.message) - }) -} - -const getShareURL = (item) => { - return `${location.protocol}//${location.host}/song/${item.song_id}` -} - -const uploadCover = (file) => { - // 压缩图片并上传 - new Compressor(file.file, { - quality: 0.6, - success(result) { - const formData = new FormData() - formData.append('file', result, result.name) - showLoading('图片上传中...') - // 执行上传操作 - httpPost('/api/upload', formData) - .then((res) => { - editData.value.cover = res.data.url - ElMessage.success({ message: '上传成功', duration: 500 }) - closeLoading() - }) - .catch((e) => { - ElMessage.error('图片上传失败:' + e.message) - closeLoading() - }) - }, - error(err) { - console.log(err.message) - }, - }) -} - -const isGenerating = ref(false) -const createLyric = () => { - if (data.value.lyrics === '') { - return showMessageError('请输入歌词描述') - } - isGenerating.value = true - httpPost('/api/prompt/lyric', { prompt: data.value.lyrics }) - .then((res) => { - const lines = res.data.split('\n') - data.value.title = lines.shift().replace(/\*/g, '') - lines.shift() - data.value.lyrics = lines.join('\n') - isGenerating.value = false - }) - .catch((e) => { - showMessageError('歌词生成失败:' + e.message) - isGenerating.value = false - }) -} diff --git a/web/src/views/mobile/pages/JimengCreate.vue b/web/src/views/mobile/pages/JimengCreate.vue index b7e88e91..1c069d2d 100644 --- a/web/src/views/mobile/pages/JimengCreate.vue +++ b/web/src/views/mobile/pages/JimengCreate.vue @@ -27,7 +27,7 @@ 'flex flex-col items-center p-3 rounded-lg border-2 transition-colors', activeCategory === category.key ? 'border-blue-500 bg-blue-50 text-blue-700' - : 'border-gray-200 bg-gray-50 text-gray-600 hover:border-gray-300 hover:bg-gray-100' + : 'border-gray-200 bg-gray-50 text-gray-600 hover:border-gray-300 hover:bg-gray-100', ]" > @@ -71,7 +71,7 @@ @@ -110,15 +110,28 @@ ref="imageToImageInput" type="file" accept=".jpg,.png,.jpeg" - @change="(e) => onImageUpload({ file: e.target.files[0], name: e.target.files[0]?.name })" + @change=" + (e) => onImageUpload({ file: e.target.files[0], name: e.target.files[0]?.name }) + " class="hidden" /> -
- - 上传图片 +
+ + 上传图片
@@ -151,7 +164,7 @@ @@ -169,15 +182,27 @@ ref="imageEditInput" type="file" accept=".jpg,.png,.jpeg" - @change="(e) => onImageUpload({ file: e.target.files[0], name: e.target.files[0]?.name })" + @change=" + (e) => onImageUpload({ file: e.target.files[0], name: e.target.files[0]?.name }) + " class="hidden" /> -
- - 上传图片 +
+ + 上传图片
@@ -228,15 +253,28 @@ ref="imageEffectsInput" type="file" accept=".jpg,.png,.jpeg" - @change="(e) => onImageUpload({ file: e.target.files[0], name: e.target.files[0]?.name })" + @change=" + (e) => onImageUpload({ file: e.target.files[0], name: e.target.files[0]?.name }) + " class="hidden" /> -
- - 上传图片 +
+ + 上传图片
@@ -254,7 +292,9 @@ @@ -262,7 +302,7 @@ @@ -288,7 +328,7 @@ @@ -310,11 +350,23 @@ @change="(e) => handleMultipleImageUpload(e)" class="hidden" /> -
- - 上传图片 +
+ + 上传图片
-
+
-
+
@@ -353,7 +409,7 @@ @@ -407,7 +463,12 @@
- +
{{ getFunctionName(item.type) }} @@ -488,7 +556,10 @@ @click="playMedia(item)" class="px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-1" > - + {{ item.type.includes('video') ? '播放' : '查看' }}