-
{{ item.title }}
-
{{
- item.major_model_version
- }}
-
用户上传
-
-
- 完整歌曲
-
-
-
- {{ item.ref_song.title }}
-
+
+
+
+
+
+ {{ item.tags || item.prompt }}
+
+
+
+
-
{{ item.tags }}
-
-
-
-
-
- {{ item.title }}
- {{ item.prompt }}
-
+
+
+
+
+
+
+
+
-
-
- {{ item.err_msg }}
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+ 生成进度
+ {{ item.progress }}%
+
+
+
+
+
+
+
+
+
{{ item.err_msg || '未知错误' }}
+
+
-
-
+
-
+
+
+
+
@@ -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') ? '播放' : '查看' }}