Suno页面重构完成

This commit is contained in:
GeekMaster
2025-08-07 16:26:12 +08:00
parent eab8265b9c
commit cb7235bb83
7 changed files with 162 additions and 53 deletions

View File

@@ -623,7 +623,6 @@ export const useJimengStore = defineStore('jimeng', () => {
replaceImg,
}
})
export const imageSizeOptions = [
{ label: '1:1 (1328x1328)', value: '1328x1328' },
{ label: '3:2 (1584x1056)', value: '1584x1056' },

View File

@@ -5,7 +5,8 @@ import Compressor from 'compressorjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { compact } from 'lodash'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { getSystemInfo } from './cache'
export const useSunoStore = defineStore('suno', () => {
// 响应式数据
@@ -60,6 +61,7 @@ export const useSunoStore = defineStore('suno', () => {
const editData = ref({ title: '', cover: '', id: 0 })
const promptPlaceholder = ref('请在这里输入你自己写的歌词...')
const isGenerating = ref(false)
const sunoPower = ref(0)
// 分页相关
const page = ref(1)
@@ -72,6 +74,12 @@ export const useSunoStore = defineStore('suno', () => {
// 计算属性
const hasRefSong = computed(() => refSong.value !== null)
onMounted(() => {
getSystemInfo().then((res) => {
sunoPower.value = res.data.suno_power
})
})
// 方法
const fetchData = async (_page) => {
if (_page) {
@@ -393,7 +401,7 @@ export const useSunoStore = defineStore('suno', () => {
pageSize,
total,
hasRefSong,
sunoPower,
// 方法
fetchData,
create,

View File

@@ -312,9 +312,10 @@
type="primary"
@click="store.submitTask"
:loading="store.submitting"
class="w-full"
size="large"
>
立即生成 ({{ store.currentPowerCost }}<i class="iconfont icon-vip2"></i>)
立即生成 ({{ store.currentPowerCost }} <i class="iconfont icon-vip2 !text-xs"></i>)
</el-button>
</div>
</div>

View File

@@ -204,7 +204,11 @@
<button @click="store.create" :disabled="store.loading" class="create-btn">
<i v-if="store.loading" class="iconfont icon-loading animate-spin"></i>
<i v-else class="iconfont icon-chuangzuo"></i>
<span>{{ store.loading ? '创作中...' : store.btnText }}</span>
<span
>{{ store.loading ? '创作中...' : store.btnText }} ({{ store.sunoPower }}
<i class="iconfont icon-vip2 !text-xs"></i>
)</span
>
</button>
</div>
@@ -516,7 +520,6 @@ import MusicPlayer from '@/components/MusicPlayer.vue'
import { checkSession } from '@/store/cache'
import { useSunoStore } from '@/store/suno'
import { InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
// 使用 Pinia store

View File

@@ -93,7 +93,7 @@ const aiTools = ref([
tag: 'AI音乐',
status: 'active',
statusText: '可用',
url: '/mobile/suno-create',
url: '/mobile/suno',
},
{
key: 'video',
@@ -104,7 +104,7 @@ const aiTools = ref([
tag: 'AI视频',
status: 'beta',
statusText: '测试版',
url: '/mobile/video-create',
url: '/mobile/video',
},
{
key: 'jimeng',
@@ -115,7 +115,7 @@ const aiTools = ref([
tag: 'AI绘画',
status: 'active',
statusText: '可用',
url: '/mobile/jimeng-create',
url: '/mobile/jimeng',
},
{
key: 'imgWall',

View File

@@ -151,21 +151,21 @@ const features = ref([
name: '音乐创作',
icon: 'icon-mp3',
color: '#EF4444',
url: '/mobile/suno-create',
url: '/mobile/suno',
},
{
key: 'video',
name: '视频生成',
icon: 'icon-video',
color: '#10B981',
url: '/mobile/video-create',
url: '/mobile/video',
},
{
key: 'jimeng',
name: '即梦AI',
icon: 'icon-jimeng',
color: '#F97316',
url: '/mobile/jimeng-create',
url: '/mobile/jimeng',
},
{ key: 'agent', name: '智能体', icon: 'icon-app', color: '#3B82F6', url: '/mobile/apps' },
{

View File

@@ -141,7 +141,10 @@
<!-- 续写歌曲 -->
<div v-if="refSong" class="bg-white rounded-xl p-4 shadow-sm border-l-4 border-orange-400">
<div class="flex items-center justify-between mb-3">
<h3 class="text-gray-900 font-medium">续写歌曲</h3>
<h3 class="text-gray-900 font-medium flex items-center">
<i class="iconfont icon-link mr-2 text-orange-500"></i>
续写歌曲
</h3>
<button
@click="removeRefSong"
class="px-3 py-1 text-sm bg-red-100 text-red-600 rounded-lg hover:bg-red-200 transition-colors"
@@ -154,41 +157,23 @@
<span class="text-gray-600">歌曲名称</span>
<span class="text-gray-900 font-medium">{{ refSong.title }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">歌曲时长</span>
<span class="text-gray-900 font-medium">{{ refSong.duration }}</span>
</div>
<div>
<label class="block text-gray-700 font-medium mb-2">续写开始时间()</label>
<input
v-model="refSong.extend_secs"
type="number"
:min="0"
:max="refSong.duration"
placeholder="从第几秒开始续写"
class="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
</div>
<!-- 上传音乐 -->
<div class="bg-white rounded-xl p-4 shadow-sm">
<label class="block text-gray-700 font-medium mb-3">上传音乐文件(可选)</label>
<div
class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-blue-400 hover:bg-blue-50 transition-colors cursor-pointer"
>
<input
ref="fileInput"
type="file"
accept=".wav,.mp3"
@change="handleFileSelect"
class="hidden"
/>
<div @click="$refs.fileInput.click()" class="flex flex-col items-center space-y-2">
<i class="iconfont icon-upload text-blue-500 text-2xl"></i>
<span class="text-gray-700 font-medium">上传音乐文件</span>
<small class="text-gray-500">支持 .wav, .mp3 格式</small>
</div>
</div>
<div v-if="uploadFiles.length > 0" class="mt-3 p-3 bg-gray-50 rounded-lg">
<div class="flex items-center space-x-2">
<i class="iconfont icon-success text-green-500"></i>
<span class="text-sm text-gray-700">{{ uploadFiles[0].name }}</span>
<p class="text-sm text-gray-500 mt-1">
建议从 {{ Math.floor(refSong.duration * 0.8) }}-{{ refSong.duration }} 秒开始续写
</p>
</div>
</div>
</div>
@@ -198,12 +183,40 @@
<button
@click="create"
:disabled="loading"
class="w-full py-4 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
class="w-full py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
>
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
<span>{{ loading ? '创作中...' : btnText }}</span>
</button>
</div>
<!-- 上传音乐 -->
<div class="bg-white rounded-xl p-4 shadow-sm">
<label class="block text-gray-700 font-medium mb-3">上传音乐文件</label>
<el-upload
ref="uploadRef"
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
:before-upload="beforeUpload"
accept=".wav,.mp3"
class="upload-area w-full"
>
<template #trigger>
<el-button class="upload-btn w-full" size="large" type="primary">
<i class="iconfont icon-upload mr-2"></i>
<span>上传音乐</span>
</el-button>
</template>
</el-upload>
<div class="mt-3 p-3 bg-gray-50 rounded-lg text-sm text-gray-500">
<div class="upload-tips">
<p> 上传你自己的音乐文件然后进行二次创作</p>
<p> 请上传6-60秒的原始音频</p>
<p> 检测到人声的音频将仅设为私人音频</p>
</div>
</div>
</div>
</div>
<!-- 作品列表 -->
@@ -354,6 +367,14 @@
<i v-else class="iconfont icon-download !text-xs"></i>
<span>{{ item.downloading ? '下载中...' : '下载' }}</span>
</button>
<button
v-if="item.progress === 100"
@click="extend(item)"
class="px-3 py-1.5 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 transition-colors flex items-center justify-center min-w-[60px]"
>
<i class="iconfont icon-link !text-xs mr-1"></i>
<span>续写</span>
</button>
</div>
<button
@click="showDeleteDialog(item)"
@@ -494,6 +515,7 @@ 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 deleteItem = ref(null)
@@ -549,7 +571,8 @@ onMounted(() => {
// 启动定时轮询,检查任务状态
tastPullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchData(1) // 只刷新第一页数据
// 只刷新第一页数据,用于检查任务状态变化
refreshFirstPage()
}
}, 5000)
@@ -620,17 +643,27 @@ const createLyric = () => {
})
}
const handleFileSelect = (event) => {
const file = event.target.files[0]
if (!file) return
const handleFileChange = (file) => {
uploadFiles.value = [file]
if (file.status === 'ready') {
// 文件已准备好,可以进行上传
uploadAudio(file)
}
}
uploadFiles.value = [{ file, name: file.name }]
uploadAudio({ file, name: file.name })
const beforeUpload = (file) => {
// 限制文件大小,例如 10MB
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
showToastMessage('文件大小不能超过 10MB!', 'error')
return false
}
return true
}
const uploadAudio = (file) => {
const formData = new FormData()
formData.append('file', file.file, file.name)
formData.append('file', file.raw, file.name)
showLoading('正在上传文件...')
httpPost('/api/upload', formData)
@@ -644,6 +677,12 @@ const uploadAudio = (file) => {
fetchData(1)
showToastMessage('歌曲上传成功', 'success')
removeRefSong()
// 清空上传文件列表
uploadFiles.value = []
// 清空 el-upload 组件的文件列表
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
})
.catch((e) => {
showToastMessage('歌曲上传失败:' + e.message, 'error')
@@ -725,11 +764,7 @@ const fetchData = (_page) => {
listLoading.value = false
taskPulling.value = needPull
// 如果任务有变化,则刷新任务列表
if (JSON.stringify(list.value) !== JSON.stringify(items)) {
list.value = items
}
// 分页逻辑:第一页替换数据,其他页追加数据
if (page.value === 1) {
list.value = items
} else {
@@ -753,6 +788,44 @@ const loadMore = () => {
}
}
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.progress === 100) {
v.major_model_version = v['raw_data']['major_model_version']
}
// 检查是否有未完成的任务(进度为 0 或 102
if (v.progress === 0 || v.progress === 102) {
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
@@ -817,6 +890,16 @@ const showDeleteDialog = (item) => {
})
}
const extend = (item) => {
refSong.value = item
refSong.value.extend_secs = item.duration
data.value.title = item.title
custom.value = true
btnText.value = '续写歌曲'
// 滚动到页面顶部
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const removeRefSong = () => {
refSong.value = null
btnText.value = '开始创作'
@@ -933,4 +1016,19 @@ const removeRefSong = () => {
background-color: #4b5563;
}
}
/* el-upload 组件样式定制 */
.upload-area {
width: 100%;
:deep(.el-upload) {
width: 100%;
display: block;
}
:deep(.el-button) {
width: 100%;
display: block;
}
}
</style>