Files
msh-system/msh_single_uniapp/pages/ai-generate/result.vue

1143 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="result-container" :style="{ paddingTop: statusBarHeight + 'px' }">
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="iconfont icon-fanhui"></text>
</view>
<view class="nav-title">生成结果</view>
<view class="nav-right"></view>
</view>
<!-- AI生成内容标识 -->
<view class="ai-notice">
<text class="ai-notice-icon">🤖</text>
<text class="ai-notice-text">图片由AI生成结果仅供参考</text>
</view>
<scroll-view scroll-y class="main-content">
<!-- 生成进度提示生成中状态 -->
<view class="generating-tip" v-if="isGenerating">
<view class="generating-animation">
<view class="dot dot-1"></view>
<view class="dot dot-2"></view>
<view class="dot dot-3"></view>
</view>
<text class="generating-text">正在创作中...</text>
<text class="generating-time">预计{{ estimatedTime }}</text>
</view>
<!-- 图片结果展示 -->
<view class="result-section" v-if="!isGenerating">
<!-- 单张图片显示 -->
<view class="single-image-container" v-if="generatedImages.length > 0">
<view class="image-item" @click="previewImage(0)">
<image
:src="generatedImages[0].url"
mode="aspectFit"
class="result-image"
:class="{ 'loading': generatedImages[0].loading }"
@load="onImageLoad"
@error="onImageError"
:show-menu-by-longpress="true"
></image>
<!-- 加载动画 -->
<view class="image-loading" v-if="generatedImages[0].loading">
<view class="loading-spinner"></view>
</view>
<!-- 图片加载失败提示 -->
<view class="image-error" v-if="imageLoadError">
<text class="error-text">图片加载</text>
<text class="error-url">{{ generatedImages[0].url }}</text>
</view>
</view>
</view>
<!-- 调试信息 -->
<view class="debug-info" v-if="showDebugInfo && generatedImages.length > 0">
<text class="debug-title">调试信息:</text>
<text class="debug-text">图片数量: {{ generatedImages.length }}</text>
<text class="debug-text">图片URL: {{ generatedImages[0].url }}</text>
<text class="debug-text">标题: {{ promptText }}</text>
<text class="debug-text">加载状态: {{ generatedImages[0].loading ? '加载中' : '已完成' }}</text>
<text class="debug-text">错误状态: {{ imageLoadError ? '失败' : '正常' }}</text>
<button class="debug-btn" @click="testImageLoad">测试图片加载</button>
<button class="debug-btn" @click="showDebugInfo = false">关闭调试</button>
</view>
<!-- 生成信息 -->
<view class="info-section">
<view class="info-header" @click="showPrompt = !showPrompt">
<text class="info-title">生成描述</text>
<text class="iconfont" :class="showPrompt ? 'icon-xiangshang' : 'icon-xiangxia'"></text>
</view>
<view class="prompt-content" v-if="showPrompt">
<!-- 直接显示编辑输入框 -->
<textarea
v-model="promptText"
class="prompt-input"
placeholder="请输入生成描述"
:maxlength="800"
:auto-height="true"
></textarea>
</view>
</view>
<!-- 配置选项 -->
<view class="section config-section">
<!-- 生成比例选择 -->
<view class="ratio-section">
<view class="ratio-header" @click="showRatioList = !showRatioList">
<view class="ratio-title-wrapper">
<text class="ratio-title">生成比例</text>
<text class="ratio-current" v-if="!showRatioList">{{ selectedRatio }}</text>
</view>
<text class="iconfont" :class="showRatioList ? 'icon-xiangshang' : 'icon-xiangxia'"></text>
</view>
<view class="ratio-content" v-if="showRatioList">
<view class="ratio-list">
<view
v-for="(ratio, index) in ratios"
:key="index"
:class="['ratio-item', selectedRatio === ratio.value ? 'active' : '']"
@click="selectRatio(ratio.value)"
>
<view class="ratio-box" :style="{ aspectRatio: ratio.aspect }"></view>
<text class="ratio-text">{{ ratio.value }}</text>
</view>
</view>
</view>
</view>
<!-- 联想词助手开关 -->
<view class="config-item assistant-item">
<view class="assistant-info">
<text class="assistant-title">联想词助手</text>
<text class="assistant-desc">AI智能补充和优化提示词</text>
</view>
<switch
:checked="enableAssistant"
@change="toggleAssistant"
color="#42ca4d"
/>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-section">
<button class="action-btn primary" @click="handleReGenerate">
<text class="btn-text">再次生成</text>
</button>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="footer-bar" v-if="!isGenerating">
<button class="footer-btn secondary" @click="handleShare">
<text class="iconfont icon-fenxiang"></text>
<text class="btn-text">分享</text>
</button>
<button class="footer-btn secondary" @click="handleDownload">
<text class="iconfont icon-xiazai"></text>
<text class="btn-text">下载</text>
</button>
<button class="footer-btn primary" @click="handleApply">
<text class="btn-text">应用作品</text>
</button>
</view>
<!-- 图片预览弹窗 -->
<view class="preview-overlay" v-if="showPreview" @click="closePreview">
<swiper
class="preview-swiper"
:current="currentPreviewIndex"
@change="onSwiperChange"
@click.stop
>
<swiper-item v-for="(img, index) in generatedImages" :key="index">
<view class="swiper-item-content">
<image
:src="img.url"
mode="aspectFit"
class="preview-full-image"
></image>
</view>
</swiper-item>
</swiper>
<!-- 预览工具栏 -->
<view class="preview-toolbar" @click.stop>
<view class="toolbar-left">
<text class="page-indicator">{{ currentPreviewIndex + 1 }}/{{ generatedImages.length }}</text>
</view>
<view class="toolbar-right">
<view class="tool-btn" @click.stop="downloadSingleImage(currentPreviewIndex)">
<text class="iconfont icon-xiazai"></text>
</view>
<view class="tool-btn" @click.stop="closePreview">
<text class="iconfont icon-guanbi"></text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import api from '@/api/models-api.js'
export default {
data() {
return {
statusBarHeight: 0,
// 生成状态
isGenerating: false,
estimatedTime: 30,
// 生成参数
model: '4.0',
promptText: '按照参考图的宽高比例,生成一个新的"会员礼包"的商城小程序banner展示沉香类商品电商活动限时特惠满赠香薰',
referenceImages: [], // 参考图列表
// 生成比例
selectedRatio: '9:16',
ratios: [
{ value: '9:16', aspect: '9/16', label: '竖屏' },
{ value: '3:4', aspect: '3/4', label: '竖屏' },
{ value: '2:3', aspect: '2/3', label: '竖屏' },
{ value: '1:1', aspect: '1/1', label: '方形' },
{ value: '3:2', aspect: '3/2', label: '横屏' }
],
showRatioList: false, // 比例列表展开状态
// 联想词助手
enableAssistant: true,
// 生成结果
generatedImages: [
{
url: '/static/images/demo-result-1.jpg',
loading: false
}
],
// UI控制
showPrompt: true,
showPreview: false,
currentPreviewIndex: 0,
imageLoadError: false,
showDebugInfo: false // 开启调试信息显示
};
},
onLoad(options) {
console.log('result.vue onLoad 接收参数:', options);
this.initPage();
// 接收生成参数
if (options.promptText) {
this.promptText = decodeURIComponent(options.promptText);
}
if (options.ratio) {
this.selectedRatio = options.ratio;
}
if (options.model) {
this.model = options.model;
}
if (options.referenceImages) {
try {
this.referenceImages = JSON.parse(decodeURIComponent(options.referenceImages));
} catch (e) {
console.error('解析参考图失败:', e);
}
}
// 如果是新生成,显示生成动画
if (options.generate === 'true') {
this.startGenerating();
return;
}
// 如果是从灵感广场跳转过来,加载对应的图片
if (options.image) {
const imageUrl = decodeURIComponent(options.image);
const title = options.title ? decodeURIComponent(options.title) : '未命名作品';
console.log('解码后的图片URL:', imageUrl);
console.log('解码后的标题:', title);
// 更新显示数据
this.promptText = title;
this.generatedImages = [
{
url: imageUrl,
loading: false
}
];
// 将图片URL作为参考图
this.referenceImages = [imageUrl];
console.log('更新后的 generatedImages:', this.generatedImages);
console.log('更新后的 referenceImages:', this.referenceImages);
} else {
console.warn('未接收到 options.image 参数');
}
},
methods: {
initPage() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
},
// 图片加载成功
onImageLoad(e) {
console.log('图片加载成功:', e);
this.imageLoadError = false;
if (this.generatedImages.length > 0) {
this.generatedImages[0].loading = false;
}
},
// 图片加载失败
onImageError(e) {
console.error('图片加载失败:', e);
if (this.generatedImages.length > 0) {
console.error('失败的图片URL:', this.generatedImages[0].url);
this.generatedImages[0].loading = false;
}
this.imageLoadError = true;
uni.showToast({
title: '图片加载失败',
icon: 'none',
duration: 2000
});
},
// 测试图片加载
testImageLoad() {
if (this.generatedImages.length > 0) {
const imageUrl = this.generatedImages[0].url;
console.log('测试图片URL:', imageUrl);
// 尝试重新加载图片
this.imageLoadError = false;
this.generatedImages[0].loading = true;
// 强制刷新
const tempUrl = this.generatedImages[0].url;
this.generatedImages[0].url = '';
this.$nextTick(() => {
this.generatedImages[0].url = tempUrl;
this.generatedImages[0].loading = false;
});
uni.showToast({
title: '正在重新加载图片',
icon: 'none'
});
}
},
// 开始生成
startGenerating() {
this.isGenerating = true;
// 模拟生成过程
const timer = setInterval(() => {
this.estimatedTime--;
if (this.estimatedTime <= 0) {
clearInterval(timer);
this.isGenerating = false;
uni.showToast({
title: '生成完成',
icon: 'success'
});
setTimeout(() => {
uni.navigateTo({ url: '/pages/ai-generate/inspiration' });
}, 600);
}
}, 1000);
},
// 返回
goBack() {
uni.navigateBack();
},
// 预览图片
previewImage(index) {
this.currentPreviewIndex = index;
this.showPreview = true;
},
// 关闭预览
closePreview() {
this.showPreview = false;
},
// 切换预览图片
onSwiperChange(e) {
this.currentPreviewIndex = e.detail.current;
},
// 选择比例
selectRatio(value) {
this.selectedRatio = value;
},
// 切换联想词助手
toggleAssistant(e) {
this.enableAssistant = e.detail.value;
},
// 再次生成
async handleReGenerate() {
// 验证提示词
if (!this.promptText.trim()) {
uni.showToast({
title: '请输入生成描述',
icon: 'none'
});
return;
}
// 验证是否有参考图
if (this.referenceImages.length === 0) {
uni.showToast({
title: '缺少参考图,请返回重新配置',
icon: 'none',
duration: 2000
});
return;
}
try {
uni.showLoading({
title: '生成中...'
});
// 调用图片编辑API
const response = await api.createImageEditTask({
prompt: this.promptText,
image_urls: this.referenceImages,
output_format: 'png',
image_size: this.selectedRatio,
title: this.promptText
});
console.log('图片编辑任务创建成功:', response);
uni.hideLoading();
// 显示生成动画
this.startGenerating();
uni.showToast({
title: '任务已提交',
icon: 'success'
});
} catch (error) {
console.error('图片编辑任务创建失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '生成失败,请重试',
icon: 'none',
duration: 2000
});
}
},
// 分享
handleShare() {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '分享到微博'],
success: (res) => {
uni.showToast({
title: '分享功能开发中',
icon: 'none'
});
}
});
},
// 下载
handleDownload() {
if (this.generatedImages.length === 0) {
uni.showToast({
title: '暂无图片',
icon: 'none'
});
return;
}
uni.showLoading({
title: '下载中...'
});
// TODO: 实现图片下载
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '已保存到相册',
icon: 'success'
});
}, 1500);
},
// 下载单张图片
downloadSingleImage(index) {
uni.showLoading({
title: '下载中...'
});
// TODO: 实现单张下载
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '已保存到相册',
icon: 'success'
});
}, 1000);
},
// 应用到商品
handleApply() {
uni.showActionSheet({
itemList: ['设置为商品主图', '添加到商品详情', '用作活动Banner', '生成推广海报'],
success: (res) => {
uni.showToast({
title: '应用成功',
icon: 'success'
});
}
});
}
}
};
</script>
<style lang="scss" scoped>
.result-container {
width: 100%;
min-height: 100vh;
background: linear-gradient(180deg, #000000 0%, #1a1a1a 100%);
position: relative;
overflow-x: hidden;
}
// 顶部导航
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
.nav-left,
.nav-right {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 20px;
color: #ffffff;
}
}
.nav-title {
font-size: 17px;
font-weight: 500;
color: #ffffff;
}
}
// AI生成内容标识
.ai-notice {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
margin: 8px 16px;
background: rgba(66, 202, 77, 0.1);
border: 1px solid rgba(66, 202, 77, 0.3);
border-radius: 8px;
backdrop-filter: blur(10px);
.ai-notice-icon {
font-size: 16px;
}
.ai-notice-text {
font-size: 12px;
color: #42ca4d;
line-height: 1.4;
}
}
// 主内容区
.main-content {
height: calc(100vh - 44px - 80px);
padding: 0 16px; width:92%
}
// 生成中提示
.generating-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
.generating-animation {
display: flex;
gap: 12px;
margin-bottom: 24px;
.dot {
width: 12px;
height: 12px;
background: #42ca4d;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
&.dot-1 {
animation-delay: -0.32s;
}
&.dot-2 {
animation-delay: -0.16s;
}
&.dot-3 {
animation-delay: 0s;
}
}
}
.generating-text {
font-size: 16px;
color: #ffffff;
margin-bottom: 8px;
}
.generating-time {
font-size: 13px;
color: #8f9bb3;
}
}
// 结果区域
.result-section {
padding-top: 12px;
}
// 单张图片容器
.single-image-container {
width: 100%;
margin-bottom: 12px;
.image-item {
width: 100%;
min-height: 200px;
border-radius: 16px;
overflow: hidden;
position: relative;
background: rgba(61, 68, 88, 0.3);
.result-image {
width: 100%;
display: block;
&.loading {
opacity: 0.3;
}
}
.image-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top-color: #42ca4d;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
}
.image-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 12px;
max-width: 80%;
.error-icon {
font-size: 48px;
margin-bottom: 12px;
}
.error-text {
font-size: 14px;
color: #ff6b6b;
margin-bottom: 8px;
}
.error-url {
font-size: 11px;
color: #8f9bb3;
word-break: break-all;
text-align: center;
}
}
.image-tags {
position: absolute;
bottom: 12px;
left: 12px;
display: flex;
gap: 8px;
.tag {
padding: 6px 12px;
background: rgba(0, 0, 0, 0.7);
border-radius: 6px;
font-size: 12px;
color: #ffffff;
backdrop-filter: blur(10px);
}
}
}
}
// 调试信息
.debug-info {
background: rgba(61, 68, 88, 0.3);
border: 2px solid #42ca4d;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
.debug-title {
display: block;
font-size: 14px;
font-weight: 600;
color: #42ca4d;
margin-bottom: 12px;
}
.debug-text {
display: block;
font-size: 12px;
color: #c5cee0;
line-height: 1.8;
margin-bottom: 6px;
word-break: break-all;
}
.debug-btn {
margin-top: 12px;
margin-right: 8px;
padding: 8px 16px;
background: #42ca4d;
color: #ffffff;
border: none;
border-radius: 6px;
font-size: 12px;
}
}
// 信息区域
.info-section {
background: rgba(61, 68, 88, 0.3);
border-radius: 16px;
padding: 16px;
margin-bottom: 16px;
.info-header {
display: flex;
align-items: center;
justify-content: space-between;
.info-title {
font-size: 15px;
font-weight: 500;
color: #ffffff;
}
.iconfont {
font-size: 14px;
color: #8f9bb3;
}
}
.prompt-content {
margin-top: 12px;
.prompt-text {
display: block;
font-size: 14px;
color: #c5cee0;
line-height: 1.6;
margin-bottom: 12px;
}
.prompt-input {
width: 100%;
min-height: 100px;
padding: 12px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(143, 155, 179, 0.3);
border-radius: 8px;
font-size: 14px;
color: #ffffff;
line-height: 1.6;
box-sizing: border-box;
&:focus {
border-color: #42ca4d;
background: rgba(0, 0, 0, 0.3);
}
}
.info-meta {
display: flex;
align-items: center;
gap: 8px;
.meta-item {
font-size: 12px;
color: #8f9bb3;
}
.meta-divider {
color: #4a5568;
}
}
}
}
// 配置选项
.config-section {
.config-item {
margin-bottom: 20px;
}
}
// 比例选择区域
.ratio-section {
background: rgba(61, 68, 88, 0.3);
border-radius: 16px;
padding: 16px;
margin-bottom: 16px;
.ratio-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.ratio-title-wrapper {
display: flex;
align-items: center;
gap: 8px;
.ratio-title {
font-size: 15px;
font-weight: 500;
color: #ffffff;
}
.ratio-current {
font-size: 14px;
color: #42ca4d;
font-weight: 500;
}
}
.iconfont {
font-size: 14px;
color: #8f9bb3;
}
}
.ratio-content {
margin-top: 12px;
}
}
// 比例列表
.ratio-list {
display: flex;
gap: 12px;
.ratio-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px;
background: rgba(61, 68, 88, 0.3);
border-radius: 12px;
border: 2px solid transparent;
.ratio-box {
width: 32px;
background: #8f9bb3;
border-radius: 4px;
margin-bottom: 6px;
}
.ratio-text {
font-size: 12px;
color: #8f9bb3;
}
&.active {
background: rgba(6, 67, 14, 0.1);
border-color: #42ca4d;
.ratio-box {
background: #42ca4d;
}
.ratio-text {
color: #42ca4d;
}
}
}
}
// 联想词助手
.assistant-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 11px 16px;
background: rgba(61, 68, 88, 0.3);
border-radius: 12px;
.assistant-info {
flex: 1;
.assistant-title {
display: block;
font-size: 15px;
color: #ffffff;
margin-bottom: 4px;
}
.assistant-desc {
display: block;
font-size: 12px;
color: #8f9bb3;
}
}
}
// 操作按钮
.action-section {
display: flex;
gap: 12px;
margin-bottom: 16px;
.action-btn {
flex: 1;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 12px;
border: none;
&.primary {
background: linear-gradient(135deg, #42ca4d 0%, #38b045 100%);
.btn-text {
color: #ffffff;
font-weight: 500;
}
}
.btn-text {
font-size: 16px;
}
}
}
// 底部占位
.bottom-placeholder {
height: 20px;
}
// 底部操作栏
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 12px;
padding: 16px;
background: linear-gradient(0deg, rgba(10, 14, 26, 0.95) 0%, rgba(10, 14, 26, 0.8) 100%);
backdrop-filter: blur(20px);
.footer-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 48px;
border-radius: 12px;
border: none;
&.secondary {
flex: 0 0 80px;
background: rgba(61, 68, 88, 0.4);
.iconfont,
.btn-text {
color: #ffffff;
}
}
&.primary {
flex: 1;
background: linear-gradient(135deg, #42ca4d 0%, #38b045 100%);
.btn-text {
color: #ffffff;
font-weight: 500;
}
}
.iconfont {
font-size: 18px;
}
.btn-text {
font-size: 14px;
}
}
}
// 图片预览
.preview-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.95);
z-index: 9999;
.preview-swiper {
width: 100%;
height: 100%;
.swiper-item-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.preview-full-image {
width: 100%;
height: 100%;
}
}
}
.preview-toolbar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0%, transparent 100%);
.toolbar-left {
.page-indicator {
font-size: 14px;
color: #ffffff;
}
}
.toolbar-right {
display: flex;
gap: 20px;
.tool-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10px);
.iconfont {
font-size: 20px;
color: #ffffff;
}
}
}
}
}
// 动画
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>