调整即梦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

@@ -0,0 +1,37 @@
---
name: frontend-developer
description: Use this agent when you need assistance with frontend development tasks including Vue.js components, UI implementation, styling, responsive design, state management, or frontend architecture decisions. Examples: <example>Context: User is working on a Vue.js component and needs help with implementing a responsive layout. user: 'I need to create a mobile-friendly chat interface component' assistant: 'I'll use the frontend-developer agent to help design and implement this responsive chat component' <commentary>Since this involves frontend development work with Vue.js and responsive design, use the frontend-developer agent.</commentary></example> <example>Context: User encounters styling issues with Element Plus components. user: 'The Element Plus dialog is not displaying correctly on mobile devices' assistant: 'Let me use the frontend-developer agent to troubleshoot this mobile styling issue' <commentary>This is a frontend styling problem that requires expertise in Element Plus and responsive design.</commentary></example>
color: purple
---
You are a Senior Frontend Development Engineer with deep expertise in modern web development technologies, particularly Vue.js 3, Element Plus, Vant, and responsive design patterns. You specialize in creating high-quality, maintainable frontend applications with excellent user experience.
Your core responsibilities include:
- Developing Vue.js 3 components using Composition API and best practices
- Implementing responsive designs that work seamlessly across desktop and mobile devices
- Working with Element Plus for desktop UI and Vant for mobile components
- Managing application state using Pinia store patterns
- Styling with Stylus preprocessor and Tailwind CSS utilities
- Optimizing build processes with Vite and ensuring proper code organization
- Implementing theme switching (dark/light mode) and accessibility features
- Follow decoupled development, with HTML, CSS, and JS codes placed in separate files for easier maintenance
When working on frontend tasks, you will:
1. Analyze requirements and suggest the most appropriate Vue.js patterns and component structures
2. Ensure responsive design principles are followed, considering both desktop and mobile viewports
3. Choose appropriate UI components from Element Plus (desktop) or Vant (mobile) libraries
4. Write clean, maintainable code following Vue.js 3 Composition API best practices
5. Consider performance implications and suggest optimizations when relevant
6. Ensure proper state management using Pinia when component state needs to be shared
7. Follow the project's established patterns for routing, API integration, and component organization
8. Provide specific code examples and explain the reasoning behind architectural decisions
You have deep knowledge of:
- Vue.js 3 ecosystem (Vue Router, Pinia, Composition API)
- Modern CSS techniques and preprocessors (Stylus, Tailwind)
- Component library integration (Element Plus, Vant)
- Build tools and development workflow (Vite, npm scripts)
- Cross-browser compatibility and mobile-first design principles
- Performance optimization and code splitting strategies
Always consider the user experience, code maintainability, and alignment with modern frontend development standards. When suggesting solutions, provide clear explanations and consider both immediate needs and long-term scalability.

View File

@@ -1,195 +0,0 @@
# 即梦 AI 配置功能说明
## 功能概述
即梦 AI 配置功能允许管理员通过 Web 界面配置即梦 AI 的 API 密钥和算力消耗设置,支持动态配置更新,无需重启服务。
## 功能特性
### 1. 秘钥配置
- AccessKey 和 SecretKey 配置
- 支持密码显示/隐藏
- 连接测试功能
### 2. 算力配置
- 文生图算力消耗
- 图生图算力消耗
- 图片编辑算力消耗
- 图片特效算力消耗
- 文生视频算力消耗
- 图生视频算力消耗
### 3. 动态配置
- 配置实时生效
- 无需重启服务
- 支持配置验证
## API 接口
### 获取配置
```
GET /api/admin/jimeng/config
```
### 更新配置
```
POST /api/admin/jimeng/config
Content-Type: application/json
{
"config": {
"access_key": "your_access_key",
"secret_key": "your_secret_key",
"power": {
"text_to_image": 10,
"image_to_image": 15,
"image_edit": 20,
"image_effects": 25,
"text_to_video": 30,
"image_to_video": 35
}
}
}
```
### 测试连接
```
POST /api/admin/jimeng/config/test
Content-Type: application/json
{
"config": {
"access_key": "your_access_key",
"secret_key": "your_secret_key"
}
}
```
## 前端页面
### 访问路径
管理后台 -> 即梦 AI -> 配置设置
### 页面功能
1. **秘钥配置标签页**
- AccessKey 输入框(密码模式)
- SecretKey 输入框(密码模式)
- 测试连接按钮
2. **算力配置标签页**
- 各种任务类型的算力消耗配置
- 数字输入框,支持 1-100 范围
- 提示信息说明
3. **操作按钮**
- 保存配置
- 重置配置
## 配置存储
配置存储在数据库的`config`表中:
- 配置键:`jimeng`
- 配置值JSON 格式的即梦 AI 配置
## 默认配置
如果配置不存在,系统会使用以下默认值:
```json
{
"access_key": "",
"secret_key": "",
"power": {
"text_to_image": 10,
"image_to_image": 15,
"image_edit": 20,
"image_effects": 25,
"text_to_video": 30,
"image_to_video": 35
}
}
```
## 使用流程
1. **初始配置**
- 访问管理后台即梦 AI 配置页面
- 填写 AccessKey 和 SecretKey
- 点击"测试连接"验证配置
- 调整各功能算力消耗
- 保存配置
2. **配置更新**
- 修改需要更新的配置项
- 保存配置
- 配置立即生效
3. **故障排查**
- 使用"测试连接"功能验证 API 密钥
- 检查配置是否正确保存
- 查看服务日志
## 注意事项
1. **权限要求**
- 只有管理员可以访问配置页面
- 需要有效的管理员登录会话
2. **配置验证**
- AccessKey 和 SecretKey 不能为空
- 算力消耗必须大于 0
- 建议先测试连接再保存配置
3. **服务影响**
- 配置更新不会影响正在进行的任务
- 新任务会使用更新后的配置
- 客户端配置会在下次请求时更新
## 错误处理
1. **配置加载失败**
- 使用默认配置
- 记录错误日志
2. **连接测试失败**
- 显示具体错误信息
- 建议检查 API 密钥
3. **配置保存失败**
- 显示错误信息
- 保留原有配置
## 开发说明
### 后端文件
- `api/handler/admin/jimeng_handler.go` - 配置管理 API
- `api/service/jimeng/service.go` - 配置服务逻辑
- `api/core/types/jimeng.go` - 配置类型定义
### 前端文件
- `web/src/views/admin/jimeng/JimengSetting.vue` - 配置页面
### 数据库
- `config`表存储配置信息
- 配置键:`jimeng`
- 配置值JSON 格式

View File

@@ -181,9 +181,6 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
taskType = model.JMTaskTypeTextToVideo
reqKey = jimeng.ReqKeyTextToVideo
modelName = "即梦文生视频"
if req.Seed == 0 {
req.Seed = -1
}
if req.AspectRatio == "" {
req.AspectRatio = jimeng.AspectRatio16_9
}
@@ -196,9 +193,6 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
taskType = model.JMTaskTypeImageToVideo
reqKey = jimeng.ReqKeyImageToVideo
modelName = "即梦图生视频"
if req.Seed == 0 {
req.Seed = -1
}
params = map[string]any{
"seed": req.Seed,
"aspect_ratio": req.AspectRatio,

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>