mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-11 21:54:26 +08:00
重构 suno 页面
This commit is contained in:
5
.claude/commands/refactor.md
Normal file
5
.claude/commands/refactor.md
Normal file
@@ -0,0 +1,5 @@
|
||||
重构当前页面代码
|
||||
|
||||
1. 把当前页面 JS 代码全部抽离,然后是采用 Pinia 重构
|
||||
2. 把当前页面 CSS 代码全部抽离,如果是 stylus 语法代码,则需要改成 SCSS 语法代码
|
||||
3. 尽量做到代码的复用性,不要重复造轮子
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
417
web/src/store/suno.js
Normal 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
@@ -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 })
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user