- 将默认AI生图模型升级为flux-dev及seedream-5.0版本 - SiliconFlow模型由FLUX.1-dev切换为Kolors,优化调用参数和返回值 - 火山引擎Seedream升级至5.0 lite版本,支持多视角参考图传入 - 设计图片字段由字符串改为Text扩展URL长度限制 - 设计图下载支持远程URL重定向和本地文件兼容 - 生成AI图片时多视角保持风格一致,SiliconFlow复用seed,Seedream传参考图 - 后台配置界面更改模型名称及价格显示,新增API Key状态检测 - 前端照片下载从链接改为按钮,远程文件新窗口打开 - 设计相关接口支持较长请求超时,下载走API路径无/api前缀 - 前端页面兼容驼峰与下划线格式URL参数识别 - 用户中心设计图下载支持本地文件Token授权下载 - 初始化数据库新增完整表结构与约束,适配新版设计业务逻辑
507 lines
11 KiB
Vue
507 lines
11 KiB
Vue
<template>
|
||
<div class="design-preview">
|
||
<!-- 视角 Tab 栏(多图时显示) -->
|
||
<div class="view-tabs" v-if="hasMultipleViews">
|
||
<button
|
||
v-for="(img, idx) in design.images"
|
||
:key="img.id"
|
||
class="view-tab"
|
||
:class="{ active: activeViewIndex === idx }"
|
||
@click="activeViewIndex = idx"
|
||
>
|
||
{{ img.view_name }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 图片预览区 -->
|
||
<div class="preview-container">
|
||
<div class="image-wrapper" :style="{ transform: `scale(${scale})` }">
|
||
<el-image
|
||
:src="currentImageUrl"
|
||
:alt="design.prompt"
|
||
fit="contain"
|
||
:preview-src-list="allImageUrls"
|
||
:initial-index="activeViewIndex"
|
||
preview-teleported
|
||
class="design-image"
|
||
>
|
||
<template #placeholder>
|
||
<div class="image-placeholder">
|
||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||
<span>加载中...</span>
|
||
</div>
|
||
</template>
|
||
<template #error>
|
||
<div class="image-error">
|
||
<el-icon><PictureFilled /></el-icon>
|
||
<span>图片加载失败</span>
|
||
</div>
|
||
</template>
|
||
</el-image>
|
||
</div>
|
||
|
||
<!-- 视角指示器(多图时显示) -->
|
||
<div class="view-indicator" v-if="hasMultipleViews">
|
||
<span class="indicator-text">{{ activeViewName }} ({{ activeViewIndex + 1 }}/{{ design.images.length }})</span>
|
||
</div>
|
||
|
||
<!-- 缩放控制 -->
|
||
<div class="zoom-controls">
|
||
<button class="zoom-btn" @click="zoomOut" :disabled="scale <= 0.5">
|
||
<el-icon><ZoomOut /></el-icon>
|
||
</button>
|
||
<span class="zoom-level">{{ Math.round(scale * 100) }}%</span>
|
||
<button class="zoom-btn" @click="zoomIn" :disabled="scale >= 2">
|
||
<el-icon><ZoomIn /></el-icon>
|
||
</button>
|
||
<button class="zoom-btn reset-btn" @click="resetZoom" v-if="scale !== 1">
|
||
<el-icon><RefreshRight /></el-icon>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 设计信息 -->
|
||
<div class="design-info">
|
||
<h4 class="info-title">设计详情</h4>
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<span class="info-label">品类</span>
|
||
<span class="info-value">{{ design.category?.name || '-' }}</span>
|
||
</div>
|
||
<div class="info-item" v-if="design.sub_type">
|
||
<span class="info-label">类型</span>
|
||
<span class="info-value">{{ design.sub_type.name }}</span>
|
||
</div>
|
||
<div class="info-item" v-if="design.color">
|
||
<span class="info-label">颜色</span>
|
||
<span class="info-value">{{ design.color.name }}</span>
|
||
</div>
|
||
<div class="info-item full-width">
|
||
<span class="info-label">设计需求</span>
|
||
<span class="info-value prompt">{{ design.prompt }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="action-buttons">
|
||
<button
|
||
class="action-btn download-btn"
|
||
@click="handleDownload"
|
||
:disabled="downloading"
|
||
>
|
||
<el-icon><Download /></el-icon>
|
||
<span>{{ downloading ? '下载中...' : '下载设计图' }}</span>
|
||
</button>
|
||
<button class="action-btn secondary-btn" @click="goToUserCenter">
|
||
<el-icon><User /></el-icon>
|
||
<span>查看我的设计</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { Loading, PictureFilled, ZoomIn, ZoomOut, RefreshRight, Download, User } from '@element-plus/icons-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import type { Design } from '@/stores/design'
|
||
import { getDesignDownloadUrl } from '@/api/design'
|
||
import request from '@/api/request'
|
||
|
||
const props = defineProps<{
|
||
design: Design
|
||
}>()
|
||
|
||
const router = useRouter()
|
||
|
||
// 当前视角索引
|
||
const activeViewIndex = ref(0)
|
||
|
||
// 缩放比例
|
||
const scale = ref(1)
|
||
|
||
// 是否有多视角图片
|
||
const hasMultipleViews = computed(() => {
|
||
return props.design.images && props.design.images.length > 1
|
||
})
|
||
|
||
// 当前视角名称
|
||
const activeViewName = computed(() => {
|
||
if (props.design.images && props.design.images.length > 0) {
|
||
return props.design.images[activeViewIndex.value]?.view_name || ''
|
||
}
|
||
return ''
|
||
})
|
||
|
||
// 获取图片URL
|
||
const toImageUrl = (url: string | null): string => {
|
||
if (!url) return ''
|
||
if (url.startsWith('http')) return url
|
||
// /uploads 路径已由 Vite 代理到后端,不需要加 /api 前缀
|
||
if (url.startsWith('/uploads')) return url
|
||
return url
|
||
}
|
||
|
||
// 当前显示的图片URL
|
||
const currentImageUrl = computed(() => {
|
||
// 优先用多视角图片
|
||
if (props.design.images && props.design.images.length > 0) {
|
||
return toImageUrl(props.design.images[activeViewIndex.value]?.image_url)
|
||
}
|
||
// 兼容旧数据,使用单图
|
||
return toImageUrl(props.design.image_url)
|
||
})
|
||
|
||
// 所有图片URL(用于大图预览)
|
||
const allImageUrls = computed(() => {
|
||
if (props.design.images && props.design.images.length > 0) {
|
||
return props.design.images.map(img => toImageUrl(img.image_url))
|
||
}
|
||
return [toImageUrl(props.design.image_url)]
|
||
})
|
||
|
||
// 下载状态
|
||
const downloading = ref(false)
|
||
|
||
// 下载文件名
|
||
const downloadFilename = computed(() => {
|
||
const category = props.design.category?.name || '设计'
|
||
const subType = props.design.sub_type?.name || ''
|
||
const viewSuffix = hasMultipleViews.value ? `-${activeViewName.value}` : ''
|
||
return `${category}${subType ? '-' + subType : ''}${viewSuffix}-${props.design.id}.png`
|
||
})
|
||
|
||
// 下载设计图
|
||
const handleDownload = () => {
|
||
const imgUrl = currentImageUrl.value
|
||
if (!imgUrl) {
|
||
ElMessage.error('图片不存在')
|
||
return
|
||
}
|
||
// 远程 URL 直接新窗口打开(用户右键可保存)
|
||
if (imgUrl.startsWith('http')) {
|
||
window.open(imgUrl, '_blank')
|
||
return
|
||
}
|
||
// 本地文件通过 axios 携带 Token 下载
|
||
downloading.value = true
|
||
request.get(getDesignDownloadUrl(props.design.id), {
|
||
responseType: 'blob'
|
||
}).then((response: any) => {
|
||
const blob = new Blob([response], { type: 'image/png' })
|
||
const url = window.URL.createObjectURL(blob)
|
||
const link = document.createElement('a')
|
||
link.href = url
|
||
link.download = downloadFilename.value
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
window.URL.revokeObjectURL(url)
|
||
ElMessage.success('下载成功')
|
||
}).catch(() => {
|
||
ElMessage.error('下载失败,请重试')
|
||
}).finally(() => {
|
||
downloading.value = false
|
||
})
|
||
}
|
||
|
||
// 切换视角时重置缩放
|
||
watch(activeViewIndex, () => {
|
||
scale.value = 1
|
||
})
|
||
|
||
// 放大
|
||
const zoomIn = () => {
|
||
if (scale.value < 2) {
|
||
scale.value = Math.min(2, scale.value + 0.25)
|
||
}
|
||
}
|
||
|
||
// 缩小
|
||
const zoomOut = () => {
|
||
if (scale.value > 0.5) {
|
||
scale.value = Math.max(0.5, scale.value - 0.25)
|
||
}
|
||
}
|
||
|
||
// 重置缩放
|
||
const resetZoom = () => {
|
||
scale.value = 1
|
||
}
|
||
|
||
// 跳转到用户中心
|
||
const goToUserCenter = () => {
|
||
ElMessage.success('设计已自动保存到您的设计历史中')
|
||
router.push('/user')
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
$primary-color: #5B7E6B;
|
||
$primary-light: #8BAF9C;
|
||
$secondary-color: #C4A86C;
|
||
$bg-color: #FAF8F5;
|
||
$bg-dark: #F0EDE8;
|
||
$border-color: #E8E4DF;
|
||
$text-primary: #2C2C2C;
|
||
$text-secondary: #6B6B6B;
|
||
$text-light: #999999;
|
||
|
||
.design-preview {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24px;
|
||
}
|
||
|
||
.view-tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 4px;
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.view-tab {
|
||
flex: 1;
|
||
padding: 10px 16px;
|
||
background: transparent;
|
||
border: 1px solid transparent;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
color: $text-secondary;
|
||
cursor: pointer;
|
||
transition: all 0.25s ease;
|
||
letter-spacing: 1px;
|
||
|
||
&:hover {
|
||
color: $primary-color;
|
||
background: rgba($primary-color, 0.05);
|
||
}
|
||
|
||
&.active {
|
||
background: $primary-color;
|
||
color: #fff;
|
||
border-color: $primary-color;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.view-indicator {
|
||
position: absolute;
|
||
top: 16px;
|
||
left: 16px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.indicator-text {
|
||
font-size: 12px;
|
||
color: #fff;
|
||
}
|
||
|
||
.preview-container {
|
||
position: relative;
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.image-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 300px;
|
||
max-height: 500px;
|
||
transition: transform 0.3s ease;
|
||
transform-origin: center center;
|
||
}
|
||
|
||
.design-image {
|
||
max-width: 100%;
|
||
max-height: 450px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||
cursor: zoom-in;
|
||
|
||
:deep(img) {
|
||
max-width: 100%;
|
||
max-height: 450px;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
|
||
.image-placeholder,
|
||
.image-error {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
width: 300px;
|
||
height: 300px;
|
||
background: $bg-color;
|
||
border-radius: 8px;
|
||
color: $text-light;
|
||
|
||
.el-icon {
|
||
font-size: 48px;
|
||
}
|
||
}
|
||
|
||
.loading-icon {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.zoom-controls {
|
||
position: absolute;
|
||
bottom: 16px;
|
||
right: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.zoom-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
background: transparent;
|
||
border: 1px solid $border-color;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
color: $text-secondary;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: $primary-color;
|
||
border-color: $primary-color;
|
||
color: #fff;
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.zoom-level {
|
||
min-width: 48px;
|
||
text-align: center;
|
||
font-size: 13px;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.design-info {
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 20px 24px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.info-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: $text-primary;
|
||
margin: 0 0 16px 0;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid $border-color;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
&.full-width {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 12px;
|
||
color: $text-light;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 14px;
|
||
color: $text-primary;
|
||
|
||
&.prompt {
|
||
line-height: 1.6;
|
||
color: $text-secondary;
|
||
}
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.action-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 14px 28px;
|
||
border-radius: 8px;
|
||
font-size: 15px;
|
||
cursor: pointer;
|
||
transition: all 0.25s ease;
|
||
text-decoration: none;
|
||
letter-spacing: 1px;
|
||
|
||
.el-icon {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
|
||
.download-btn {
|
||
background: $primary-color;
|
||
color: #fff;
|
||
border: none;
|
||
|
||
&:hover {
|
||
background: #4a6a5a;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba($primary-color, 0.3);
|
||
}
|
||
}
|
||
|
||
.secondary-btn {
|
||
background: #fff;
|
||
color: $primary-color;
|
||
border: 1px solid $primary-color;
|
||
|
||
&:hover {
|
||
background: $primary-color;
|
||
color: #fff;
|
||
}
|
||
}
|
||
</style>
|