图生图
This commit is contained in:
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.nyc_output
|
||||
coverage
|
||||
.DS_Store
|
||||
*.log
|
||||
dist
|
||||
.vscode
|
||||
.idea
|
||||
32
database/ai_image_analysis.sql
Normal file
32
database/ai_image_analysis.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- AI图像分析服务数据库表结构
|
||||
-- 基于qwen3-vl-plus模型的图像分析请求和响应记录(合并表)
|
||||
|
||||
-- 创建AI图像分析记录表(合并请求和响应)
|
||||
CREATE TABLE `ai_image_analysis` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`model` varchar(50) NOT NULL COMMENT '使用的AI模型',
|
||||
`image_url` varchar(500) NOT NULL COMMENT '图像URL',
|
||||
`user_question` text COMMENT '用户问题文本',
|
||||
`response_content` text COMMENT 'AI返回的内容',
|
||||
`role` varchar(20) DEFAULT 'assistant' COMMENT '角色',
|
||||
`finish_reason` varchar(20) COMMENT '完成原因',
|
||||
`choice_index` int(11) DEFAULT 0 COMMENT '选择索引',
|
||||
`usage_prompt_tokens` int(11) COMMENT '提示词token数量',
|
||||
`usage_completion_tokens` int(11) COMMENT '完成token数量',
|
||||
`usage_total_tokens` int(11) COMMENT '总token数量',
|
||||
`request_data` json COMMENT '完整请求参数JSON',
|
||||
`response_data` json COMMENT '完整响应数据JSON',
|
||||
`user_name` varchar(100) COMMENT '用户姓名',
|
||||
`user_phone` varchar(20) COMMENT '用户电话',
|
||||
`session_name` varchar(200) COMMENT '会话名称',
|
||||
`status` tinyint(4) DEFAULT 1 COMMENT '状态:1-成功,0-失败',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_model` (`model`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_created_at` (`updated_at`),
|
||||
KEY `idx_model_created` (`model`, `created_at`),
|
||||
KEY `idx_image_url` (`image_url`(255))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI图像视频分析记录表';
|
||||
|
||||
128
mock/dealership.js
Normal file
128
mock/dealership.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import Mock from 'mockjs'
|
||||
|
||||
// 模拟经销商数据
|
||||
const dealershipData = Mock.mock({
|
||||
'list|20': [{
|
||||
'id|+1': 1,
|
||||
'dealershipName|1': ['销售演示-上汽', '售后演示', 'DCC演示', '销售演示-奔驰', '销售演示-比亚迪', '销售演示-宝马', '销售演示-奥迪', '销售演示-丰田', '销售演示-本田', '销售演示-日产'],
|
||||
'dealershipCode': function() {
|
||||
const codes = ['XS001', 'SH001', 'DCC001', 'BC001', 'BYD001', 'BMW001', 'AUDI001', 'TOYOTA001', 'HONDA001', 'NISSAN001']
|
||||
return codes[this.id - 1] || 'DS' + Mock.Random.string('number', 3)
|
||||
},
|
||||
'remainingHours|1': [-36.31, -55.44, -4.58, 9809.16, 9959.56, 5000, -100, 2000, -50, 8000],
|
||||
'salesCount|5-20': 1,
|
||||
'avgRecordings|1-10': 1,
|
||||
'avgRecordingDuration|10-80.2-2': 1,
|
||||
'region|1': ['', '华东', '华南', '华北', '华中', '西南', '西北', '东北'],
|
||||
'groupName|1': ['', '默认', '销售组', '售后组', 'DCC组'],
|
||||
'language|1': ['普通话', '上海话', '粤语', '英语'],
|
||||
'dealershipType|1': ['售前', '售后', '销售'],
|
||||
'status|1': [true, false]
|
||||
}]
|
||||
})
|
||||
|
||||
// 模拟经销商列表查询
|
||||
Mock.mock(/\/dealership\/list/, 'get', (options) => {
|
||||
const params = new URLSearchParams(options.url.split('?')[1])
|
||||
const current = parseInt(params.get('current')) || 1
|
||||
const size = parseInt(params.get('size')) || 20
|
||||
const dealershipName = params.get('dealershipName')
|
||||
const dealershipCode = params.get('dealershipCode')
|
||||
const region = params.get('region')
|
||||
const status = params.get('status')
|
||||
|
||||
let filteredData = dealershipData.list
|
||||
|
||||
// 根据条件过滤
|
||||
if (dealershipName) {
|
||||
filteredData = filteredData.filter(item => item.dealershipName.includes(dealershipName))
|
||||
}
|
||||
|
||||
if (dealershipCode) {
|
||||
filteredData = filteredData.filter(item => item.dealershipCode.includes(dealershipCode))
|
||||
}
|
||||
|
||||
if (region) {
|
||||
filteredData = filteredData.filter(item => item.region === region)
|
||||
}
|
||||
|
||||
if (status !== null && status !== undefined && status !== '') {
|
||||
filteredData = filteredData.filter(item => item.status === (status === 'true'))
|
||||
}
|
||||
|
||||
const start = (current - 1) * size
|
||||
const end = start + size
|
||||
const pageData = filteredData.slice(start, end)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '查询成功',
|
||||
total: filteredData.length,
|
||||
current: current,
|
||||
pages: Math.ceil(filteredData.length / size),
|
||||
data: pageData,
|
||||
size: size
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟添加经销商
|
||||
Mock.mock('/dealership/add', 'post', () => {
|
||||
return {
|
||||
success: true,
|
||||
message: '经销商添加成功'
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟更新经销商
|
||||
Mock.mock('/dealership/update', 'put', () => {
|
||||
return {
|
||||
success: true,
|
||||
message: '经销商信息更新成功'
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟删除经销商
|
||||
Mock.mock(/\/dealership\/delete\/\d+/, 'delete', () => {
|
||||
return {
|
||||
success: true,
|
||||
message: '经销商删除成功'
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟根据ID查询经销商
|
||||
Mock.mock(/\/dealership\/get\/\d+/, 'get', () => {
|
||||
return {
|
||||
success: true,
|
||||
message: '查询成功',
|
||||
data: dealershipData.list[0]
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟根据经销商代码查询
|
||||
Mock.mock(/\/dealership\/getByCode/, 'get', () => {
|
||||
return {
|
||||
success: true,
|
||||
message: '查询成功',
|
||||
data: dealershipData.list.slice(0, 3),
|
||||
count: 3
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟批量删除经销商
|
||||
Mock.mock('/dealership/batchDelete', 'delete', () => {
|
||||
return {
|
||||
success: true,
|
||||
message: '批量删除成功,共删除 3 条记录'
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟获取经销商统计信息
|
||||
Mock.mock('/dealership/statistics', 'get', () => {
|
||||
return {
|
||||
success: true,
|
||||
message: '统计信息获取成功',
|
||||
data: {
|
||||
totalDealerships: 20
|
||||
}
|
||||
}
|
||||
})
|
||||
171
mock/stock.js
Normal file
171
mock/stock.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import Mock from 'mockjs'
|
||||
|
||||
// 模拟股票数据
|
||||
const stockData = Mock.mock({
|
||||
'list|100': [{
|
||||
'code': /^[0-9]{6}$/,
|
||||
'name': '@ctitle(2,4)',
|
||||
'industry|1': ['银行', '房地产', '白酒', '电子', '医药', '新能源', '化工', '钢铁', '汽车', '家电'],
|
||||
'pattern|1': ['V型反转', '强势上涨', '底部反转', '突破上涨', '双底形态', '头肩底'],
|
||||
'risePercent|20-60.1-1': 1,
|
||||
'volumeIncrease|1': ['1.5倍', '2倍', '2.5倍', '3倍', '3.5倍', '4倍'],
|
||||
'similarity|60-95.1-0': 1,
|
||||
'occurDate': '@date("yyyy-MM-dd")',
|
||||
'currentPrice|10-100.2-2': 1,
|
||||
'marketCap|100-1000.1-1': 1,
|
||||
'pe|5-50.1-1': 1,
|
||||
'pb|0.5-10.2-2': 1
|
||||
}]
|
||||
})
|
||||
|
||||
// 模拟相似股票查询
|
||||
Mock.mock(/\/api\/stock\/similar/, 'get', (options) => {
|
||||
const params = new URLSearchParams(options.url.split('?')[1])
|
||||
const page = parseInt(params.get('page')) || 1
|
||||
const pageSize = parseInt(params.get('pageSize')) || 20
|
||||
const patternType = params.get('patternType')
|
||||
const minRise = parseInt(params.get('minRise')) || 0
|
||||
|
||||
let filteredData = stockData.list
|
||||
|
||||
// 根据条件过滤
|
||||
if (patternType) {
|
||||
const patternMap = {
|
||||
'v_reversal': 'V型反转',
|
||||
'strong_rise': '强势上涨',
|
||||
'bottom_reversal': '底部反转',
|
||||
'breakout_rise': '突破上涨'
|
||||
}
|
||||
filteredData = filteredData.filter(item => item.pattern === patternMap[patternType])
|
||||
}
|
||||
|
||||
if (minRise > 0) {
|
||||
filteredData = filteredData.filter(item => item.risePercent >= minRise)
|
||||
}
|
||||
|
||||
const start = (page - 1) * pageSize
|
||||
const end = start + pageSize
|
||||
const pageData = filteredData.slice(start, end)
|
||||
|
||||
return {
|
||||
code: 20000,
|
||||
data: {
|
||||
list: pageData,
|
||||
total: filteredData.length,
|
||||
page,
|
||||
pageSize
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟股票历史数据
|
||||
Mock.mock(/\/api\/stock\/history\/\d+/, 'get', () => {
|
||||
const days = 100
|
||||
const data = []
|
||||
let basePrice = Mock.Random.float(10, 100, 2, 2)
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const change = Mock.Random.float(-0.05, 0.08, 4, 4)
|
||||
basePrice = basePrice * (1 + change)
|
||||
|
||||
data.push({
|
||||
date: Mock.Random.date('yyyy-MM-dd'),
|
||||
open: Mock.Random.float(basePrice * 0.98, basePrice * 1.02, 2, 2),
|
||||
high: Mock.Random.float(basePrice * 1.01, basePrice * 1.05, 2, 2),
|
||||
low: Mock.Random.float(basePrice * 0.95, basePrice * 0.99, 2, 2),
|
||||
close: basePrice,
|
||||
volume: Mock.Random.integer(1000000, 10000000),
|
||||
amount: Mock.Random.integer(10000000, 100000000)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
code: 20000,
|
||||
data: data.reverse()
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟股票基本信息
|
||||
Mock.mock(/\/api\/stock\/info\/\d+/, 'get', () => {
|
||||
return {
|
||||
code: 20000,
|
||||
data: {
|
||||
code: Mock.Random.string('number', 6),
|
||||
name: Mock.Random.ctitle(2, 4),
|
||||
industry: Mock.Random.pick(['银行', '房地产', '白酒', '电子', '医药', '新能源']),
|
||||
currentPrice: Mock.Random.float(10, 100, 2, 2),
|
||||
change: Mock.Random.float(-5, 10, 2, 2),
|
||||
changePercent: Mock.Random.float(-10, 20, 2, 2),
|
||||
marketCap: Mock.Random.float(100, 1000, 1, 1),
|
||||
pe: Mock.Random.float(5, 50, 1, 1),
|
||||
pb: Mock.Random.float(0.5, 10, 2, 2),
|
||||
turnover: Mock.Random.float(1, 10, 2, 2),
|
||||
volume: Mock.Random.integer(1000000, 10000000),
|
||||
amount: Mock.Random.integer(10000000, 100000000)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟行业列表
|
||||
Mock.mock('/api/stock/industries', 'get', () => {
|
||||
return {
|
||||
code: 20000,
|
||||
data: [
|
||||
'银行', '房地产', '白酒', '电子', '医药', '新能源', '化工', '钢铁', '汽车', '家电',
|
||||
'通信', '计算机', '传媒', '农林牧渔', '有色金属', '建筑材料', '机械设备', '电气设备',
|
||||
'纺织服装', '轻工制造', '商业贸易', '交通运输', '公用事业', '国防军工'
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟热门股票
|
||||
Mock.mock('/api/stock/hot', 'get', () => {
|
||||
return {
|
||||
code: 20000,
|
||||
data: Mock.mock({
|
||||
'list|20': [{
|
||||
'code': /^[0-9]{6}$/,
|
||||
'name': '@ctitle(2,4)',
|
||||
'currentPrice|10-100.2-2': 1,
|
||||
'changePercent|1-20.1-1': 1,
|
||||
'volume|1000000-10000000': 1
|
||||
}]
|
||||
}).list
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟自选股操作
|
||||
Mock.mock('/api/stock/watchlist', 'post', () => {
|
||||
return {
|
||||
code: 20000,
|
||||
message: '添加成功'
|
||||
}
|
||||
})
|
||||
|
||||
Mock.mock('/api/stock/watchlist', 'get', () => {
|
||||
return {
|
||||
code: 20000,
|
||||
data: Mock.mock({
|
||||
'list|10': [{
|
||||
'code': /^[0-9]{6}$/,
|
||||
'name': '@ctitle(2,4)',
|
||||
'currentPrice|10-100.2-2': 1,
|
||||
'changePercent|1-20.1-1': 1
|
||||
}]
|
||||
}).list
|
||||
}
|
||||
})
|
||||
|
||||
// 模拟导出功能
|
||||
Mock.mock('/api/stock/export', 'post', () => {
|
||||
return {
|
||||
code: 20000,
|
||||
message: '导出成功',
|
||||
data: {
|
||||
downloadUrl: '/api/download/stock-analysis.xlsx'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
29
nginx.conf
Normal file
29
nginx.conf
Normal file
@@ -0,0 +1,29 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# 处理 Vue Router 的 history 模式
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location /static/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 启用 gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
}
|
||||
97
npm-dev.service
Normal file
97
npm-dev.service
Normal file
@@ -0,0 +1,97 @@
|
||||
[Unit]
|
||||
Description=Vue Admin Development Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your-username
|
||||
WorkingDirectory=/path/to/your/project
|
||||
ExecStart=/usr/bin/npm run dev
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -87,13 +87,26 @@ export function imageProcessing(data) {
|
||||
// 文本生图(基于参考图像和提示词)
|
||||
export function imageGenByText(data) {
|
||||
return request({
|
||||
url: '/image-model/image-gen-byText',
|
||||
url: '/image-model/text-to-image',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
// 图像生图(基于源图像生成新图像)
|
||||
export function imageGenByImage(formData) {
|
||||
return request({
|
||||
url: '/image-model/image-to-image',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
// 检查任务状态
|
||||
export function checkImageModelStatus() {
|
||||
return request({
|
||||
|
||||
@@ -311,18 +311,18 @@ export const constantRoutes = [
|
||||
component: () => import('@/views/image-model/image-list'),
|
||||
meta: { title: '图像管理列表', icon: 'el-icon-picture' }
|
||||
},
|
||||
{
|
||||
path: 'poster-generation',
|
||||
name: 'PosterGeneration',
|
||||
component: () => import('@/views/image-model/poster-generation'),
|
||||
meta: { title: '海报生成', icon: 'el-icon-picture-outline-round' }
|
||||
},
|
||||
{
|
||||
path: 'image-generation',
|
||||
name: 'ImageGeneration',
|
||||
component: () => import('@/views/image-model/image-generation'),
|
||||
meta: { title: '文本生图', icon: 'el-icon-picture-outline' }
|
||||
},
|
||||
{
|
||||
path: 'image-to-image',
|
||||
name: 'ImageToImage',
|
||||
component: () => import('@/views/image-model/image-to-image'),
|
||||
meta: { title: '图像生图', icon: 'el-icon-picture-outline' }
|
||||
},
|
||||
{
|
||||
path: 'background-model',
|
||||
name: 'BackgroundModel',
|
||||
@@ -330,10 +330,10 @@ export const constantRoutes = [
|
||||
meta: { title: '图像智聚', icon: 'el-icon-picture-outline' }
|
||||
},
|
||||
{
|
||||
path: 'virtual-model',
|
||||
name: 'VirtualModel',
|
||||
component: () => import('@/views/image-model/virtual-model'),
|
||||
meta: { title: '虚拟模特儿模型', icon: 'el-icon-user' }
|
||||
path: 'smart-image-analysis',
|
||||
name: 'SmartImageAnalysis',
|
||||
component: () => import('@/views/video/image-video-analysis'),
|
||||
meta: { title: '智解图片', icon: 'el-icon-cpu' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="素材图" value="material" />
|
||||
<el-option label="多图生图" value="result" />
|
||||
<el-option label="图生图" value="result" />
|
||||
<el-option label="文本生图" value="text_to_image" />
|
||||
<el-option label="去除水印" value="remove_water" />
|
||||
<el-option label="其他" value="other" />
|
||||
@@ -407,7 +407,10 @@
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="成品图片URL">
|
||||
<el-input v-model="temp.resultImageUrl" placeholder="请输入成品图片URL" />
|
||||
<el-input v-model="temp.resultImageTempUrl" placeholder="成品图片临时URL" readonly />
|
||||
<el-button v-if="temp.resultImageTempUrl" type="text" size="small" style="margin-top: 5px;" @click="openMaterialUrl(temp.resultImageTempUrl)">
|
||||
查看成品图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="提示词">
|
||||
<el-input
|
||||
@@ -643,6 +646,10 @@ export default {
|
||||
if (row.materialUrl && !this.temp.materialUrl) {
|
||||
this.temp.materialUrl = row.materialUrl
|
||||
}
|
||||
// 确保成品图片临时URL字段正确复制
|
||||
if (row.resultImageTempUrl && !this.temp.resultImageTempUrl) {
|
||||
this.temp.resultImageTempUrl = row.resultImageTempUrl
|
||||
}
|
||||
this.dialogStatus = 'view'
|
||||
this.dialogTitle = '查看图像详情'
|
||||
this.dialogVisible = true
|
||||
|
||||
397
src/views/image-model/image-to-image.vue
Normal file
397
src/views/image-model/image-to-image.vue
Normal file
@@ -0,0 +1,397 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>图像生图</span>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" class="inline-form half-width">
|
||||
<el-form-item label="图片名称" prop="imageName">
|
||||
<el-input v-model="form.imageName" placeholder="请输入图片名称" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属人姓名" prop="ownerName">
|
||||
<el-input v-model="form.ownerName" placeholder="请输入所属人姓名" maxlength="30" show-word-limit :disabled="true" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属人电话" prop="ownerPhone">
|
||||
<el-input v-model="form.ownerPhone" placeholder="请输入所属人电话" maxlength="20" :disabled="true" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型" prop="model">
|
||||
<el-select v-model="form.model" placeholder="请选择模型" filterable style="width: 100%">
|
||||
<el-option v-for="m in modelOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="基础图片" prop="baseImage">
|
||||
<el-upload
|
||||
ref="baseUpload"
|
||||
:auto-upload="false"
|
||||
:on-change="handleBaseImageChange"
|
||||
:before-upload="beforeUpload"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
class="image-upload"
|
||||
>
|
||||
<div v-if="!form.baseImageUrl" class="upload-area">
|
||||
<i class="el-icon-plus" />
|
||||
<div class="upload-text">点击上传基础图片</div>
|
||||
<div class="upload-hint">支持 JPG、PNG、GIF 格式,大小不超过 5MB</div>
|
||||
</div>
|
||||
<div v-else class="image-preview">
|
||||
<img :src="form.baseImageUrl" alt="基础图片预览" class="preview-image">
|
||||
<div class="image-actions">
|
||||
<el-button type="text" @click="removeBaseImage">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="参考提示词" prop="refPrompt">
|
||||
<el-input
|
||||
v-model="form.refPrompt"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="参考提示词"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
:disabled="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="生成指令" prop="prompt">
|
||||
<el-input
|
||||
v-model="form.prompt"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入生成指令,如:将这张图片转换为油画风格"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="button-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
class="generate-button"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<i class="el-icon-magic-stick" />
|
||||
生成图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import request from '@/utils/request'
|
||||
export default {
|
||||
name: 'ImageToImage',
|
||||
data() {
|
||||
return {
|
||||
submitting: false,
|
||||
form: {
|
||||
imageName: '',
|
||||
ownerName: '',
|
||||
ownerPhone: '',
|
||||
model: 'qwen-image-edit',
|
||||
baseImage: null,
|
||||
baseImageUrl: '',
|
||||
refPrompt: '请参考:把图像转为蓝底证件照,工作上午照片; 新增元素,删除元素,替换元素,人像修改 ,获得正视视角,朝向左侧,将背景更改为海滩',
|
||||
prompt: '',
|
||||
parameters: ''
|
||||
},
|
||||
modelOptions: [
|
||||
{ label: 'QW-PIC-EDIT', value: 'qwen-image-edit' },
|
||||
{ label: 'W-PIC-EIDT', value: 'wanx2.1-imageedit' }
|
||||
],
|
||||
rules: {
|
||||
imageName: [
|
||||
{ required: true, message: '请填写图片名称', trigger: 'blur' }
|
||||
],
|
||||
ownerName: [
|
||||
{ required: true, message: '请填写所属人姓名', trigger: 'blur' }
|
||||
],
|
||||
ownerPhone: [
|
||||
{ required: true, message: '请填写所属人电话', trigger: 'blur' },
|
||||
{ pattern: /^\+?\d[\d\-\s]{5,19}$/,
|
||||
message: '电话格式不正确', trigger: 'blur' }
|
||||
],
|
||||
model: [
|
||||
{ required: true, message: '请选择模型', trigger: 'change' }
|
||||
],
|
||||
baseImage: [
|
||||
{ required: true, message: '请上传基础图片', trigger: 'change' }
|
||||
],
|
||||
prompt: [
|
||||
{ required: true, message: '请填写生成指令', trigger: 'blur' },
|
||||
{ min: 4, message: '指令至少 4 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 预填充当前登录人的姓名和电话
|
||||
try {
|
||||
const name = this.$store.getters && this.$store.getters.name
|
||||
const phone = this.$store.getters && this.$store.getters.phone
|
||||
if (name) this.form.ownerName = name
|
||||
if (phone) this.form.ownerPhone = phone
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
beforeUpload(file) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt5M = file.size / 1024 / 1024 < 5 // 减少到5MB
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
||||
const isValidType = allowedTypes.includes(file.type)
|
||||
|
||||
if (!isImage) {
|
||||
this.$message.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isValidType) {
|
||||
this.$message.error('只支持 JPG、PNG、GIF 格式的图片!')
|
||||
return false
|
||||
}
|
||||
if (!isLt5M) {
|
||||
this.$message.error('图片大小不能超过 5MB!')
|
||||
return false
|
||||
}
|
||||
return false // 阻止自动上传
|
||||
},
|
||||
handleBaseImageChange(file, fileList) {
|
||||
if (file.raw) {
|
||||
// 如果文件大于2MB,进行压缩
|
||||
if (file.raw.size > 2 * 1024 * 1024) {
|
||||
this.compressImage(file.raw)
|
||||
} else {
|
||||
this.form.baseImage = file.raw
|
||||
this.form.baseImageUrl = URL.createObjectURL(file.raw)
|
||||
this.$refs.formRef && this.$refs.formRef.validateField('baseImage')
|
||||
}
|
||||
}
|
||||
},
|
||||
compressImage(file) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
// 计算压缩后的尺寸
|
||||
const maxWidth = 1920
|
||||
const maxHeight = 1080
|
||||
let { width, height } = img
|
||||
|
||||
if (width > height) {
|
||||
if (width > maxWidth) {
|
||||
height = (height * maxWidth) / width
|
||||
width = maxWidth
|
||||
}
|
||||
} else {
|
||||
if (height > maxHeight) {
|
||||
width = (width * maxHeight) / height
|
||||
height = maxHeight
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
// 绘制压缩后的图片
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
this.form.baseImage = blob
|
||||
this.form.baseImageUrl = URL.createObjectURL(blob)
|
||||
this.$refs.formRef && this.$refs.formRef.validateField('baseImage')
|
||||
this.$message.success('图片已自动压缩')
|
||||
}, 'image/jpeg', 0.8)
|
||||
}
|
||||
|
||||
img.src = URL.createObjectURL(file)
|
||||
},
|
||||
removeBaseImage() {
|
||||
this.form.baseImage = null
|
||||
this.form.baseImageUrl = ''
|
||||
this.$refs.formRef && this.$refs.formRef.validateField('baseImage')
|
||||
},
|
||||
handleSubmit() {
|
||||
console.log('handleSubmit 被调用')
|
||||
console.log('表单ref是否存在:', !!this.$refs.formRef)
|
||||
this.submit()
|
||||
},
|
||||
submit() {
|
||||
console.log('提交按钮被点击')
|
||||
console.log('当前表单数据:', this.form)
|
||||
|
||||
this.$refs.formRef && this.$refs.formRef.validate(async valid => {
|
||||
console.log('表单验证结果:', valid)
|
||||
if (!valid) {
|
||||
console.log('表单验证失败')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('开始提交数据')
|
||||
this.submitting = true
|
||||
|
||||
// 检查是否已选择图片
|
||||
if (!this.form.baseImage) {
|
||||
this.$message.error('请先上传基础图片')
|
||||
this.submitting = false
|
||||
return
|
||||
}
|
||||
|
||||
// 立即显示提交成功提示
|
||||
this.$message.success('提交成功,请在图像管理列表查看结果。')
|
||||
this.submitting = false
|
||||
|
||||
// 异步提交到后端,不等待响应
|
||||
this.submitToBackend()
|
||||
})
|
||||
},
|
||||
async submitToBackend() {
|
||||
try {
|
||||
// 构建FormData请求数据,匹配后端@RequestParam接口格式
|
||||
const formData = new FormData()
|
||||
formData.append('multipartFile', this.form.baseImage)
|
||||
formData.append('refPrompt', this.form.refPrompt)
|
||||
formData.append('prompt', this.form.prompt)
|
||||
formData.append('ownerName', this.form.ownerName)
|
||||
formData.append('ownerPhone', this.form.ownerPhone)
|
||||
formData.append('imageName', this.form.imageName)
|
||||
formData.append('model', this.form.model)
|
||||
formData.append('parameters', this.form.parameters)
|
||||
|
||||
console.log('异步发送FormData请求数据')
|
||||
// 使用项目的请求服务,但移除超时限制,不等待响应
|
||||
request({
|
||||
url: '/image-model/image-to-image',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 0 // 移除超时限制
|
||||
}).then(response => {
|
||||
console.log('后端请求已发送,状态:', response)
|
||||
}).catch(error => {
|
||||
console.log('后端请求发送完成,可能仍在处理中:', error.message)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('异步提交过程中出现错误,但不影响用户体验:', e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.half-width {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.generate-button {
|
||||
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 2px 8px rgba(64, 158, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.generate-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
.generate-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.upload-area i {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.image-actions .el-button {
|
||||
color: white;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user