重构 suno 页面

This commit is contained in:
GeekMaster
2025-08-07 11:57:32 +08:00
parent e8e3783af0
commit 5f24df6cee
8 changed files with 1268 additions and 716 deletions

View File

@@ -0,0 +1,5 @@
重构当前页面代码
1. 把当前页面 JS 代码全部抽离,然后是采用 Pinia 重构
2. 把当前页面 CSS 代码全部抽离,如果是 stylus 语法代码,则需要改成 SCSS 语法代码
3. 尽量做到代码的复用性,不要重复造轮子

View File

@@ -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;
}
}
}
.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);
}
}
}

View File

@@ -1,82 +0,0 @@
<template>
<div class="black-input-wrapper">
<el-input
v-model="model"
:type="type"
:rows="rows"
@input="onInput"
resize="none"
:placeholder="placeholder"
:maxlength="maxlength"
/>
<div class="word-stat" v-if="rows > 1">
<span>{{ value.length }}</span
>/<span>{{ maxlength }}</span>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
value: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
type: {
type: String,
default: 'input',
},
rows: {
type: Number,
default: 5,
},
maxlength: {
type: Number,
default: 1024,
},
})
watch(
() => props.value,
(newValue) => {
model.value = newValue
}
)
const model = ref(props.value)
// eslint-disable-next-line no-undef
const emits = defineEmits(['update:value'])
const onInput = (value) => {
emits('update:value', value)
}
</script>
<style lang="scss">
.black-input-wrapper {
position: relative;
.el-textarea__inner {
padding: 20px;
font-size: 16px;
}
.word-stat {
position: absolute;
bottom: 10px;
right: 10px;
color: rgb(209, 203, 199);
font-family: Neue Montreal, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji,
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-size: 0.875rem;
line-height: 1.25rem;
span {
margin: 0 1px;
}
}
}
</style>

View File

@@ -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'),
},
],

417
web/src/store/suno.js Normal file
View File

