调整即梦AI移动端功能

This commit is contained in:
GeekMaster
2025-08-08 18:01:42 +08:00
parent 8c03ecad2b
commit 604ce985bd
8 changed files with 424 additions and 401 deletions

View File

@@ -775,6 +775,107 @@
}
}
}
/* 快捷操作按钮样式 */
&__works-item-quick-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #f3f4f6;
}
&__works-item-quick-action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: #f9fafb;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
&:hover {
background: #e5e7eb;
color: #374151;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
&--danger {
color: #ef4444;
&:hover {
background: #fef2f2;
color: #dc2626;
}
}
i {
font-size: 16px;
}
}
/* 错误信息样式 */
&__works-item-error {
margin-top: 8px;
padding: 8px 12px;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
}
&__works-item-error-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
&__works-item-error-text {
flex: 1;
font-size: 12px;
color: #dc2626;
line-height: 1.4;
word-break: break-all;
}
&__works-item-error-copy-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
border-radius: 4px;
background: #fee2e2;
color: #dc2626;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
&:hover {
background: #fecaca;
color: #b91c1c;
}
i {
font-size: 12px;
}
}
}
/* 旋转动画 */

View File

@@ -1,8 +1,8 @@
import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog'
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http'
import { defineStore } from 'pinia'
import { showConfirmDialog } from 'vant'
import { computed, ref } from 'vue'
import { computed, reactive, ref, watch } from 'vue'
export const useJimengStore = defineStore('mobile-jimeng', () => {
// 响应式数据
@@ -22,6 +22,16 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
const taskPulling = ref(true)
const tastPullHandler = ref(null)
// 新增:算力配置
const powerConfig = ref({
text_to_image: 20,
image_to_image: 30,
image_edit: 25,
image_effects: 15,
text_to_video: 100,
image_to_video: 120,
})
// 功能分类
const categories = ref([
{ key: 'image_generation', name: '图像生成' },
@@ -115,35 +125,45 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
]
// 功能参数
const textToImageParams = ref({
size: '1024x1024',
scale: 7.5,
use_pre_llm: false,
// 各功能的参数
const textToImageParams = reactive({
size: '1328x1328',
scale: 2.5,
seed: -1,
use_pre_llm: true,
})
const imageToImageParams = ref({
image_input: [],
size: '1024x1024',
const imageToImageParams = reactive({
image_input: '',
size: '1328x1328',
gpen: 0.4,
skin: 0.3,
skin_unifi: 0,
gen_mode: 'creative',
seed: -1,
})
const imageEditParams = ref({
image_urls: [],
const imageEditParams = reactive({
image_urls: '',
scale: 0.5,
seed: -1,
})
const imageEffectsParams = ref({
image_input1: [],
const imageEffectsParams = reactive({
image_input1: '',
template_id: '',
size: '1024x1024',
size: '1328x1328',
})
const textToVideoParams = ref({
const textToVideoParams = reactive({
aspect_ratio: '16:9',
seed: -1,
})
const imageToVideoParams = ref({
const imageToVideoParams = reactive({
image_urls: [],
aspect_ratio: '16:9',
seed: -1,
})
// 计算属性
@@ -152,12 +172,29 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
return useImageInput.value ? 'image_to_image' : 'text_to_image'
} else if (activeCategory.value === 'image_editing') {
return 'image_edit'
} else if (activeCategory.value === 'image_effects') {
return 'image_effects'
} else if (activeCategory.value === 'video_generation') {
return useImageInput.value ? 'image_to_video' : 'text_to_video'
}
return 'text_to_image'
})
// 新增:动态计算当前算力消耗
const updateCurrentPowerCost = () => {
const functionKey = activeFunction.value
currentPowerCost.value = powerConfig.value[functionKey] || 10
}
// 监听任务类型变化,自动更新算力
watch(
[activeCategory, useImageInput],
() => {
updateCurrentPowerCost()
},
{ immediate: true }
)
// Actions
const getCategoryIcon = (category) => {
const iconMap = {
@@ -174,52 +211,17 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
useImageInput.value = false
}
const switchInputMode = () => {
currentPrompt.value = ''
}
const handleMultipleImageUpload = (event) => {
const files = Array.from(event.target.files)
files.forEach((file) => {
if (imageToVideoParams.value.image_urls.length < 2) {
onImageUpload({ file, name: file.name })
// 新增:获取算力配置
const fetchPowerConfig = async () => {
try {
const res = await httpGet('/api/jimeng/power-config')
if (res.data) {
powerConfig.value = res.data
updateCurrentPowerCost() // 更新当前算力消耗
}
})
}
const removeImage = (index) => {
imageToVideoParams.value.image_urls.splice(index, 1)
}
const onImageUpload = (file) => {
const formData = new FormData()
formData.append('file', file.file, file.name)
showLoading('正在上传图片...')
return httpPost('/api/upload', formData)
.then((res) => {
showMessageOK('图片上传成功')
const imageData = { url: res.data.url, content: res.data.url }
// 根据当前活动功能添加到相应的参数中
if (activeFunction.value === 'image_to_image') {
imageToImageParams.value.image_input = [imageData]
} else if (activeFunction.value === 'image_edit') {
imageEditParams.value.image_urls = [imageData]
} else if (activeFunction.value === 'image_effects') {
imageEffectsParams.value.image_input1 = [imageData]
} else if (activeFunction.value === 'image_to_video') {
imageToVideoParams.value.image_urls.push(imageData)
}
return res.data.url
})
.catch((e) => {
showMessageError('图片上传失败:' + e.message)
})
.finally(() => {
closeLoading()
})
} catch (error) {
console.error('获取算力配置失败:', error)
}
}
const submitTask = () => {
@@ -229,27 +231,62 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
}
submitting.value = true
const params = {
type: activeFunction.value,
prompt: currentPrompt.value,
}
let requestData = { task_type: activeFunction.value, prompt: currentPrompt.value }
// 根据功能类型添加相应参数
if (activeFunction.value === 'text_to_image') {
Object.assign(params, textToImageParams.value)
} else if (activeFunction.value === 'image_to_image') {
Object.assign(params, imageToImageParams.value)
} else if (activeFunction.value === 'image_edit') {
Object.assign(params, imageEditParams.value)
} else if (activeFunction.value === 'image_effects') {
Object.assign(params, imageEffectsParams.value)
} else if (activeFunction.value === 'text_to_video') {
Object.assign(params, textToVideoParams.value)
} else if (activeFunction.value === 'image_to_video') {
Object.assign(params, imageToVideoParams.value)
switch (activeFunction.value) {
case 'text_to_image':
Object.assign(requestData, {
width: parseInt(textToImageParams.size.split('x')[0]),
height: parseInt(textToImageParams.size.split('x')[1]),
scale: textToImageParams.scale,
seed: textToImageParams.seed,
use_pre_llm: textToImageParams.use_pre_llm,
})
break
case 'image_to_image':
Object.assign(requestData, {
image_input: imageToImageParams.image_input,
width: parseInt(imageToImageParams.size.split('x')[0]),
height: parseInt(imageToImageParams.size.split('x')[1]),
gpen: imageToImageParams.gpen,
skin: imageToImageParams.skin,
skin_unifi: imageToImageParams.skin_unifi,
gen_mode: imageToImageParams.gen_mode,
seed: imageToImageParams.seed,
})
break
case 'image_edit':
Object.assign(requestData, {
image_urls: [imageEditParams.image_urls],
scale: imageEditParams.scale,
seed: imageEditParams.seed,
})
break
case 'image_effects':
Object.assign(requestData, {
image_input: imageEffectsParams.image_input1,
template_id: imageEffectsParams.template_id,
width: parseInt(imageEffectsParams.size.split('x')[0]),
height: parseInt(imageEffectsParams.size.split('x')[1]),
prompt: imageEffectsParams.prompt,
})
break
case 'text_to_video':
Object.assign(requestData, {
aspect_ratio: textToVideoParams.aspect_ratio,
seed: textToVideoParams.seed,
})
break
case 'image_to_video':
Object.assign(requestData, {
image_urls: imageToVideoParams.image_urls,
aspect_ratio: imageToVideoParams.aspect_ratio,
seed: imageToVideoParams.seed,
})
break
}
return httpPost('/api/jimeng/create', params)
return httpPost('/api/jimeng/task', requestData)
.then(() => {
fetchData(1)
taskPulling.value = true
@@ -333,7 +370,7 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
})
}
const removeJob = (item) => {
const removeJob = async (item) => {
return showConfirmDialog({
title: '确认删除',
message: '此操作将会删除任务相关文件,继续操作吗?',
@@ -383,39 +420,94 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
}
}
const resetParams = () => {
textToImageParams.value = {
size: '1024x1024',
scale: 7.5,
use_pre_llm: false,
}
imageToImageParams.value = {
image_input: [],
size: '1024x1024',
}
imageEditParams.value = {
image_urls: [],
scale: 0.5,
}
imageEffectsParams.value = {
image_input1: [],
template_id: '',
size: '1024x1024',
}
textToVideoParams.value = {
aspect_ratio: '16:9',
}
imageToVideoParams.value = {
image_urls: [],
aspect_ratio: '16:9',
}
}
const closeMediaDialog = () => {
showMediaDialog.value = false
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
.writeText(prompt)
.then(() => {
showMessageOK('提示词已复制')
})
.catch(() => {
showMessageError('复制失败')
})
}
// 新增:复制错误信息功能
const copyErrorMsg = (msg) => {
navigator.clipboard
.writeText(msg)
.then(() => {
showMessageOK('错误信息已复制')
})
.catch(() => {
showMessageError('复制失败')
})
}
// 新增:初始化方法
const init = async () => {
await fetchPowerConfig()
}
return {
// State
activeCategory,
@@ -443,6 +535,7 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
imageEffectsParams,
textToVideoParams,
imageToVideoParams,
powerConfig,
// Computed
activeFunction,
@@ -450,10 +543,6 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
// Actions
getCategoryIcon,
switchCategory,
switchInputMode,
handleMultipleImageUpload,
removeImage,
onImageUpload,
submitTask,
fetchData,
loadMore,
@@ -465,7 +554,11 @@ export const useJimengStore = defineStore('mobile-jimeng', () => {
getTaskType,
startTaskPolling,
stopTaskPolling,
resetParams,
closeMediaDialog,
fetchPowerConfig,
drawSame,
copyPrompt,
copyErrorMsg,
init,
}
})

View File

@@ -1,10 +1,10 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
import { checkSession } from '@/store/cache'
import { getSystemInfo } from '@/store/cache'
import { closeLoading, showLoading, showToastMessage } from '@/utils/dialog'
import { httpDownload, httpGet, httpPost } from '@/utils/http'
import { replaceImg } from '@/utils/libs'
import { getSystemInfo } from '@/store/cache'
import { defineStore } from 'pinia'
import { showConfirmDialog } from 'vant'
import { reactive, ref } from 'vue'
export const useSunoStore = defineStore('suno', () => {
// 状态
@@ -35,7 +35,6 @@ export const useSunoStore = defineStore('suno', () => {
const uploadRef = ref(null)
const isGenerating = ref(false)
const deleting = ref(false)
const deleteItem = ref(null)
const models = ref([
{ label: 'v3.0', value: 'chirp-v3-0' },
{ label: 'v3.5', value: 'chirp-v3-5' },
@@ -287,10 +286,25 @@ export const useSunoStore = defineStore('suno', () => {
item.downloading = false
})
}
const showDeleteDialog = (item) => {
deleteItem.value = item
// 这里建议在页面层处理弹窗store 只负责数据和业务
const removeJob = (item) => {
showConfirmDialog({
title: '确认删除',
message: '此操作将会删除任务相关文件,继续操作吗?',
confirmButtonText: '确认删除',
cancelButtonText: '取消',
}).then(() => {
httpGet('/api/suno/remove', { id: item.id })
.then(() => {
showToastMessage('任务删除成功', 'success')
fetchData(1)
})
.catch(() => {
showToastMessage('任务删除失败', 'error')
})
})
}
const extend = (item) => {
refSong.value = item
refSong.value.extend_secs = item.duration
@@ -324,7 +338,6 @@ export const useSunoStore = defineStore('suno', () => {
uploadRef,
isGenerating,
deleting,
deleteItem,
models,
tags,
page,
@@ -346,7 +359,7 @@ export const useSunoStore = defineStore('suno', () => {
refreshFirstPage,
play,
download,
showDeleteDialog,
removeJob,
extend,
removeRefSong,
}

View File

@@ -49,11 +49,7 @@
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="flex justify-between items-center w-full">
<span class="text-gray-700 font-semibold">图生图人像写真</span>
<el-switch
v-model="jimengStore.useImageInput"
@change="jimengStore.switchInputMode"
size="default"
/>
<el-switch v-model="jimengStore.useImageInput" size="default" />
</div>
</div>
@@ -284,9 +280,9 @@
<el-image
v-if="item.img_url"
:src="item.img_url"
:preview-src-list="[item.img_url]"
fit="cover"
class="w-full h-full"
:preview-disabled="true"
>
<template #error>
<div class="jimeng-create__works-item-thumb-placeholder">
@@ -294,19 +290,6 @@
</div>
</template>
</el-image>
<el-image
v-else-if="item.video_url"
:src="item.video_url"
fit="cover"
class="w-full h-full"
:preview-disabled="true"
>
<template #error>
<div class="jimeng-create__works-item-thumb-placeholder">
<i class="iconfont icon-video"></i>
</div>
</template>
</el-image>
<div v-else class="jimeng-create__works-item-thumb-placeholder">
<i
:class="
@@ -326,13 +309,7 @@
"
></i>
</button>
<!-- 进度动画 -->
<div
v-if="item.status === 'in_queue' || item.status === 'generating'"
class="jimeng-create__works-item-thumb-status jimeng-create__works-item-thumb-status--loading"
>
<i class="iconfont icon-loading animate-spin"></i>
</div>
<!-- 失败状态 -->
<div
v-if="item.status === 'failed'"
@@ -392,45 +369,70 @@
</div>
</div>
<!-- 操作按钮 -->
<div class="jimeng-create__works-item-actions">
<div class="jimeng-create__works-item-actions-left">
<!-- 快捷操作按钮 -->
<div class="jimeng-create__works-item-quick-actions">
<!-- 复制提示词 -->
<button
v-if="item.prompt"
@click="jimengStore.copyPrompt(item.prompt)"
class="jimeng-create__works-item-quick-action-btn"
title="复制提示词"
>
<i class="iconfont icon-copy"></i>
</button>
<span v-if="item.status === 'success'">
<!-- 画同款 -->
<button
v-if="item.status === 'completed'"
@click="jimengStore.playMedia(item)"
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--primary"
@click="jimengStore.drawSame(item)"
class="jimeng-create__works-item-quick-action-btn"
title="画同款"
>
<i
:class="item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'"
></i>
<span>{{ item.type.includes('video') ? '播放' : '查看' }}</span>
<i class="iconfont icon-image-list"></i>
</button>
<!-- 下载 -->
<button
v-if="item.status === 'completed'"
v-if="item.status === 'completed' && (item.img_url || item.video_url)"
@click="jimengStore.downloadFile(item)"
:disabled="item.downloading"
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--success"
class="jimeng-create__works-item-quick-action-btn"
title="下载"
>
<i v-if="item.downloading" class="iconfont icon-loading animate-spin"></i>
<i v-else class="iconfont icon-download"></i>
<span>{{ item.downloading ? '下载中...' : '下载' }}</span>
</button>
<i v-else class="iconfont icon-download"></i></button
></span>
<!-- 重试 -->
<button
v-if="item.status === 'failed'"
@click="jimengStore.retryTask(item.id)"
class="jimeng-create__works-item-quick-action-btn"
title="重试"
>
<i class="iconfont icon-refresh"></i>
</button>
<!-- 删除 -->
<button @click="jimengStore.removeJob(item)" class="p-2">
<i class="iconfont icon-remove"></i> 删除
</button>
</div>
<!-- 错误信息复制 -->
<div
v-if="item.status === 'failed' && item.err_msg"
class="jimeng-create__works-item-error"
>
<div class="jimeng-create__works-item-error-content">
<span class="jimeng-create__works-item-error-text">{{ item.err_msg }}</span>
<button
v-if="item.status === 'failed'"
@click="jimengStore.retryTask(item.id)"
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--warning"
@click="jimengStore.copyErrorMsg(item.err_msg)"
class="jimeng-create__works-item-error-copy-btn"
title="复制错误信息"
>
<i class="iconfont icon-refresh"></i>
<span>重试</span>
<i class="iconfont icon-copy"></i>
</button>
</div>
<button
@click="jimengStore.removeJob(item)"
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--danger"
>
<i class="iconfont icon-remove"></i>
<span>删除</span>
</button>
</div>
</div>
@@ -515,6 +517,7 @@ const handleTemplateChange = (value) => {
onMounted(() => {
checkSession()
.then(() => {
jimengStore.init() // 初始化算力配置
jimengStore.fetchData(1)
jimengStore.startTaskPolling()
})

View File

@@ -388,7 +388,7 @@
</button>
</div>
<button
@click="showDeleteDialog(item)"
@click="suno.removeJob(item)"
class="px-3 py-1.5 bg-red-100 text-red-600 text-sm rounded-lg hover:bg-red-200 transition-colors flex items-center space-x-1"
>
<i class="iconfont icon-remove !text-xs"></i>
@@ -494,7 +494,6 @@
import '@/assets/css/mobile/suno.scss'
import CustomSelect from '@/components/mobile/CustomSelect.vue'
import { useSunoStore } from '@/store/mobile/suno'
import { showConfirmDialog } from 'vant'
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
@@ -536,28 +535,6 @@ onUnmounted(() => {
if (tastPullHandler) clearInterval(tastPullHandler)
window.removeEventListener('scroll', handleScroll)
})
// 删除弹窗(页面层处理)
const showDeleteDialog = (item) => {
suno.deleteItem = item
showConfirmDialog({
title: '确认删除',
message: '此操作将会删除任务相关文件,继续操作吗?',
confirmButtonText: '确认删除',
cancelButtonText: '取消',
})
.then(() => {
if (!suno.deleteItem) return
suno.deleting = true
suno.deleteItem && suno.deleteItem.id && suno.$patch({ deleting: true })
suno.deleteItem && suno.deleteItem.id && suno.$patch({ deleting: false })
suno.deleteItem = null
suno.fetchData(1)
})
.catch(() => {
suno.deleteItem = null
})
}
</script>
<style lang="scss" scoped>