多图生成图片

This commit is contained in:
ZLI263
2025-10-17 21:28:59 +08:00
parent 84da5e60ed
commit c809e383fa
5 changed files with 848 additions and 127 deletions

View File

@@ -5,7 +5,7 @@ export function getImageList(params) {
return request({
url: '/image-model/page',
method: 'post',
params: params // 使用params而不是data,这样参数会作为URL查询参数传递
params: params // 使用params传递URL查询参数,因为后端使用@RequestParam
})
}
@@ -79,6 +79,16 @@ export function imageProcessing(data) {
return request({
url: '/image-model/image-background',
method: 'post',
data
data,
timeout: 60000 // 设置60秒超时,图像处理可能需要较长时间
})
}
// 检查任务状态
export function checkImageModelStatus() {
return request({
url: '/image-model/check-status',
method: 'post',
timeout: 30000 // 设置30秒超时
})
}

75
src/image-management.js Normal file
View File

@@ -0,0 +1,75 @@
import request from '@/utils/request'
// 获取图像管理列表
export function getImageList(params) {
return request({
url: '/image-model/page',
method: 'post',
params: params // 使用params而不是data,这样参数会作为URL查询参数传递
})
}
// 获取图像详情
export function getImageDetail(id) {
return request({
url: `/api/image-management/${id}`,
method: 'get'
})
}
// 创建图像
export function createImage(data) {
return request({
url: '/api/image-management',
method: 'post',
data
})
}
// 更新图像
export function updateImage(id, data) {
return request({
url: `/api/image-management/${id}`,
method: 'put',
data
})
}
// 删除图像
export function deleteImage(uuid) {
return request({
url: `/image-model/delete/${uuid}`,
method: 'delete'
})
}
// 批量删除图像
export function batchDeleteImages(ids) {
return request({
url: '/api/image-management/batch-delete',
method: 'post',
data: { ids }
})
}
// 上传素材
export function uploadMaterial(formData, params = {}) {
return request({
url: '/image-model/upload',
method: 'post',
data: formData,
params: params, // 添加查询参数
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 去除水印
export function removeWatermark(data) {
return request({
url: '/image-model/removeWatermar',
method: 'post',
data
})
}

View File

@@ -14,6 +14,12 @@ const service = axios.create({
service.interceptors.request.use(
config => {
// do something before request is sent
console.log('请求配置:', {
url: config.url,
method: config.method,
data: config.data,
params: config.params
})
if (store.getters.token) {
// 使用Bearer token格式

View File

@@ -6,7 +6,7 @@
<!-- 主图选择区域 -->
<div class="form-section">
<div class="section-header">
<h3>主图选择</h3>
<h3>主图选择 <span class="required-tag">(必选)</span></h3>
</div>
<div class="section-content">
<div class="two-column-layout">
@@ -18,6 +18,9 @@
placeholder="请选择主图"
class="image-select"
:loading="imageListLoading"
filterable
remote
:remote-method="handleBaseImageSearch"
@change="handleBaseImageChange"
>
<el-option
@@ -32,17 +35,17 @@
<!-- 右列:图片预览 -->
<div class="right-column">
<div v-if="selectedImage" class="image-preview">
<div v-if="selectedBaseImage" class="image-preview">
<img
:src="selectedImage.materialTempUrl"
:alt="selectedImage.imageName"
:src="selectedBaseImage.materialTempUrl"
:alt="selectedBaseImage.imageName"
class="preview-image"
@error="handleImageError"
>
<div class="image-details">
<p><strong>{{ selectedImage.imageName }}</strong></p>
<p>{{ selectedImage.ownerName }}</p>
<p>{{ getImageTypeText(selectedImage.imageType) }}</p>
<p><strong>{{ selectedBaseImage.imageName }}</strong></p>
<p>{{ selectedBaseImage.ownerName }}</p>
<p>{{ getImageTypeText(selectedBaseImage.imageType) }}</p>
</div>
</div>
</div>
@@ -53,7 +56,7 @@
<!-- 参考图选择区域 -->
<div class="form-section">
<div class="section-header">
<h3>参考图选择</h3>
<h3>参考图选择 <span class="required-tag">(必选)</span></h3>
</div>
<div class="section-content">
<div class="two-column-layout">
@@ -65,6 +68,9 @@
placeholder="请选择参考图"
class="image-select"
:loading="imageListLoading"
filterable
remote
:remote-method="handleRefImageSearch"
@change="handleRefImageChange"
>
<el-option
@@ -109,7 +115,11 @@
<!-- 前景边缘图1选择区域 -->
<div class="form-section">
<div class="section-header">
<h3>前景边缘图1选择</h3>
<h3>前景边缘图1选择 <span class="optional-tag">(可选)</span></h3>
<div class="format-tip">
<i class="el-icon-info" />
<span>请选择支持透明通道的图片(PNG、GIF、WebP格式),此字段为可选项</span>
</div>
</div>
<div class="section-content">
<div class="two-column-layout">
@@ -121,6 +131,9 @@
placeholder="请选择前景边缘图1"
class="image-select"
:loading="imageListLoading"
filterable
remote
:remote-method="handleForegroundEdge1Search"
@change="handleForegroundEdge1Change"
>
<el-option
@@ -165,7 +178,11 @@
<!-- 前景边缘图2选择区域 -->
<div class="form-section">
<div class="section-header">
<h3>前景边缘图2选择</h3>
<h3>前景边缘图2选择 <span class="optional-tag">(可选)</span></h3>
<div class="format-tip">
<i class="el-icon-info" />
<span>请选择支持透明通道的图片(PNG、GIF、WebP格式),此字段为可选项</span>
</div>
</div>
<div class="section-content">
<div class="two-column-layout">
@@ -177,6 +194,9 @@
placeholder="请选择前景边缘图2"
class="image-select"
:loading="imageListLoading"
filterable
remote
:remote-method="handleForegroundEdge2Search"
@change="handleForegroundEdge2Change"
>
<el-option
@@ -221,7 +241,11 @@
<!-- 背景边缘图选择区域 -->
<div class="form-section">
<div class="section-header">
<h3>背景边缘图选择</h3>
<h3>背景边缘图选择 <span class="optional-tag">(可选)</span></h3>
<div class="format-tip">
<i class="el-icon-info" />
<span>此字段为可选项,可以选择也可以不选择</span>
</div>
</div>
<div class="section-content">
<div class="two-column-layout">
@@ -233,6 +257,9 @@
placeholder="请选择背景边缘图"
class="image-select"
:loading="imageListLoading"
filterable
remote
:remote-method="handleBackgroundEdgeSearch"
@change="handleBackgroundEdgeChange"
>
<el-option
@@ -314,6 +341,14 @@
</div>
</div>
<div class="parameter-row">
<div class="parameter-item">
<label class="parameter-label">图片名称</label>
<el-input
v-model="form.imageName"
placeholder="请输入图片名称"
class="parameter-input"
/>
</div>
<div class="parameter-item">
<label class="parameter-label">所有人姓名</label>
<el-input
@@ -378,6 +413,8 @@ export default {
n: 1,
refPromptWeight: 0.5,
modelVersion: 'v3',
// 图片信息
imageName: '',
// 所有人信息
ownerName: '',
ownerPhone: ''
@@ -389,7 +426,23 @@ export default {
selectedRefImage: null,
selectedForegroundEdge1: null,
selectedForegroundEdge2: null,
selectedBackgroundEdge: null
selectedBackgroundEdge: null,
// 搜索关键词状态
searchKeywords: {
baseImage: '',
refImage: '',
foregroundEdge1: '',
foregroundEdge2: '',
backgroundEdge: ''
},
// 防抖定时器
searchTimeouts: {
baseImage: null,
refImage: null,
foregroundEdge1: null,
foregroundEdge2: null,
backgroundEdge: null
}
}
},
computed: {
@@ -411,13 +464,26 @@ export default {
},
// 加载图像列表
loadImageList() {
loadImageList(searchKeyword = '') {
this.imageListLoading = true
const params = {
current: 1,
size: 20
size: 30,
imageType: 'material' // 只显示素材类型的图片
}
// 如果有搜索关键词,添加到参数中
if (searchKeyword && searchKeyword.trim()) {
params.imageName = searchKeyword.trim()
}
console.log('发送到后端的搜索参数:', params)
console.log('参数详情:', {
imageType: params.imageType,
imageName: params.imageName,
current: params.current,
size: params.size
})
getImageList(params).then(response => {
console.log('图像列表响应:', response)
console.log('响应数据结构:', {
@@ -454,6 +520,22 @@ export default {
})
},
// 主图搜索方法
handleBaseImageSearch(query) {
console.log('主图搜索关键词:', query)
this.searchKeywords.baseImage = query
// 清除之前的定时器
if (this.searchTimeouts.baseImage) {
clearTimeout(this.searchTimeouts.baseImage)
}
// 设置防抖,500ms后执行搜索
this.searchTimeouts.baseImage = setTimeout(() => {
this.loadImageList(query)
}, 500)
},
// 处理主图选择变化
handleBaseImageChange(imageId) {
console.log('选择的主图ID:', imageId)
@@ -465,6 +547,22 @@ export default {
}
},
// 参考图搜索方法
handleRefImageSearch(query) {
console.log('参考图搜索关键词:', query)
this.searchKeywords.refImage = query
// 清除之前的定时器
if (this.searchTimeouts.refImage) {
clearTimeout(this.searchTimeouts.refImage)
}
// 设置防抖,500ms后执行搜索
this.searchTimeouts.refImage = setTimeout(() => {
this.loadImageList(query)
}, 500)
},
// 处理参考图选择变化
handleRefImageChange(refImageId) {
console.log('选择的参考图ID:', refImageId)
@@ -476,6 +574,22 @@ export default {
}
},
// 前景边缘图1搜索方法
handleForegroundEdge1Search(query) {
console.log('前景边缘图1搜索关键词:', query)
this.searchKeywords.foregroundEdge1 = query
// 清除之前的定时器
if (this.searchTimeouts.foregroundEdge1) {
clearTimeout(this.searchTimeouts.foregroundEdge1)
}
// 设置防抖,500ms后执行搜索
this.searchTimeouts.foregroundEdge1 = setTimeout(() => {
this.loadImageList(query)
}, 500)
},
// 处理前景边缘图1选择变化
handleForegroundEdge1Change(edge1Id) {
console.log('选择的前景边缘图1 ID:', edge1Id)
@@ -487,6 +601,22 @@ export default {
}
},
// 前景边缘图2搜索方法
handleForegroundEdge2Search(query) {
console.log('前景边缘图2搜索关键词:', query)
this.searchKeywords.foregroundEdge2 = query
// 清除之前的定时器
if (this.searchTimeouts.foregroundEdge2) {
clearTimeout(this.searchTimeouts.foregroundEdge2)
}
// 设置防抖,500ms后执行搜索
this.searchTimeouts.foregroundEdge2 = setTimeout(() => {
this.loadImageList(query)
}, 500)
},
// 处理前景边缘图2选择变化
handleForegroundEdge2Change(edge2Id) {
console.log('选择的前景边缘图2 ID:', edge2Id)
@@ -498,6 +628,22 @@ export default {
}
},
// 背景边缘图搜索方法
handleBackgroundEdgeSearch(query) {
console.log('背景边缘图搜索关键词:', query)
this.searchKeywords.backgroundEdge = query
// 清除之前的定时器
if (this.searchTimeouts.backgroundEdge) {
clearTimeout(this.searchTimeouts.backgroundEdge)
}
// 设置防抖,500ms后执行搜索
this.searchTimeouts.backgroundEdge = setTimeout(() => {
this.loadImageList(query)
}, 500)
},
// 处理背景边缘图选择变化
handleBackgroundEdgeChange(backgroundEdgeId) {
console.log('选择的背景边缘图ID:', backgroundEdgeId)
@@ -522,14 +668,66 @@ export default {
person: '人物图',
product: '产品图',
material: '素材图',
result: '成品',
other: '其他'
}
return typeMap[type] || '未知'
},
// 验证图片格式
validateImageFormat(image, imageName) {
// 检查图片URL是否包含透明通道信息
const url = image.materialTempUrl
if (!url) {
return { valid: false, message: `${imageName}图片URL无效` }
}
console.log(`验证${imageName}格式:`, {
url: url,
imageName: image.imageName,
imageType: image.imageType
})
// 检查文件扩展名 - 需要先去除URL参数
const urlWithoutParams = url.split('?')[0] // 去除问号后的签名信息
const extension = urlWithoutParams.split('.').pop().toLowerCase()
const supportedFormats = ['png', 'gif', 'webp', 'jpg', 'jpeg']
console.log(`原始URL:${url}`)
console.log(`去除参数后的URL:${urlWithoutParams}`)
console.log(`文件扩展名:${extension}`)
// 对于前景边缘图,我们放宽格式要求,因为后端会处理格式转换
// 主要检查是否是有效的图片格式
if (!supportedFormats.includes(extension)) {
return {
valid: false,
message: `${imageName}图片格式不支持,请使用常见的图片格式(PNG、JPG、GIF、WebP等)`
}
}
// 如果是前景边缘图,给出建议但不强制要求特定格式
if (imageName.includes('前景边缘图')) {
console.log(`前景边缘图格式检查通过:${extension}`)
// 如果已经是PNG格式,不需要显示建议
if (extension === 'png') {
return { valid: true }
}
// 对于其他格式,给出建议但不阻止使用
return {
valid: true,
message: `建议使用PNG格式以获得更好的透明通道支持,当前格式:${extension.toUpperCase()}`
}
}
return { valid: true }
},
// 处理生成按钮点击
handleGenerate() {
// 验证必填项
// 验证必填项 - 只有主图和参考图是必选的
if (!this.form.baseImageId) {
this.$message.warning('请选择主图')
return
@@ -538,17 +736,44 @@ export default {
this.$message.warning('请选择参考图')
return
}
if (!this.form.foregroundEdge1Id) {
this.$message.warning('请选择前景边缘图1')
if (!this.form.imageName) {
this.$message.warning('请输入图片名称')
return
}
if (!this.form.foregroundEdge2Id) {
this.$message.warning('请选择前景边缘图2')
return
// 其他图片都是可选的,不进行必填验证
// 验证已选择的图片格式(仅检查基本格式,不强制要求特定格式)
// 前景边缘图1 - 可选
if (this.form.foregroundEdge1Id && this.selectedForegroundEdge1) {
const foregroundEdge1Validation = this.validateImageFormat(this.selectedForegroundEdge1, '前景边缘图1')
if (!foregroundEdge1Validation.valid) {
this.$message.error(foregroundEdge1Validation.message)
return
} else if (foregroundEdge1Validation.message) {
console.log('前景边缘图1格式建议:', foregroundEdge1Validation.message)
}
}
if (!this.form.backgroundEdgeId) {
this.$message.warning('请选择背景边缘图')
return
// 前景边缘图2 - 可选
if (this.form.foregroundEdge2Id && this.selectedForegroundEdge2) {
const foregroundEdge2Validation = this.validateImageFormat(this.selectedForegroundEdge2, '前景边缘图2')
if (!foregroundEdge2Validation.valid) {
this.$message.error(foregroundEdge2Validation.message)
return
} else if (foregroundEdge2Validation.message) {
console.log('前景边缘图2格式建议:', foregroundEdge2Validation.message)
}
}
// 背景边缘图 - 可选
if (this.form.backgroundEdgeId && this.selectedBackgroundEdge) {
const backgroundEdgeValidation = this.validateImageFormat(this.selectedBackgroundEdge, '背景边缘图')
if (!backgroundEdgeValidation.valid) {
this.$message.error(backgroundEdgeValidation.message)
return
} else if (backgroundEdgeValidation.message) {
console.log('背景边缘图格式建议:', backgroundEdgeValidation.message)
}
}
// 开始生成
@@ -556,36 +781,61 @@ export default {
// 构建后端参数结构
const requestData = {
base_image_url: this.selectedBaseImage.materialTempUrl,
ref_image_url: this.selectedRefImage.materialTempUrl,
ref_prompt: this.form.refPrompt,
reference_edge: {
foreground_edge: [
this.selectedForegroundEdge1.materialTempUrl,
this.selectedForegroundEdge2.materialTempUrl
baseImageUrl: this.selectedBaseImage.materialTempUrl,
refImageUrl: this.selectedRefImage.materialTempUrl,
refPrompt: this.form.refPrompt,
imageName: this.form.imageName,
ownerName: this.form.ownerName,
ownerPhone: this.form.ownerPhone,
referenceEdge: {
foregroundEdge: [
// 前景边缘图1 - 可选
...(this.selectedForegroundEdge1 ? [this.selectedForegroundEdge1.materialTempUrl] : []),
// 前景边缘图2 - 可选
...(this.selectedForegroundEdge2 ? [this.selectedForegroundEdge2.materialTempUrl] : [])
],
background_edge: [
this.selectedBackgroundEdge.materialTempUrl
backgroundEdge: [
// 背景边缘图 - 可选
...(this.selectedBackgroundEdge ? [this.selectedBackgroundEdge.materialTempUrl] : [])
],
foreground_edge_prompt: [
this.form.foregroundEdge1Prompt,
this.form.foregroundEdge2Prompt
foregroundEdgePrompt: [
// 前景边缘图1提示词 - 可选,确保与foregroundEdge数组长度匹配
...(this.selectedForegroundEdge1 ? [this.form.foregroundEdge1Prompt || ''] : []),
// 前景边缘图2提示词 - 可选,确保与foregroundEdge数组长度匹配
...(this.selectedForegroundEdge2 ? [this.form.foregroundEdge2Prompt || ''] : [])
],
background_edge_prompt: [
this.form.backgroundEdgePrompt
backgroundEdgePrompt: [
// 背景边缘图提示词 - 可选
...(this.selectedBackgroundEdge ? [this.form.backgroundEdgePrompt || ''] : [])
]
},
parameters: {
n: this.form.n,
ref_prompt_weight: this.form.refPromptWeight,
model_version: this.form.modelVersion
},
owner_info: {
owner_name: this.form.ownerName,
owner_phone: this.form.ownerPhone
refPromptWeight: this.form.refPromptWeight,
modelVersion: this.form.modelVersion
}
}
// 验证数组长度匹配
const foregroundEdgeLength = requestData.referenceEdge.foregroundEdge.length
const foregroundEdgePromptLength = requestData.referenceEdge.foregroundEdgePrompt.length
console.log('数组长度验证:', {
foregroundEdge: foregroundEdgeLength,
foregroundEdgePrompt: foregroundEdgePromptLength,
isMatched: foregroundEdgeLength === foregroundEdgePromptLength
})
if (foregroundEdgeLength !== foregroundEdgePromptLength) {
console.error('数组长度不匹配!', {
foregroundEdge: requestData.referenceEdge.foregroundEdge,
foregroundEdgePrompt: requestData.referenceEdge.foregroundEdgePrompt
})
this.$message.error('前景边缘图和提示词数量不匹配,请检查输入')
this.generateLoading = false
return
}
console.log('开始生成图像,参数:', requestData)
// 调用后端接口
@@ -610,13 +860,38 @@ export default {
console.log('请求ID:', response.requestId)
}
} else {
this.$message.error(response.message || '图像处理失败')
// 处理特定的错误代码
let errorMessage = response.message || '图像处理失败'
if (response.code === 'BadRequest.UnsupportedFileFormat') {
errorMessage = '图片格式不支持:前景边缘图需要RGBA或LA模式的图片(支持透明通道的PNG、GIF或WebP格式)'
} else if (response.code === 'BadRequest.InvalidImageFormat') {
errorMessage = '图片格式无效,请检查图片文件是否损坏'
} else if (response.code === 'BadRequest.ImageSizeExceeded') {
errorMessage = '图片尺寸过大,请使用较小的图片'
}
this.$message.error(errorMessage)
console.error('图像处理失败:', response)
}
}).catch(error => {
this.generateLoading = false
console.error('调用图像处理接口失败:', error)
this.$message.error('调用图像处理接口失败:' + (error.message || '未知错误'))
// 处理网络错误或服务器错误
let errorMessage = '调用图像处理接口失败'
if (error.response && error.response.data) {
const errorData = error.response.data
if (errorData.code === 'BadRequest.UnsupportedFileFormat') {
errorMessage = '图片格式不支持:前景边缘图需要RGBA或LA模式的图片(支持透明通道的PNG、GIF或WebP格式)'
} else if (errorData.message) {
errorMessage = errorData.message
}
} else if (error.message) {
errorMessage = error.message
}
this.$message.error(errorMessage)
})
}
}
@@ -627,7 +902,7 @@ export default {
.app-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
padding: 10px;
}
.content {
@@ -639,33 +914,67 @@ export default {
}
.form-section {
margin-bottom: 30px;
margin-bottom: 20px;
border: 1px solid #e4e7ed;
border-radius: 12px;
border-radius: 8px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.section-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 20px;
color: white;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 12px 16px;
color: #495057;
border-bottom: 1px solid #e4e7ed;
}
.section-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
font-size: 14px;
font-weight: 500;
}
.format-tip {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.format-tip i {
color: #409eff;
}
.optional-tag {
font-size: 12px;
color: #909399;
font-weight: normal;
background: #f0f2f5;
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
}
.required-tag {
font-size: 12px;
color: #f56c6c;
font-weight: normal;
background: #fef0f0;
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
}
.section-content {
padding: 20px;
padding: 16px;
}
.two-column-layout {
display: flex;
gap: 30px;
gap: 20px;
align-items: flex-start;
}
@@ -680,7 +989,7 @@ export default {
}
.selector-wrapper {
margin-bottom: 15px;
margin-bottom: 12px;
}
.image-select {
@@ -690,23 +999,23 @@ export default {
.image-preview {
display: flex;
align-items: center;
gap: 12px;
padding: 15px;
gap: 10px;
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 8px;
border-radius: 6px;
background: #f8f9fa;
width: 100%;
height: 120px;
height: 100px;
box-sizing: border-box;
}
.preview-image {
width: 60px;
height: 60px;
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 8px;
border: 2px solid #dcdfe6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 6px;
border: 1px solid #dcdfe6;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
}
@@ -735,7 +1044,7 @@ export default {
}
.prompt-container {
margin-top: 15px;
margin-top: 12px;
}
.prompt-input {
@@ -757,12 +1066,12 @@ export default {
.parameter-container {
display: flex;
flex-direction: column;
gap: 20px;
gap: 16px;
}
.parameter-row {
display: flex;
gap: 30px;
gap: 20px;
align-items: center;
}
@@ -787,23 +1096,23 @@ export default {
.button-container {
display: flex;
justify-content: center;
padding: 20px 0;
padding: 16px 0;
}
.generate-button {
padding: 12px 40px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 10px 32px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
background: linear-gradient(135deg, #409eff 0%, #36a3f7 100%);
border: none;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
transition: all 0.3s ease;
}
.generate-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.generate-button:active {

View File

@@ -42,6 +42,7 @@
<el-option label="人物图" value="person" />
<el-option label="产品图" value="product" />
<el-option label="素材图" value="material" />
<el-option label="成品" value="result" />
<el-option label="其他" value="other" />
</el-select>
<el-button
@@ -70,6 +71,15 @@
>
上传素材
</el-button>
<el-button
class="filter-item"
type="warning"
icon="el-icon-refresh"
:loading="statusCheckLoading"
@click="handleStatusCheck"
>
状态检查
</el-button>
<el-button
v-if="multipleSelection.length > 0"
class="filter-item"
@@ -128,16 +138,35 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="素材图片" width="100" align="center">
<el-table-column label="素材图片" width="120" align="center">
<template slot-scope="scope">
<el-button
v-if="scope.row.materialTempUrl"
type="text"
size="small"
@click="openMaterialUrl( scope.row.materialTempUrl)"
>
查看
</el-button>
<div v-if="scope.row.materialTempUrl">
<!-- 如果是JSON串,显示下拉框 -->
<el-select
v-if="isJsonString(scope.row.materialTempUrl)"
v-model="selectedMaterialUrl[scope.row.uuid]"
placeholder="选择图片"
size="small"
style="width: 120px;"
@change="handleMaterialUrlChange(scope.row.uuid, $event)"
>
<el-option
v-for="(item, index) in parseMaterialUrls(scope.row.materialTempUrl)"
:key="index"
:label="item.label || `图片${index + 1}`"
:value="item.url || item"
/>
</el-select>
<!-- 如果是单个URL,显示查看按钮 -->
<el-button
v-else
type="text"
size="small"
@click="openMaterialUrl(scope.row.materialTempUrl)"
>
查看
</el-button>
</div>
<span v-else>-</span>
</template>
</el-table-column>
@@ -159,11 +188,6 @@
<span>{{ scope.row.prompt || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="功能函数" min-width="120" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ scope.row.functionName || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="放大倍数" width="100" align="center">
<template slot-scope="scope">
<span>{{ scope.row.scaleFactor || '-' }}</span>
@@ -174,6 +198,16 @@
<span>{{ formatDateTime(scope.row.updateTime) }}</span>
</template>
</el-table-column>
<el-table-column label="任务状态" width="120" align="center">
<template slot-scope="scope">
<el-tag
:type="getTaskStatusTagType(scope.row.taskStatus)"
size="small"
>
{{ getTaskStatusText(scope.row.taskStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" align="center">
<template slot-scope="scope">
<el-button
@@ -236,6 +270,73 @@
</div>
</el-dialog>
<!-- 素材信息对话框 -->
<el-dialog
title="素材信息"
:visible.sync="materialDialogVisible"
width="60%"
center
>
<div v-if="isJsonString(currentMaterialData)">
<div style="border: 1px solid #e4e7ed; border-radius: 6px; padding: 16px; background: #f8f9fa;">
<!-- 图像信息 -->
<div style="margin-bottom: 16px;">
<h4 style="margin: 0 0 12px 0; color: #303133; font-size: 14px; font-weight: 600;">图像信息</h4>
<div v-if="getMaterialData(currentMaterialData).baseImageUrl" style="margin-bottom: 8px;">
<div style="font-weight: 500; color: #606266;">基础图像 (baseImageUrl):</div>
<div style="margin-left: 16px;">
<div style="font-weight: 500; color: #606266;">地址: </div>
<el-button type="text" size="small" style="padding: 0; color: #409eff; text-decoration: underline;" @click="openMaterialUrl(getMaterialData(currentMaterialData).baseImageUrl)">
链接
</el-button>
</div>
</div>
<div v-if="getMaterialData(currentMaterialData).refImageUrl" style="margin-bottom: 8px;">
<div style="font-weight: 500; color: #606266;">参考图像 (refImageUrl):</div>
<div style="margin-left: 16px;">
<div style="font-weight: 500; color: #606266;">地址: </div>
<el-button type="text" size="small" style="padding: 0; color: #409eff; text-decoration: underline;" @click="openMaterialUrl(getMaterialData(currentMaterialData).refImageUrl)">
链接
</el-button>
</div>
</div>
</div>
<!-- 边缘信息 -->
<div v-if="getMaterialData(currentMaterialData).referenceEdge">
<h4 style="margin: 0 0 12px 0; color: #303133; font-size: 14px; font-weight: 600;">边缘信息 (referenceEdge)</h4>
<!-- 背景边缘图片 -->
<div v-if="getMaterialData(currentMaterialData).referenceEdge.backgroundEdge" style="margin-bottom: 8px;">
<div style="font-weight: 500; color: #606266;">背景边缘图片 (backgroundEdge):</div>
<div v-for="(url, index) in getMaterialData(currentMaterialData).referenceEdge.backgroundEdge" :key="index" style="margin-left: 16px;">
<div style="font-weight: 500; color: #606266;">地址: </div>
<el-button type="text" size="small" style="padding: 0; color: #409eff; text-decoration: underline;" @click="openMaterialUrl(url)">
链接
</el-button>
</div>
</div>
<!-- 前景边缘图片 -->
<div v-if="getMaterialData(currentMaterialData).referenceEdge.foregroundEdge" style="margin-bottom: 8px;">
<div style="font-weight: 500; color: #606266;">前景边缘图片 (foregroundEdge):</div>
<div v-for="(url, index) in getMaterialData(currentMaterialData).referenceEdge.foregroundEdge" :key="index" style="margin-left: 16px;">
<div style="font-weight: 500; color: #606266;">地址{{ index + 1 }}: </div>
<el-button type="text" size="small" style="padding: 0; color: #409eff; text-decoration: underline;" @click="openMaterialUrl(url)">
链接
</el-button>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<el-input v-model="currentMaterialData" placeholder="素材URL" readonly />
<el-button v-if="currentMaterialData" type="text" size="small" style="margin-top: 5px;" @click="openMaterialUrl(currentMaterialData)">
查看素材图片
</el-button>
</div>
</el-dialog>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
@@ -283,6 +384,7 @@
<el-option label="人物图" value="person" />
<el-option label="产品图" value="product" />
<el-option label="素材图" value="material" />
<el-option label="成品" value="result" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
@@ -300,7 +402,10 @@
</el-col>
</el-row>
<el-form-item label="素材URL">
<el-input v-model="temp.materialUrl" placeholder="请输入素材URL" />
<el-input v-model="temp.materialTempUrl" placeholder="素材URL" readonly />
<el-button v-if="temp.materialTempUrl" type="text" size="small" style="margin-top: 5px;" @click="openMaterialUrl(temp.materialTempUrl)">
查看素材图片
</el-button>
</el-form-item>
<el-form-item label="成品图片URL">
<el-input v-model="temp.resultImageUrl" placeholder="请输入成品图片URL" />
@@ -406,6 +511,7 @@ export default {
imageType: undefined
},
multipleSelection: [],
selectedMaterialUrl: {}, // 存储每行选中的素材URL
temp: {
id: undefined,
imageName: '',
@@ -426,10 +532,14 @@ export default {
dialogTitle: '',
previewDialogVisible: false,
previewImageUrl: '',
materialDialogVisible: false,
currentMaterialData: '',
// 上传素材相关
uploadDialogVisible: false,
uploadLoading: false,
fileList: [],
// 状态检查相关
statusCheckLoading: false,
uploadForm: {
imageName: '',
ownerName: '',
@@ -527,6 +637,13 @@ export default {
},
handleView(row) {
this.temp = Object.assign({}, row)
// 确保素材URL字段正确复制
if (row.materialTempUrl && !this.temp.materialTempUrl) {
this.temp.materialTempUrl = row.materialTempUrl
}
if (row.materialUrl && !this.temp.materialUrl) {
this.temp.materialUrl = row.materialUrl
}
this.dialogStatus = 'view'
this.dialogTitle = '查看图像详情'
this.dialogVisible = true
@@ -678,12 +795,102 @@ export default {
this.$message.warning('素材图片URL不存在')
}
},
// 检查字符串是否为JSON格式
isJsonString(str) {
if (!str || typeof str !== 'string') {
console.log('isJsonString: 不是字符串或为空', str)
return false
}
try {
const parsed = JSON.parse(str)
const isObject = typeof parsed === 'object' && parsed !== null
const hasContent = Array.isArray(parsed) ? parsed.length > 0 : Object.keys(parsed).length > 0
const result = isObject && hasContent
console.log('isJsonString 判断结果:', {
str: str.substring(0, 100) + '...',
isObject,
hasContent,
result,
parsed: parsed
})
return result
} catch (e) {
console.log('isJsonString: JSON解析失败', e.message)
return false
}
},
// 解析素材URL的JSON串
parseMaterialUrls(jsonStr) {
try {
const data = JSON.parse(jsonStr)
const urls = []
// 如果是数组,直接返回
if (Array.isArray(data)) {
return data
}
// 如果是对象,提取所有URL字段
if (typeof data === 'object' && data !== null) {
// 提取基础图片URL
if (data.baseImageUrl) {
urls.push({ label: '基础图片', url: data.baseImageUrl })
}
if (data.refImageUrl) {
urls.push({ label: '参考图片', url: data.refImageUrl })
}
// 提取参考边缘图片
if (data.referenceEdge) {
if (data.referenceEdge.foregroundEdge && Array.isArray(data.referenceEdge.foregroundEdge)) {
data.referenceEdge.foregroundEdge.forEach((url, index) => {
urls.push({ label: `前景边缘图${index + 1}`, url })
})
}
if (data.referenceEdge.backgroundEdge && Array.isArray(data.referenceEdge.backgroundEdge)) {
data.referenceEdge.backgroundEdge.forEach((url, index) => {
urls.push({ label: `背景边缘图${index + 1}`, url })
})
}
}
}
return urls
} catch (e) {
console.error('解析素材URL JSON失败:', e)
return []
}
},
// 处理素材URL下拉框选择变化
handleMaterialUrlChange(uuid, selectedUrl) {
console.log('选择素材URL:', { uuid, selectedUrl })
if (selectedUrl) {
this.openMaterialUrl(selectedUrl)
}
},
// 获取素材数据的解析结果
getMaterialData(jsonStr) {
try {
return JSON.parse(jsonStr)
} catch (e) {
console.error('解析素材数据失败:', e)
return {}
}
},
// 打开素材信息对话框
openMaterialDialog(materialData) {
this.currentMaterialData = materialData
this.materialDialogVisible = true
},
getImageTypeTag(type) {
const typeMap = {
background: 'success',
person: 'primary',
product: 'warning',
material: 'danger',
result: 'success',
other: 'info'
}
return typeMap[type] || 'info'
@@ -694,10 +901,31 @@ export default {
person: '人物图',
product: '产品图',
material: '素材图',
result: '成品',
other: '其他'
}
return typeMap[type] || '未知'
},
// 获取任务状态文本
getTaskStatusText(status) {
const statusMap = {
PENDING: '待处理',
RUNNING: '运行中',
SUCCEEDED: '成功',
FAILED: '失败'
}
return statusMap[status] || '未知'
},
// 获取任务状态标签类型
getTaskStatusTagType(status) {
const typeMap = {
PENDING: 'warning',
RUNNING: 'primary',
SUCCEEDED: 'success',
FAILED: 'danger'
}
return typeMap[status] || 'info'
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) {
@@ -724,6 +952,79 @@ export default {
this.$refs['uploadForm'].clearValidate()
})
},
// 状态检查方法
handleStatusCheck() {
this.statusCheckLoading = true
console.log('开始执行状态检查...')
// 导入API方法
import('@/api/image-management').then(({ checkImageModelStatus }) => {
checkImageModelStatus().then(response => {
console.log('状态检查响应:', response)
this.statusCheckLoading = false
if (response.success) {
this.$message.success(response.message || '状态检查执行成功')
// 刷新列表以获取最新状态
this.getList()
} else {
this.$message.error(response.message || '状态检查执行失败')
}
}).catch(error => {
console.error('状态检查失败:', error)
this.statusCheckLoading = false
this.$message.error('状态检查失败: ' + (error.message || '未知错误'))
})
}).catch(error => {
console.error('导入API失败:', error)
this.statusCheckLoading = false
this.$message.error('导入API失败')
})
},
// 图像格式转换方法
convertImageToRGBA(file) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
img.onload = () => {
canvas.width = img.width
canvas.height = img.height
// 绘制图像到canvas
ctx.drawImage(img, 0, 0)
// 获取RGBA数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// 确保所有像素都有Alpha通道
for (let i = 3; i < imageData.data.length; i += 4) {
if (imageData.data[i] === 0) {
imageData.data[i] = 255 // 设置不透明
}
}
// 将修改后的数据绘制回canvas
ctx.putImageData(imageData, 0, 0)
// 转换为Blob
canvas.toBlob((blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('图像转换失败'))
}
}, 'image/png') // PNG格式支持RGBA
}
img.onerror = () => {
reject(new Error('图像加载失败'))
}
img.src = URL.createObjectURL(file)
})
},
resetUploadForm() {
this.uploadForm = {
imageName: '',
@@ -771,40 +1072,60 @@ export default {
}
})
},
uploadMaterial() {
// 创建FormData对象
const formData = new FormData()
formData.append('file', this.uploadForm.file)
// 注意:imageName, ownerName, ownerPhone 作为查询参数传递,而不是FormData
async uploadMaterial() {
try {
// 检查文件类型
const file = this.uploadForm.file
if (!file.type.startsWith('image/')) {
this.$message.error('请选择图像文件')
this.uploadLoading = false
return
}
// 准备查询参数
const params = {
imageName: this.uploadForm.imageName,
ownerName: this.uploadForm.ownerName,
ownerPhone: this.uploadForm.ownerPhone
console.log('开始转换图像格式...')
// 转换图像为RGBA格式
const convertedFile = await this.convertImageToRGBA(file)
console.log('图像格式转换完成')
// 创建FormData对象
const formData = new FormData()
formData.append('file', convertedFile, file.name) // 使用转换后的文件
// 注意:imageName, ownerName, ownerPhone 作为查询参数传递,而不是FormData
// 准备查询参数
const params = {
imageName: this.uploadForm.imageName,
ownerName: this.uploadForm.ownerName,
ownerPhone: this.uploadForm.ownerPhone
}
console.log('上传数据:', {
imageName: this.uploadForm.imageName,
ownerName: this.uploadForm.ownerName,
ownerPhone: this.uploadForm.ownerPhone,
fileName: file.name,
fileSize: file.size
})
// 调用真实的上传API,传递查询参数
uploadMaterial(formData, params).then(response => {
console.log('上传响应:', response)
this.uploadLoading = false
this.uploadDialogVisible = false
this.$message.success('素材上传成功')
this.getList() // 刷新列表
this.resetUploadForm()
}).catch(error => {
console.error('上传失败:', error)
this.uploadLoading = false
this.$message.error('上传失败: ' + (error.message || '未知错误'))
})
} catch (error) {
console.error('图像格式转换失败:', error)
this.uploadLoading = false
this.$message.error('图像格式转换失败: ' + (error.message || '未知错误'))
}
console.log('上传数据:', {
imageName: this.uploadForm.imageName,
ownerName: this.uploadForm.ownerName,
ownerPhone: this.uploadForm.ownerPhone,
fileName: this.uploadForm.file.name,
fileSize: this.uploadForm.file.size
})
// 调用真实的上传API,传递查询参数
uploadMaterial(formData, params).then(response => {
console.log('上传响应:', response)
this.uploadLoading = false
this.uploadDialogVisible = false
this.$message.success('素材上传成功')
this.getList() // 刷新列表
this.resetUploadForm()
}).catch(error => {
console.error('上传失败:', error)
this.uploadLoading = false
this.$message.error('上传失败: ' + (error.message || '未知错误'))
})
},
// 去除水印处理方法
handleRemoveWatermark(row) {