diff --git a/api/handler/jimeng_handler.go b/api/handler/jimeng_handler.go index 9cc0dc68..a7680e87 100644 --- a/api/handler/jimeng_handler.go +++ b/api/handler/jimeng_handler.go @@ -153,12 +153,7 @@ func (h *JimengHandler) CreateTask(c *gin.Context) { "seed": req.Seed, "scale": req.Scale, } - if len(req.ImageUrls) > 0 { - params["image_urls"] = req.ImageUrls - } - if len(req.BinaryDataBase64) > 0 { - params["binary_data_base64"] = req.BinaryDataBase64 - } + params["image_urls"] = []string{req.ImageInput} case "image_effects": powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEffects) taskType = model.JMTaskTypeImageEffects diff --git a/web/src/assets/css/mobile/suno.scss b/web/src/assets/css/mobile/suno.scss index f7cad1dd..3a5d670e 100644 --- a/web/src/assets/css/mobile/suno.scss +++ b/web/src/assets/css/mobile/suno.scss @@ -1,6 +1,6 @@ /* 来自 SunoCreate.vue 的样式,已迁移至此,供移动端页面使用 */ -// 自定义动画 +/* 自定义动画 */ @keyframes fade-in { from { opacity: 0; @@ -61,7 +61,7 @@ animation: scale-up 0.3s ease-out; } -// 文本截断 +/* 文本截断 */ .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; @@ -69,47 +69,56 @@ overflow: hidden; } -// 滚动监听自动加载更多 +/* 滚动监听自动加载更多 */ .scroll-container { height: 100vh; overflow-y: auto; } -// 深色模式适配 +/* 深色模式适配 */ @media (prefers-color-scheme: dark) { .bg-gray-50 { background-color: #1f2937; } + .bg-white { background-color: #374151; } + .text-gray-900 { color: #f9fafb; } + .text-gray-700 { color: #d1d5db; } + .text-gray-600 { color: #9ca3af; } + .text-gray-500 { color: #6b7280; } + .border-gray-200 { border-color: #4b5563; } + .bg-gray-100:hover { background-color: #4b5563; } } -// el-upload 组件样式定制 +/* el-upload 组件样式定制 */ .upload-area { width: 100%; + :deep(.el-upload) { width: 100%; display: block; } + :deep(.el-button) { width: 100%; display: block; diff --git a/web/src/store/jimeng.js b/web/src/store/jimeng.js index 9c75ac29..8be97d0e 100644 --- a/web/src/store/jimeng.js +++ b/web/src/store/jimeng.js @@ -137,13 +137,13 @@ export const useJimengStore = defineStore('jimeng', () => { }) const imageEditParams = reactive({ - image_urls: '', + image_input: '', scale: 0.5, seed: -1, }) const imageEffectsParams = reactive({ - image_input1: '', + image_input: '', template_id: '', size: '1328x1328', }) @@ -154,7 +154,7 @@ export const useJimengStore = defineStore('jimeng', () => { }) const imageToVideoParams = reactive({ - image_urls: [], + image_input: [], aspect_ratio: '16:9', seed: -1, }) @@ -374,7 +374,7 @@ export const useJimengStore = defineStore('jimeng', () => { break case 'image_to_image': Object.assign(requestData, { - image_input: imageToImageParams.image_input, + image_input: imageToImageParams.image_input[0], width: parseInt(imageToImageParams.size.split('x')[0]), height: parseInt(imageToImageParams.size.split('x')[1]), gpen: imageToImageParams.gpen, @@ -386,14 +386,14 @@ export const useJimengStore = defineStore('jimeng', () => { break case 'image_edit': Object.assign(requestData, { - image_urls: [imageEditParams.image_urls], + image_input: imageEditParams.image_input[0], scale: imageEditParams.scale, seed: imageEditParams.seed, }) break case 'image_effects': Object.assign(requestData, { - image_input: imageEffectsParams.image_input1, + image_input: imageEffectsParams.image_input[0], template_id: imageEffectsParams.template_id, width: parseInt(imageEffectsParams.size.split('x')[0]), height: parseInt(imageEffectsParams.size.split('x')[1]), @@ -408,7 +408,7 @@ export const useJimengStore = defineStore('jimeng', () => { break case 'image_to_video': Object.assign(requestData, { - image_urls: imageToVideoParams.image_urls, + image_urls: imageToVideoParams.image_input, aspect_ratio: imageToVideoParams.aspect_ratio, seed: imageToVideoParams.seed, }) @@ -498,62 +498,6 @@ export const useJimengStore = defineStore('jimeng', () => { showDialog.value = true } - // 画同款功能 - const drawSame = (item) => { - // 联动功能开关 - if (item.type === 'text_to_image' || item.type === 'image_to_image') { - activeCategory.value = 'image_generation' - useImageInput.value = item.type === 'image_to_image' - } else if (item.type === 'text_to_video' || item.type === 'image_to_video') { - activeCategory.value = 'video_generation' - useImageInput.value = item.type === 'image_to_video' - } else if (item.type === 'image_edit') { - activeCategory.value = 'image_editing' - } else if (item.type === 'image_effects') { - activeCategory.value = 'image_effects' - } - switchFunction(item.type) - nextTick(() => { - currentPrompt.value = item.prompt - }) - if (item.type === 'text_to_image') { - if (item.width && item.height) { - textToImageParams.size = `${item.width}x${item.height}` - } - if (item.scale) textToImageParams.scale = item.scale - if (item.seed) textToImageParams.seed = item.seed - if (item.use_pre_llm !== undefined) textToImageParams.use_pre_llm = item.use_pre_llm - } else if (item.type === 'image_to_image') { - if (item.image_input) imageToImageParams.image_input = item.image_input - if (item.width && item.height) { - imageToImageParams.size = `${item.width}x${item.height}` - } - if (item.gpen) imageToImageParams.gpen = item.gpen - if (item.skin) imageToImageParams.skin = item.skin - if (item.skin_unifi) imageToImageParams.skin_unifi = item.skin_unifi - if (item.gen_mode) imageToImageParams.gen_mode = item.gen_mode - if (item.seed) imageToImageParams.seed = item.seed - } else if (item.type === 'image_edit') { - if (item.image_urls) imageEditParams.image_urls = item.image_urls - if (item.scale) imageEditParams.scale = item.scale - if (item.seed) imageEditParams.seed = item.seed - } else if (item.type === 'image_effects') { - if (item.image_input1) imageEffectsParams.image_input1 = item.image_input1 - if (item.template_id) imageEffectsParams.template_id = item.template_id - if (item.width && item.height) { - imageEffectsParams.size = `${item.width}x${item.height}` - } - } else if (item.type === 'text_to_video') { - if (item.aspect_ratio) textToVideoParams.aspect_ratio = item.aspect_ratio - if (item.seed) textToVideoParams.seed = item.seed - } else if (item.type === 'image_to_video') { - if (item.image_urls) imageToVideoParams.image_urls = item.image_urls - if (item.aspect_ratio) imageToVideoParams.aspect_ratio = item.aspect_ratio - if (item.seed) imageToVideoParams.seed = item.seed - } - showMessageOK('已填入全部参数,可直接生成同款') - } - // 页面卸载时清理轮询 const cleanup = () => { stopPolling() @@ -616,7 +560,6 @@ export const useJimengStore = defineStore('jimeng', () => { removeJob, playVideo, cleanup, - drawSame, // 工具函数 substr, diff --git a/web/src/store/mobile/jimeng.js b/web/src/store/mobile/jimeng.js index fcef03fb..8f5f50b3 100644 --- a/web/src/store/mobile/jimeng.js +++ b/web/src/store/mobile/jimeng.js @@ -1,5 +1,6 @@ import { showMessageError, showMessageOK } from '@/utils/dialog' -import { httpGet, httpPost } from '@/utils/http' +import { httpDownload, httpGet, httpPost } from '@/utils/http' +import { replaceImg } from '@/utils/libs' import { defineStore } from 'pinia' import { showConfirmDialog } from 'vant' import { computed, reactive, ref, watch } from 'vue' @@ -144,13 +145,13 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { }) const imageEditParams = reactive({ - image_urls: '', + image_input: '', scale: 0.5, seed: -1, }) const imageEffectsParams = reactive({ - image_input1: '', + image_input: '', template_id: '', size: '1328x1328', }) @@ -245,7 +246,7 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { break case 'image_to_image': Object.assign(requestData, { - image_input: imageToImageParams.image_input, + image_input: imageToImageParams.image_input[0], width: parseInt(imageToImageParams.size.split('x')[0]), height: parseInt(imageToImageParams.size.split('x')[1]), gpen: imageToImageParams.gpen, @@ -257,14 +258,14 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { break case 'image_edit': Object.assign(requestData, { - image_urls: [imageEditParams.image_urls], + image_input: imageEditParams.image_input[0], scale: imageEditParams.scale, seed: imageEditParams.seed, }) break case 'image_effects': Object.assign(requestData, { - image_input: imageEffectsParams.image_input1, + image_input: imageEffectsParams.image_input[0], template_id: imageEffectsParams.template_id, width: parseInt(imageEffectsParams.size.split('x')[0]), height: parseInt(imageEffectsParams.size.split('x')[1]), @@ -279,7 +280,7 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { break case 'image_to_video': Object.assign(requestData, { - image_urls: imageToVideoParams.image_urls, + image_urls: imageToVideoParams.image_input, aspect_ratio: imageToVideoParams.aspect_ratio, seed: imageToVideoParams.seed, }) @@ -312,11 +313,13 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { total.value = res.data.total let needPull = false const items = [] - for (let v of res.data.items) { - if (v.status === 'in_queue' || v.status === 'generating') { - needPull = true + if (res.data.items) { + for (let v of res.data.items) { + if (v.status === 'in_queue' || v.status === 'generating') { + needPull = true + } + items.push(v) } - items.push(v) } listLoading.value = false taskPulling.value = needPull @@ -347,20 +350,33 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { showMediaDialog.value = true } - const downloadFile = (item) => { + const downloadFile = async (item) => { + const url = replaceImg(item.video_url || item.img_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 - const link = document.createElement('a') - link.href = item.img_url || item.video_url - link.download = item.title || 'file' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - item.downloading = false - showMessageSuccess('开始下载') + + 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 retryTask = (id) => { - return httpPost('/api/jimeng/retry', { id }) + return httpGet('/api/jimeng/retry', { id }) .then(() => { showMessageOK('重试任务成功') fetchData(1) @@ -425,60 +441,6 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { currentMediaUrl.value = '' } - // 新增:画同款功能 - const drawSame = (item) => { - // 设置当前提示词 - currentPrompt.value = item.prompt - - // 根据任务类型设置相应的参数 - switch (item.type) { - case 'text_to_image': - activeCategory.value = 'image_generation' - useImageInput.value = false - // 设置图片尺寸(如果有的话) - if (item.width && item.height) { - textToImageParams.size = `${item.width}x${item.height}` - } - break - case 'image_to_image': - activeCategory.value = 'image_generation' - useImageInput.value = true - // 设置图片尺寸(如果有的话) - if (item.width && item.height) { - imageToImageParams.size = `${item.width}x${item.height}` - } - break - case 'image_edit': - activeCategory.value = 'image_editing' - break - case 'image_effects': - activeCategory.value = 'image_effects' - // 设置特效模板(如果有的话) - if (item.template_id) { - imageEffectsParams.template_id = item.template_id - } - break - case 'text_to_video': - activeCategory.value = 'video_generation' - useImageInput.value = false - // 设置视频比例(如果有的话) - if (item.aspect_ratio) { - textToVideoParams.aspect_ratio = item.aspect_ratio - } - break - case 'image_to_video': - activeCategory.value = 'video_generation' - useImageInput.value = true - // 设置视频比例(如果有的话) - if (item.aspect_ratio) { - imageToVideoParams.aspect_ratio = item.aspect_ratio - } - break - } - - showMessageOK('已设置画同款参数') - } - // 新增:复制提示词功能 const copyPrompt = (prompt) => { navigator.clipboard @@ -556,7 +518,6 @@ export const useJimengStore = defineStore('mobile-jimeng', () => { stopTaskPolling, closeMediaDialog, fetchPowerConfig, - drawSame, copyPrompt, copyErrorMsg, init, diff --git a/web/src/store/mobile/suno.js b/web/src/store/mobile/suno.js index 6d10ef9c..6808e316 100644 --- a/web/src/store/mobile/suno.js +++ b/web/src/store/mobile/suno.js @@ -1,5 +1,5 @@ import { getSystemInfo } from '@/store/cache' -import { closeLoading, showLoading, showToastMessage } from '@/utils/dialog' +import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog' import { httpDownload, httpGet, httpPost } from '@/utils/http' import { replaceImg } from '@/utils/libs' import { defineStore } from 'pinia' @@ -70,7 +70,7 @@ export const useSunoStore = defineStore('suno', () => { } const selectTag = (tag) => { if (data.tags.length + tag.value.length >= 119) { - showToastMessage('标签长度超出限制', 'error') + showMessageError('标签长度超出限制') return } const currentTags = data.tags.split(',').filter((t) => t.trim()) @@ -81,7 +81,7 @@ export const useSunoStore = defineStore('suno', () => { } const createLyric = () => { if (data.lyrics === '') { - showToastMessage('请输入歌词描述', 'error') + showMessageError('请输入歌词描述') return } isGenerating.value = true @@ -91,10 +91,10 @@ export const useSunoStore = defineStore('suno', () => { data.title = lines.shift().replace(/\*/g, '') lines.shift() data.lyrics = lines.join('\n') - showToastMessage('歌词生成成功', 'success') + showMessageOK('歌词生成成功') }) .catch((e) => { - showToastMessage('歌词生成失败:' + e.message, 'error') + showMessageError('歌词生成失败:' + e.message) }) .finally(() => { isGenerating.value = false @@ -109,7 +109,7 @@ export const useSunoStore = defineStore('suno', () => { const beforeUpload = (file) => { const isLt10M = file.size / 1024 / 1024 < 10 if (!isLt10M) { - showToastMessage('文件大小不能超过 10MB!', 'error') + showMessageError('文件大小不能超过 10MB!') return false } return true @@ -127,7 +127,7 @@ export const useSunoStore = defineStore('suno', () => { }) .then(() => { fetchData(1) - showToastMessage('歌曲上传成功', 'success') + showMessageOK('歌曲上传成功') removeRefSong() uploadFiles.value = [] if (uploadRef.value) { @@ -135,14 +135,14 @@ export const useSunoStore = defineStore('suno', () => { } }) .catch((e) => { - showToastMessage('歌曲上传失败:' + e.message, 'error') + showMessageError('歌曲上传失败:' + e.message) }) .finally(() => { closeLoading() }) }) .catch((e) => { - showToastMessage('文件上传失败:' + e.message, 'error') + showMessageError('文件上传失败:' + e.message) }) .finally(() => { closeLoading() @@ -155,21 +155,21 @@ export const useSunoStore = defineStore('suno', () => { data.extend_secs = refSong.value ? refSong.value.extend_secs : 0 if (refSong.value) { if (data.extend_secs > refSong.value.duration) { - showToastMessage('续写开始时间不能超过原歌曲长度', 'error') + showMessageError('续写开始时间不能超过原歌曲长度') return } } else if (custom.value) { if (data.lyrics === '') { - showToastMessage('请输入歌词', 'error') + showMessageError('请输入歌词') return } if (data.title === '') { - showToastMessage('请输入歌曲标题', 'error') + showMessageError('请输入歌曲标题') return } } else { if (data.prompt === '') { - showToastMessage('请输入歌曲描述', 'error') + showMessageError('请输入歌曲描述') return } } @@ -178,10 +178,10 @@ export const useSunoStore = defineStore('suno', () => { .then(() => { fetchData(1) taskPulling.value = true - showToastMessage('创建任务成功', 'success') + showMessageOK('创建任务成功') }) .catch((e) => { - showToastMessage('创建任务失败:' + e.message, 'error') + showMessageError('创建任务失败:' + e.message) }) .finally(() => { loading.value = false @@ -219,7 +219,7 @@ export const useSunoStore = defineStore('suno', () => { }) .catch((e) => { listLoading.value = false - showToastMessage('获取作品列表失败:' + e.message, 'error') + showMessageError('获取作品列表失败:' + e.message) }) } const loadMore = () => { @@ -279,7 +279,7 @@ export const useSunoStore = defineStore('suno', () => { item.downloading = false }) .catch(() => { - showToastMessage('下载失败', 'error') + showMessageError('下载失败') item.downloading = false }) .finally(() => { @@ -296,11 +296,11 @@ export const useSunoStore = defineStore('suno', () => { }).then(() => { httpGet('/api/suno/remove', { id: item.id }) .then(() => { - showToastMessage('任务删除成功', 'success') + showMessageOK('任务删除成功') fetchData(1) }) .catch(() => { - showToastMessage('任务删除失败', 'error') + showMessageError('任务删除失败') }) }) } diff --git a/web/src/utils/dialog.js b/web/src/utils/dialog.js index cba7c8df..a4bf3d95 100644 --- a/web/src/utils/dialog.js +++ b/web/src/utils/dialog.js @@ -33,6 +33,10 @@ export function showMessageOK(message) { } } +export function showMessageSuccess(message) { + showMessageOK(message) +} + export function showMessageInfo(message) { if (isMobile()) { showToast(message) @@ -56,24 +60,3 @@ export function showLoading(message = '正在处理...') { export function closeLoading() { closeToast() } - -// 自定义 Toast 消息系统 -export function showToastMessage(message, type = 'info', duration = 3000) { - const toast = document.createElement('div') - toast.className = `fixed top-20 left-1/2 transform -translate-x-1/2 z-50 px-4 py-2 rounded-lg text-white font-medium ${ - type === 'error' ? 'bg-red-500' : type === 'success' ? 'bg-green-500' : 'bg-blue-500' - } animate-fade-in` - toast.textContent = message - document.body.appendChild(toast) - - if (duration > 0) { - setTimeout(() => { - toast.classList.add('animate-fade-out') - setTimeout(() => { - if (document.body.contains(toast)) { - document.body.removeChild(toast) - } - }, 300) - }, duration) - } -} diff --git a/web/src/views/Index.vue b/web/src/views/Index.vue index 5870ec94..bfcf35e1 100644 --- a/web/src/views/Index.vue +++ b/web/src/views/Index.vue @@ -118,6 +118,7 @@ import MarkdownIt from 'markdown-it' import emoji from 'markdown-it-emoji' import { onMounted, ref } from 'vue' import { useRouter } from 'vue-router' +import { isMobile } from '@/utils/libs' const router = useRouter() @@ -146,6 +147,10 @@ const md = new MarkdownIt({ }).use(emoji) onMounted(() => { + if (isMobile()) { + router.push('/mobile/index') + return + } getSystemInfo() .then((res) => { title.value = res.data.title diff --git a/web/src/views/Jimeng.vue b/web/src/views/Jimeng.vue index f37f3fc9..59bb3543 100644 --- a/web/src/views/Jimeng.vue +++ b/web/src/views/Jimeng.vue @@ -138,7 +138,7 @@