|
|
@@ -0,0 +1,243 @@
|
|
|
+<template>
|
|
|
+ <div class="product-image-generation">
|
|
|
+ <div class="two-column-layout">
|
|
|
+ <div class="left-column">
|
|
|
+ <el-card class="config-card" shadow="hover">
|
|
|
+ <template #header><span class="card-header"><el-icon><Setting /></el-icon>生成配置</span></template>
|
|
|
+
|
|
|
+ <div class="config-body">
|
|
|
+ <el-form :model="formData" label-position="top" size="small" class="config-form">
|
|
|
+ <!-- 产品图+参考图 并排正方形 -->
|
|
|
+ <el-form-item class="upload-row">
|
|
|
+ <div class="upload-row-inner">
|
|
|
+ <div v-for="item in uploadConfig" :key="item.key" class="upload-cell">
|
|
|
+ <span class="upload-label">{{ item.label }}<span v-if="item.required" class="required"> *</span></span>
|
|
|
+ <el-upload
|
|
|
+ class="image-uploader square"
|
|
|
+ :action="`${uploadPath}/fileUploadAndDownload/upload`"
|
|
|
+ :headers="{ 'x-token': userStore.token }"
|
|
|
+ :show-file-list="false"
|
|
|
+ :on-success="(r) => handleUploadSuccess(item.key, r)"
|
|
|
+ :before-upload="beforeImageUpload"
|
|
|
+ accept="image/jpeg,image/png,image/webp"
|
|
|
+ >
|
|
|
+ <div v-if="formData[item.key]" class="uploaded-preview">
|
|
|
+ <el-image :src="formatImageUrl(formData[item.key])" fit="contain" class="preview-image">
|
|
|
+ <template #error><div class="image-error"><el-icon><Picture /></el-icon></div></template>
|
|
|
+ </el-image>
|
|
|
+ <div class="upload-mask"><el-icon><ZoomIn /></el-icon><span>更换</span></div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="upload-placeholder">
|
|
|
+ <el-icon :size="20"><Plus /></el-icon>
|
|
|
+ <span>{{ item.placeholder }}</span>
|
|
|
+ </div>
|
|
|
+ </el-upload>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="提示词" required>
|
|
|
+ <el-input v-model="formData.prompt" type="textarea" :rows="5" placeholder="请输入对效果图的描述" maxlength="500" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 尺寸+模型 紧凑同一行(红框区域) -->
|
|
|
+ <el-form-item class="config-row-compact">
|
|
|
+ <div class="config-row-inner">
|
|
|
+ <div class="config-item">
|
|
|
+ <span class="config-label">尺寸</span>
|
|
|
+ <el-radio-group v-model="formData.size" size="small" class="size-radio-group">
|
|
|
+ <el-radio v-for="item in sizeOptions" :key="item.value" :label="item.value" border>{{ item.label }}</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </div>
|
|
|
+ <div class="config-item model-item">
|
|
|
+ <span class="config-label">模型</span>
|
|
|
+ <el-select v-model="formData.model" placeholder="请选择模型" size="small" class="model-select" filterable>
|
|
|
+ <el-option v-for="item in modelList" :key="item.id" :label="item.name" :value="item.name" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <div class="action-buttons-sticky">
|
|
|
+ <el-button type="success" :loading="isOptimizing" @click="optimizePrompt">
|
|
|
+ <el-icon><MagicStick /></el-icon>{{ isOptimizing ? '优化中...' : '优化提示词' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" :loading="isGenerating" @click="generateImage">
|
|
|
+ <el-icon><PictureFilled /></el-icon>{{ isGenerating ? '生成中...' : '生成效果图' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="right-column">
|
|
|
+ <el-card class="result-card" shadow="hover">
|
|
|
+ <template #header><span class="card-header"><el-icon><Picture /></el-icon>生成效果图</span></template>
|
|
|
+ <div class="result-area">
|
|
|
+ <div v-if="generatedImageUrl" class="result-preview">
|
|
|
+ <el-image :src="formatImageUrl(generatedImageUrl)" fit="contain" class="result-image" :preview-src-list="[formatImageUrl(generatedImageUrl)]" preview-teleported>
|
|
|
+ <template #error><div class="image-error-large"><el-icon :size="48"><Picture /></el-icon><span>图片加载失败</span></div></template>
|
|
|
+ </el-image>
|
|
|
+ <el-button type="primary" size="small" @click="downloadImage"><el-icon><Download /></el-icon>下载</el-button>
|
|
|
+ </div>
|
|
|
+ <div v-else class="result-empty">
|
|
|
+ <el-icon :size="60" class="empty-icon"><Picture /></el-icon>
|
|
|
+ <p>效果图将在此处显示</p>
|
|
|
+ <span>上传产品图、填写提示词后点击「生成效果图」</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { useUserStore } from '@/pinia/modules/user'
|
|
|
+import { Setting, Picture, Plus, ZoomIn, MagicStick, PictureFilled, Download } from '@element-plus/icons-vue'
|
|
|
+
|
|
|
+defineOptions({ name: 'ProductImageGeneration' })
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+const uploadPath = ref(import.meta.env.VITE_BASE_API)
|
|
|
+const uploadConfig = [
|
|
|
+ { key: 'productImageUrl', label: '产品图', placeholder: '上传产品图', required: true },
|
|
|
+ { key: 'referenceImageUrl', label: '参考图(可选)', placeholder: '上传参考图', required: false },
|
|
|
+]
|
|
|
+
|
|
|
+const formData = reactive({ productImageUrl: '', referenceImageUrl: '', prompt: '', size: '1:1', model: '' })
|
|
|
+const sizeOptions = [
|
|
|
+ { label: '1:1', value: '1:1' }, { label: '4:3', value: '4:3' }, { label: '3:2', value: '3:2' },
|
|
|
+ { label: '2:3', value: '2:3' }, { label: '16:9', value: '16:9' }, { label: '9:16', value: '9:16' },
|
|
|
+]
|
|
|
+const modelList = ref([{ id: 1, name: '默认模型' }])
|
|
|
+const isGenerating = ref(false)
|
|
|
+const isOptimizing = ref(false)
|
|
|
+const generatedImageUrl = ref('')
|
|
|
+
|
|
|
+const formatImageUrl = (path) => {
|
|
|
+ if (!path || typeof path !== 'string') return ''
|
|
|
+ if (path.startsWith('http://') || path.startsWith('https://')) return path
|
|
|
+ const base = import.meta.env.VITE_BASE_API
|
|
|
+ return `${base.endsWith('/') ? base : base + '/'}${path.startsWith('/') ? path.slice(1) : path}`
|
|
|
+}
|
|
|
+
|
|
|
+const beforeImageUpload = (file) => {
|
|
|
+ const ok = ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)
|
|
|
+ if (!ok) { ElMessage.error('仅支持 jpg、png、webp 格式'); return false }
|
|
|
+ if (file.size / 1024 / 1024 >= 5) { ElMessage.error('图片大小不能超过 5MB'); return false }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
+const handleUploadSuccess = (key, res) => {
|
|
|
+ if (res?.data?.file?.url) { formData[key] = res.data.file.url; ElMessage.success('上传成功') }
|
|
|
+}
|
|
|
+
|
|
|
+const optimizePrompt = async () => {
|
|
|
+ if (!formData.prompt) { ElMessage.warning('请先输入提示词'); return }
|
|
|
+ isOptimizing.value = true
|
|
|
+ try {
|
|
|
+ await new Promise(r => setTimeout(r, 1000))
|
|
|
+ ElMessage.success('提示词优化完成(演示)')
|
|
|
+ } catch { ElMessage.error('优化失败') }
|
|
|
+ finally { isOptimizing.value = false }
|
|
|
+}
|
|
|
+
|
|
|
+const generateImage = async () => {
|
|
|
+ if (!formData.productImageUrl) { ElMessage.warning('请先上传产品图'); return }
|
|
|
+ if (!formData.prompt) { ElMessage.warning('请填写提示词'); return }
|
|
|
+ isGenerating.value = true
|
|
|
+ generatedImageUrl.value = ''
|
|
|
+ try {
|
|
|
+ await new Promise(r => setTimeout(r, 2000))
|
|
|
+ generatedImageUrl.value = formData.productImageUrl
|
|
|
+ ElMessage.success('效果图生成成功')
|
|
|
+ } catch { ElMessage.error('生成失败') }
|
|
|
+ finally { isGenerating.value = false }
|
|
|
+}
|
|
|
+
|
|
|
+const downloadImage = () => {
|
|
|
+ if (!generatedImageUrl.value) return
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = formatImageUrl(generatedImageUrl.value)
|
|
|
+ link.download = `效果图_${Date.now()}.png`
|
|
|
+ link.target = '_blank'
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ ElMessage.success('下载中')
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.product-image-generation { padding: 6px 10px; height: calc(100vh - 200px); max-height: calc(100vh - 200px); min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
+.two-column-layout { display: flex; gap: 10px; flex: 1; min-height: 0; overflow: hidden; }
|
|
|
+.left-column { flex: 0 0 380px; min-width: 280px; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
+.right-column { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
+
|
|
|
+.config-card, .result-card {
|
|
|
+ flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0;
|
|
|
+ :deep(.el-card__header) { padding: 5px 10px; flex-shrink: 0; }
|
|
|
+ :deep(.el-card__body) { padding: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
+}
|
|
|
+.result-card :deep(.el-card__body) { padding: 8px; }
|
|
|
+.card-header { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; }
|
|
|
+
|
|
|
+.config-body { flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
+.config-form { flex: 1; min-height: 0; padding: 6px 10px; overflow: hidden; :deep(.el-form-item) { margin-bottom: 4px; } }
|
|
|
+.action-buttons-sticky { flex-shrink: 0; display: flex; gap: 8px; padding: 6px 10px; background: #fff; border-top: 1px solid #ebeef5; }
|
|
|
+
|
|
|
+/* 产品图+参考图 并排正方形,尺寸加大 */
|
|
|
+.upload-row-inner { display: flex; gap: 10px; width: 100%; }
|
|
|
+.upload-cell { flex: 1; min-width: 0; max-width: 140px; display: flex; flex-direction: column; gap: 4px; }
|
|
|
+.upload-label { font-size: 12px; color: #606266; .required { color: #f56c6c; } }
|
|
|
+.image-uploader.square {
|
|
|
+ width: 100%;
|
|
|
+ :deep(.el-upload) { width: 100%; display: block; }
|
|
|
+ .upload-placeholder, .uploaded-preview {
|
|
|
+ width: 100%; aspect-ratio: 1; height: auto !important; min-height: 80px; max-height: 130px; max-width: 130px; margin: 0 auto;
|
|
|
+ border: 1px dashed #d9d9d9; border-radius: 6px; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
|
+ cursor: pointer; transition: all 0.2s; background: #fafafa; overflow: hidden;
|
|
|
+ &:hover { border-color: #409eff; background: #f0f9ff; }
|
|
|
+ }
|
|
|
+ .upload-placeholder { color: #8c939d; span { font-size: 11px; margin-top: 2px; } }
|
|
|
+ .uploaded-preview {
|
|
|
+ position: relative; .preview-image { width: 100%; height: 100%; }
|
|
|
+ .upload-mask { position: absolute; inset: 0; background: rgba(0,0,0,0.4); display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; opacity: 0; transition: opacity 0.2s; span { font-size: 11px; margin-top: 2px; } }
|
|
|
+ &:hover .upload-mask { opacity: 1; }
|
|
|
+ }
|
|
|
+}
|
|
|
+.image-error { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f5f7fa; color: #909399; }
|
|
|
+
|
|
|
+/* 尺寸+模型 紧凑同一行(红框) */
|
|
|
+.config-row-compact { margin-bottom: 0 !important; }
|
|
|
+.config-row-inner { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
|
|
|
+.config-item { display: flex; flex-direction: column; gap: 4px; }
|
|
|
+.config-label { font-size: 12px; color: #606266; }
|
|
|
+.model-item { flex: 0 0 100px; }
|
|
|
+.model-select { width: 100%; min-width: 100px; }
|
|
|
+.size-radio-group { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
|
+
|
|
|
+.result-area { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; background: #f9fafb; border-radius: 6px; padding: 6px; }
|
|
|
+.result-preview { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; }
|
|
|
+.result-image { max-width: 100%; max-height: 100%; flex: 1; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
|
|
+.result-empty { text-align: center; color: #909399; .empty-icon { color: #dcdfe6; margin-bottom: 6px; } p { margin: 0 0 4px; font-size: 13px; } span { font-size: 11px; color: #c0c4cc; } }
|
|
|
+.image-error-large { width: 100%; flex: 1; min-height: 120px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #f5f7fa; color: #909399; gap: 8px; }
|
|
|
+
|
|
|
+@media (max-width: 900px) {
|
|
|
+ .two-column-layout { flex-direction: column; }
|
|
|
+ .left-column { flex: 1; min-width: 100%; min-height: 0; }
|
|
|
+ .right-column { flex: 1; min-height: 0; }
|
|
|
+}
|
|
|
+@media (max-width: 600px) {
|
|
|
+ .product-image-generation { padding: 4px 8px; height: calc(100vh - 200px); max-height: calc(100vh - 200px); }
|
|
|
+ .left-column { max-height: 55vh; }
|
|
|
+ .upload-row-inner { flex-direction: column; align-items: flex-start; }
|
|
|
+ .upload-cell { flex: 0 0 auto; width: 110px; }
|
|
|
+ .upload-cell { max-width: 110px; }
|
|
|
+ .image-uploader.square .upload-placeholder, .image-uploader.square .uploaded-preview { min-height: 70px; max-height: 100px; max-width: 100px; }
|
|
|
+}
|
|
|
+</style>
|