@@ -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,
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -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',
]"
>
<i :class="getCategoryIcon(category.key)" class="text-2xl mb-2"></i>
@@ -71,7 +71,7 @@
<!-- 图片尺寸 -->
<CustomSelect
v-model="textToImageParams.size"
:options="imageSizeOptions.map(opt => ({ label: opt.label, value: opt.value }))"
:options="imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))"
label="图片尺寸"
title="选择尺寸"
/>
@@ -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"
/>
<div @click="$refs.imageToImageInput?.click()" class="flex flex-col items-center space-y-2">
<i v-if="!imageToImageParams.image_input.length" class="iconfont icon-upload text-blue-500 text-2xl"></i>
<span v-if="!imageToImageParams.image_input.length" class="text-gray-700 font-medium">上传图片</span>
<div
@click="$refs.imageToImageInput?.click()"
class="flex flex-col items-center space-y-2"
>
<i
v-if="!imageToImageParams.image_input.length"
class="iconfont icon-upload text-blue-500 text-2xl"
></i>
<span v-if="!imageToImageParams.image_input.length" class="text-gray-700 font-medium"
>上传图片</span
>
<div v-else class="relative">
<el-image
:src="imageToImageParams.image_input[0]?.url || imageToImageParams.image_input[0]?.content"
:src="
imageToImageParams.image_input[0]?.url ||
imageToImageParams.image_input[0]?.content
"
fit="cover"
class="w-32 h-32 rounded"
/>
@@ -151,7 +164,7 @@
<!-- 图片尺寸 -->
<CustomSelect
v-model="imageToImageParams.size"
:options="imageSizeOptions.map(opt => ({ label: opt.label, value: opt.value }))"
:options="imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))"
label="图片尺寸"
title="选择尺寸"
/>
@@ -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"
/>
<div @click="$refs.imageEditInput?.click()" class="flex flex-col items-center space-y-2">
<i v-if="!imageEditParams.image_urls.length" class="iconfont icon-upload text-blue-500 text-2xl"></i>
<span v-if="!imageEditParams.image_urls.length" class="text-gray-700 font-medium">上传图片</span>
<div
@click="$refs.imageEditInput?.click()"
class="flex flex-col items-center space-y-2"
>
<i
v-if="!imageEditParams.image_urls.length"
class="iconfont icon-upload text-blue-500 text-2xl"
></i>
<span v-if="!imageEditParams.image_urls.length" class="text-gray-700 font-medium"
>上传图片</span
>
<div v-else class="relative">
<el-image
:src="imageEditParams.image_urls[0]?.url || imageEditParams.image_urls[0]?.content"
:src="
imageEditParams.image_urls[0]?.url || imageEditParams.image_urls[0]?.content
"
fit="cover"
class="w-32 h-32 rounded"
/>
@@ -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"
/>
<div @click="$refs.imageEffectsInput?.click()" class="flex flex-col items-center space-y-2">
<i v-if="!imageEffectsParams.image_input1.length" class="iconfont icon-upload text-blue-500 text-2xl"></i>
<span v-if="!imageEffectsParams.image_input1.length" class="text-gray-700 font-medium">上传图片</span>
<div
@click="$refs.imageEffectsInput?.click()"
class="flex flex-col items-center space-y-2"
>
<i
v-if="!imageEffectsParams.image_input1.length"
class="iconfont icon-upload text-blue-500 text-2xl"
></i>
<span v-if="!imageEffectsParams.image_input1.length" class="text-gray-700 font-medium"
>上传图片</span
>
<div v-else class="relative">
<el-image
:src="imageEffectsParams.image_input1[0]?.url || imageEffectsParams.image_input1[0]?.content"
:src="
imageEffectsParams.image_input1[0]?.url ||
imageEffectsParams.image_input1[0]?.content
"
fit="cover"
class="w-32 h-32 rounded"
/>
@@ -254,7 +292,9 @@
<!-- 特效模板 -->
<CustomSelect
v-model="imageEffectsParams.template_id"
:options="imageEffectsTemplateOptions.map(opt => ({ label: opt.label, value: opt.value }))"
:options="
imageEffectsTemplateOptions.map((opt) => ({ label: opt.label, value: opt.value }))
"
label="特效模板"
title="选择特效模板"
/>
@@ -262,7 +302,7 @@
<!-- 输出尺寸 -->
<CustomSelect
v-model="imageEffectsParams.size"
:options="imageSizeOptions.map(opt => ({ label: opt.label, value: opt.value }))"
:options="imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))"
label="输出尺寸"
title="选择尺寸"
/>
@@ -288,7 +328,7 @@
<!-- 视频比例 -->
<CustomSelect
v-model="textToVideoParams.aspect_ratio"
:options="videoAspectRatioOptions.map(opt => ({ label: opt.label, value: opt.value }))"
:options="videoAspectRatioOptions.map((opt) => ({ label: opt.label, value: opt.value }))"
label="视频比例"
title="选择比例"
/>
@@ -310,11 +350,23 @@
@change="(e) => handleMultipleImageUpload(e)"
class="hidden"
/>
<div @click="$refs.imageToVideoInput?.click()" class="flex flex-col items-center space-y-2">
<i v-if="!imageToVideoParams.image_urls.length" class="iconfont icon-upload text-blue-500 text-2xl"></i>
<span v-if="!imageToVideoParams.image_urls.length" class="text-gray-700 font-medium">上传图片</span>
<div
@click="$refs.imageToVideoInput?.click()"
class="flex flex-col items-center space-y-2"
>
<i
v-if="!imageToVideoParams.image_urls.length"
class="iconfont icon-upload text-blue-500 text-2xl"
></i>
<span v-if="!imageToVideoParams.image_urls.length" class="text-gray-700 font-medium"
>上传图片</span
>
<div v-else class="flex space-x-3">
<div v-for="(image, index) in imageToVideoParams.image_urls" :key="index" class="relative">
<div
v-for="(image, index) in imageToVideoParams.image_urls"
:key="index"
class="relative"
>
<el-image
:src="image?.url || image?.content"
fit="cover"
@@ -327,7 +379,11 @@
<i class="iconfont icon-close"></i>
</button>
</div>
<div v-if="imageToVideoParams.image_urls.length < 2" @click.stop="$refs.imageToVideoInput?.click()" class="w-24 h-24 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-blue-400">
<div
v-if="imageToVideoParams.image_urls.length < 2"
@click.stop="$refs.imageToVideoInput?.click()"
class="w-24 h-24 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-blue-400"
>
<i class="iconfont icon-plus text-gray-400 text-xl"></i>
</div>
</div>
@@ -353,7 +409,7 @@
<!-- 视频比例 -->
<CustomSelect
v-model="imageToVideoParams.aspect_ratio"
:options="videoAspectRatioOptions.map(opt => ({ label: opt.label, value: opt.value }))"
:options="videoAspectRatioOptions.map((opt) => ({ label: opt.label, value: opt.value }))"
label="视频比例"
title="选择比例"
/>
@@ -407,7 +463,12 @@
</template>
</el-image>
<div v-else class="w-full h-full flex items-center justify-center bg-gray-100">
<i :class="item.type.includes('video') ? 'iconfont icon-video' : 'iconfont icon-image'" class="text-gray-400 text-xl"></i>
<i
:class="
item.type.includes('video') ? 'iconfont icon-video' : 'iconfont icon-image'
"
class="text-gray-400 text-xl"
></i>
</div>
<!-- 播放/查看按钮 -->
<button
@@ -415,7 +476,12 @@
@click="playMedia(item)"
class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 opacity-0 hover:opacity-100 transition-opacity"
>
<i :class="item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'" class="text-white text-xl"></i>
<i
:class="
item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'
"
class="text-white text-xl"
></i>
</button>
<!-- 进度动画 -->
<div
@@ -465,7 +531,9 @@
<span
:class="[
'px-2 py-1 text-xs rounded-full',
getTaskType(item.type) === 'warning' ? 'bg-yellow-100 text-yellow-600' : 'bg-blue-100 text-blue-600'
getTaskType(item.type) === 'warning'
? 'bg-yellow-100 text-yellow-600'
: 'bg-blue-100 text-blue-600',
]"
>
{{ 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"
>
<i :class="item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'" class="!text-xs"></i>
<i
:class="item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'"
class="!text-xs"
></i>
<span>{{ item.type.includes('video') ? '播放' : '查看' }}</span>
</button>
<button
@@ -567,13 +638,13 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { showConfirmDialog } from 'vant'
import { httpGet, httpPost } from '@/utils/http'
import { checkSession } from '@/store/cache'
import CustomSelect from '@/components/ui/CustomSelect.vue'
import { showMessageSuccess, showMessageError, showLoading, closeLoading } from '@/utils/dialog'
import { checkSession } from '@/store/cache'
import { closeLoading, showLoading, showMessageError, showMessageSuccess } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { showConfirmDialog } from 'vant'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -592,7 +663,7 @@ const categories = ref([
{ key: 'image_generation', name: '图像生成' },
{ key: 'image_editing', name: '图像编辑' },
{ key: 'image_effects', name: '图像特效' },
{ key: 'video_generation', name: '视频生成' }
{ key: 'video_generation', name: '视频生成' },
])
// 选项数据
@@ -601,21 +672,21 @@ const imageSizeOptions = [
{ label: '768x768', value: '768x768' },
{ label: '1024x1024', value: '1024x1024' },
{ label: '1024x1536', value: '1024x1536' },
{ label: '1536x1024', value: '1536x1024' }
{ label: '1536x1024', value: '1536x1024' },
]
const videoAspectRatioOptions = [
{ label: '16:9', value: '16:9' },
{ label: '9:16', value: '9:16' },
{ label: '1:1', value: '1:1' },
{ label: '4:3', value: '4:3' }
{ label: '4:3', value: '4:3' },
]
const imageEffectsTemplateOptions = [
{ label: '亚克力装饰', value: 'acrylic_ornaments' },
{ label: '天使小雕像', value: 'angel_figurine' },
{ label: '毛毫3D拍立得', value: 'felt_3d_polaroid' },
{ label: '水彩插图', value: 'watercolor_illustration' }
{ label: '水彩插图', value: 'watercolor_illustration' },
]
// 当前提示词
@@ -730,7 +801,7 @@ const switchInputMode = () => {
// 处理多图片上传
const handleMultipleImageUpload = (event) => {
const files = Array.from(event.target.files)
files.forEach(file => {
files.forEach((file) => {
if (imageToVideoParams.value.image_urls.length < 2) {
onImageUpload({ file, name: file.name })
}
@@ -875,7 +946,7 @@ const removeJob = (item) => {
title: '确认删除',
message: '此操作将会删除任务相关文件,继续操作吗?',
confirmButtonText: '确认删除',
cancelButtonText: '取消'
cancelButtonText: '取消',
})
.then(() => {
httpGet('/api/jimeng/remove', { id: item.id })

View File

@@ -462,14 +462,14 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { httpGet, httpPost, httpDownload } from '@/utils/http'
import { checkSession } from '@/store/cache'
import CustomSelect from '@/views/mobile/components/CustomSelect.vue'
import { showToastMessage, showLoading, closeLoading } from '@/utils/dialog'
import { closeLoading, showLoading, showToastMessage } from '@/utils/dialog'
import { httpDownload, httpGet, httpPost } from '@/utils/http'
import { replaceImg } from '@/utils/libs'
import CustomSelect from '@/views/mobile/components/CustomSelect.vue'
import { showConfirmDialog } from 'vant'
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@@ -826,7 +826,7 @@ const removeRefSong = () => {
}
</script>
<style scoped>
<style lang="scss" scoped>
/* 自定义动画 */
@keyframes fade-in {
from {