Files
geekai/web/src/store/mobile/suno.js
T
RockYang d4fd38ab7e feat: 迁移 geekai-plus v4.3.1 到开源版
- 同步 Plus v4.3.1 功能源并移除商业 License 闭环
- 更新开源镜像命名、Docker 部署版本和 geekai 数据库配置
- 补充前端 ESLint 检查配置并修复存量解析与模板问题
- 保留 JWT、管理员权限、API Key 和 OAuth 等正常鉴权机制
2026-09-13 14:37:12 +08:00

367 lines
9.9 KiB
JavaScript

import { getSystemInfo } from '@/store/cache'
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
import { httpDownload, httpGet, httpPost } from '@/utils/http'
import { replaceImg } from '@/utils/libs'
import { defineStore } from 'pinia'
import { showConfirmDialog } from 'vant'
import { reactive, ref } from 'vue'
export const useSunoStore = defineStore('suno', () => {
// 状态
const custom = ref(false)
const data = reactive({
model: 'chirp-auk',
tags: '',
lyrics: '',
prompt: '',
title: '',
instrumental: false,
ref_task_id: '',
extend_secs: 0,
ref_song_id: '',
type: 1,
})
const loading = ref(false)
const list = ref([])
const listLoading = ref(false)
const listFinished = ref(false)
const btnText = ref('开始创作')
const refSong = ref(null)
const showModelPicker = ref(false)
const showPlayer = ref(false)
const showDeleteModal = ref(false)
const currentAudio = ref('')
const uploadFiles = ref([])
const uploadRef = ref(null)
const isGenerating = ref(false)
const deleting = 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: 'electronic' },
{ label: '钢琴', value: 'piano' },
{ label: '吉他', value: 'guitar' },
{ label: '嘻哈', value: 'hip hop' },
])
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const taskPulling = ref(true)
const tastPullHandler = ref(null)
const sunoPowerCost = ref(0)
onMounted(() => {
getSystemInfo().then((res) => {
sunoPowerCost.value = res.data.suno_power
})
})
// 方法
const onModelSelect = (selectedModel) => {
data.model = selectedModel.value
}
const selectTag = (tag) => {
if (data.tags.length + tag.value.length >= 119) {
showMessageError('标签长度超出限制')
return
}
const currentTags = data.tags.split(',').filter((t) => t.trim())
if (!currentTags.includes(tag.value)) {
currentTags.push(tag.value)
data.tags = currentTags.join(',')
}
}
const createLyric = () => {
if (data.lyrics === '') {
showMessageError('请输入歌词描述')
return
}
isGenerating.value = true
httpPost('/api/prompt/lyric', { prompt: data.lyrics })
.then((res) => {
const lines = res.data.split('\n')
data.title = lines.shift().replace(/\*/g, '')
lines.shift()
data.lyrics = lines.join('\n')
showMessageOK('歌词生成成功')
})
.catch((e) => {
showMessageError('歌词生成失败:' + e.message)
})
.finally(() => {
isGenerating.value = false
})
}
const handleFileChange = (file) => {
uploadFiles.value = [file]
if (file.status === 'ready') {
uploadAudio(file)
}
}
const beforeUpload = (file) => {
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
showMessageError('文件大小不能超过 10MB!')
return false
}
return true
}
const uploadAudio = (file) => {
const formData = new FormData()
formData.append('file', file.raw, 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('歌曲上传成功')
removeRefSong()
uploadFiles.value = []
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
})
.catch((e) => {
showMessageError('歌曲上传失败:' + e.message)
})
.finally(() => {
closeLoading()
})
})
.catch((e) => {
showMessageError('文件上传失败:' + e.message)
})
.finally(() => {
closeLoading()
})
}
const create = () => {
data.type = custom.value ? 2 : 1
data.ref_task_id = refSong.value ? refSong.value.task_id : ''
data.ref_song_id = refSong.value ? refSong.value.song_id : ''
data.extend_secs = refSong.value ? refSong.value.extend_secs : 0
if (refSong.value) {
if (data.extend_secs > refSong.value.duration) {
showMessageError('续写开始时间不能超过原歌曲长度')
return
}
} else if (custom.value) {
if (data.lyrics === '') {
showMessageError('请输入歌词')
return
}
if (data.title === '') {
showMessageError('请输入歌曲标题')
return
}
} else {
if (data.prompt === '') {
showMessageError('请输入歌曲描述')
return
}
}
loading.value = true
httpPost('/api/suno/create', data)
.then(() => {
fetchData(1)
taskPulling.value = true
showMessageOK('创建任务成功')
})
.catch((e) => {
showMessageError('创建任务失败:' + e.message)
})
.finally(() => {
loading.value = false
})
}
const fetchData = (_page) => {
if (_page) {
page.value = _page
}
listLoading.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.status === 'success') {
v.major_model_version = v['raw_data']['major_model_version']
}
if (v.status === 'pending' || v.status === 'in_progress' || v.status === 'downloading') {
needPull = true
}
items.push(v)
}
listLoading.value = false
taskPulling.value = needPull
if (page.value === 1) {
list.value = items
} else {
list.value.push(...items)
}
if (items.length < pageSize.value) {
listFinished.value = true
}
})
.catch((e) => {
listLoading.value = false
showMessageError('获取作品列表失败:' + e.message)
})
}
const loadMore = () => {
if (!listFinished.value && !listLoading.value) {
page.value++
fetchData()
}
}
const refreshFirstPage = () => {
const currentPage = page.value
const currentList = [...list.value]
httpGet('/api/suno/list', { page: 1, page_size: pageSize.value })
.then((res) => {
let needPull = false
const firstPageItems = []
for (let v of res.data.items) {
if (v.status === 'success') {
v.major_model_version = v['raw_data']['major_model_version']
}
if (v.status === 'pending' || v.status === 'in_progress' || v.status === 'downloading') {
needPull = true
}
firstPageItems.push(v)
}
taskPulling.value = needPull
if (currentPage === 1) {
list.value = firstPageItems
} else {
const otherPagesData = currentList.slice(pageSize.value)
list.value = [...firstPageItems, ...otherPagesData]
}
})
.catch((e) => {
console.error('刷新第一页数据失败:', e)
})
}
const play = (item) => {
currentAudio.value = item.audio_url
showPlayer.value = true
}
const download = (item) => {
const url = replaceImg(item.audio_url)
const downloadURL = `/api/download?url=${url}`
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
})
.finally(() => {
item.downloading = false
})
}
const removeJob = (item) => {
showConfirmDialog({
title: '确认删除',
message: '此操作将会删除任务相关文件,继续操作吗?',
confirmButtonText: '确认删除',
cancelButtonText: '取消',
}).then(() => {
httpGet('/api/suno/remove', { id: item.id })
.then(() => {
showMessageOK('任务删除成功')
fetchData(1)
})
.catch(() => {
showMessageError('任务删除失败')
})
})
}
const extend = (item) => {
refSong.value = item
refSong.value.extend_secs = item.duration
data.title = item.title
custom.value = true
btnText.value = '续写歌曲'
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const removeRefSong = () => {
refSong.value = null
btnText.value = '开始创作'
}
// 副作用(定时轮询、滚动监听)建议在页面层处理,store 只暴露方法
return {
// 状态
custom,
data,
loading,
list,
listLoading,
listFinished,
btnText,
refSong,
showModelPicker,
showPlayer,
showDeleteModal,
currentAudio,
uploadFiles,
uploadRef,
isGenerating,
deleting,
models,
tags,
page,
pageSize,
total,
taskPulling,
tastPullHandler,
sunoPowerCost,
// 方法
onModelSelect,
selectTag,
createLyric,
handleFileChange,
beforeUpload,
uploadAudio,
create,
fetchData,
loadMore,
refreshFirstPage,
play,
download,
removeJob,
extend,
removeRefSong,
}
})