Files
ruoyi-vue/src/views/system/marketing-image/index.vue
2025-11-29 22:56:40 +08:00

1068 lines
32 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>
<div class="marketing-image-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span class="card-title">
<i class="el-icon-picture"></i>
营销图片合成工具
</span>
<el-button
style="float: right; padding: 3px 0"
type="text"
@click="showHelp = !showHelp">
{{ showHelp ? '隐藏帮助' : '显示帮助' }}
</el-button>
</div>
<!-- 帮助说明 -->
<el-collapse-transition>
<div v-show="showHelp" class="help-section">
<el-alert
title="使用说明"
type="info"
:closable="false"
show-icon>
<div slot="default">
<p><strong>功能说明</strong></p>
<ul>
<li>自动将商品主图和价格信息合成为营销对比图</li>
<li>支持单张生成和批量生成</li>
<li>输出尺寸1080x1080适合小红书等平台</li>
<li>支持从解析接口结果中快速导入商品信息</li>
</ul>
<p><strong>使用步骤</strong></p>
<ol>
<li>方式一手动输入商品信息图片URL原价到手价</li>
<li>方式二从解析接口结果中导入点击"从解析结果导入"按钮</li>
<li>点击"生成图片"按钮</li>
<li>预览生成的图片可以下载或复制</li>
</ol>
</div>
</el-alert>
</div>
</el-collapse-transition>
<!-- 操作区域 -->
<div class="operation-section">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<!-- 单张生成 -->
<el-tab-pane label="单张生成" name="single">
<el-form :model="singleForm" label-width="120px" class="single-form">
<el-form-item label="商品主图URL" required>
<el-input
v-model="singleForm.productImageUrl"
placeholder="请输入商品主图URL支持京东图片链接"
clearable>
<el-button slot="append" @click="handlePreviewImage(singleForm.productImageUrl)" :disabled="!singleForm.productImageUrl">
预览
</el-button>
</el-input>
</el-form-item>
<el-form-item label="商品名称" required>
<el-input
v-model="singleForm.productName"
placeholder="请输入商品名称(可选,系统会自动提取关键部分)"
clearable>
</el-input>
<div class="form-tip">留空则系统自动提取关键部分</div>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="官网价" required>
<el-input-number
v-model="singleForm.originalPrice"
:min="0"
:precision="2"
:step="1"
placeholder="官网原价"
style="width: 100%">
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="到手价" required>
<el-input-number
v-model="singleForm.finalPrice"
:min="0"
:precision="2"
:step="1"
placeholder="到手价"
style="width: 100%">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button
type="primary"
icon="el-icon-magic-stick"
@click="handleGenerateSingle"
:loading="generating">
生成图片
</el-button>
<el-button
icon="el-icon-refresh-left"
@click="handleResetSingle">
重置
</el-button>
<el-button
type="info"
icon="el-icon-download"
@click="handleImportFromParse">
从解析结果导入
</el-button>
<el-button
type="warning"
icon="el-icon-connection"
@click="showParseDialog = true">
调用解析接口
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 批量生成 -->
<el-tab-pane label="批量生成" name="batch">
<div class="batch-section">
<div class="batch-toolbar">
<el-button
type="primary"
icon="el-icon-plus"
@click="handleAddBatchItem">
添加商品
</el-button>
<el-button
type="success"
icon="el-icon-upload2"
@click="handleImportFromParseBatch">
从解析结果批量导入
</el-button>
<el-button
type="warning"
icon="el-icon-connection"
@click="showParseDialog = true">
调用解析接口
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
@click="handleClearBatch"
:disabled="batchList.length === 0">
清空列表
</el-button>
<el-button
type="primary"
icon="el-icon-magic-stick"
@click="handleGenerateBatch"
:loading="batchGenerating"
:disabled="batchList.length === 0">
批量生成 ({{ batchList.length }})
</el-button>
</div>
<!-- 批量列表 -->
<el-table
:data="batchList"
border
stripe
style="width: 100%; margin-top: 20px;"
max-height="500">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="商品主图" width="120" align="center">
<template slot-scope="scope">
<el-image
:src="scope.row.productImageUrl"
:preview-src-list="[scope.row.productImageUrl]"
fit="cover"
style="width: 80px; height: 80px; cursor: pointer;"
:lazy="true">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</template>
</el-table-column>
<el-table-column label="商品名称" prop="productName" min-width="200">
<template slot-scope="scope">
<el-input
v-model="scope.row.productName"
placeholder="商品名称(可选)"
size="small">
</el-input>
</template>
</el-table-column>
<el-table-column label="官网价" width="120" align="center">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.originalPrice"
:min="0"
:precision="2"
size="small"
style="width: 100%">
</el-input-number>
</template>
</el-table-column>
<el-table-column label="到手价" width="120" align="center">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.finalPrice"
:min="0"
:precision="2"
size="small"
style="width: 100%">
</el-input-number>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center" fixed="right">
<template slot-scope="scope">
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="handleRemoveBatchItem(scope.$index)">
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- 预览区域 -->
<div v-if="previewImages.length > 0" class="preview-section">
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold;">
<i class="el-icon-view"></i>
生成结果预览 ({{ previewImages.length }})
</span>
</el-divider>
<div class="preview-grid">
<div
v-for="(item, index) in previewImages"
:key="index"
class="preview-item">
<div class="preview-card">
<div class="preview-image-wrapper">
<img
:src="item.imageBase64"
alt="营销图片"
class="preview-image"
@click="handlePreviewLargeImage(item.imageBase64)">
<div class="preview-overlay">
<el-button-group>
<el-button
type="primary"
size="mini"
icon="el-icon-download"
@click="handleDownloadImage(item.imageBase64, item.filename || `营销图片_${index + 1}.jpg`)">
下载
</el-button>
<el-button
type="success"
size="mini"
icon="el-icon-document-copy"
@click="handleCopyImage(item.imageBase64)">
复制
</el-button>
</el-button-group>
</div>
</div>
<div class="preview-info">
<div v-if="item.productName" class="preview-name">{{ item.productName }}</div>
<div class="preview-price">
<span class="original-price">¥{{ item.originalPrice }}</span>
<span class="arrow"></span>
<span class="final-price">¥{{ item.finalPrice }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="preview-actions">
<el-button
type="primary"
icon="el-icon-download"
@click="handleDownloadAll">
下载全部 ({{ previewImages.length }})
</el-button>
<el-button
icon="el-icon-delete"
@click="handleClearPreview">
清空预览
</el-button>
</div>
</div>
</el-card>
<!-- 大图预览对话框 -->
<el-dialog
title="图片预览"
:visible.sync="previewDialogVisible"
width="80%"
:before-close="handleClosePreview">
<div class="large-preview">
<img
:src="previewLargeImageUrl"
alt="预览"
style="max-width: 100%; height: auto;">
</div>
</el-dialog>
<!-- 解析接口对话框 -->
<el-dialog
title="调用解析接口"
:visible.sync="showParseDialog"
width="70%"
:close-on-click-modal="false">
<div>
<el-alert
title="输入格式:每行一个商品,格式为:京东短链接 + Tab/空格 + 到手价"
type="info"
:closable="false"
style="margin-bottom: 20px;">
<div slot="default">
<p>示例</p>
<pre style="background: #f5f7fa; padding: 10px; border-radius: 4px; margin-top: 10px;">https://u.jd.com/W17zHk2 199
https://u.jd.com/W17zcSF 349</pre>
</div>
</el-alert>
<el-input
v-model="parseText"
type="textarea"
:rows="10"
placeholder="请输入商品链接和价格,每行一个..."
style="margin-bottom: 20px;">
</el-input>
<div slot="footer" class="dialog-footer">
<el-button @click="showParseDialog = false">取消</el-button>
<el-button
type="primary"
@click="handleCallParse"
:loading="parsing">
解析
</el-button>
</div>
</div>
</el-dialog>
<!-- 从解析结果导入对话框 -->
<el-dialog
title="从解析结果导入"
:visible.sync="importDialogVisible"
width="80%"
:close-on-click-modal="false">
<div v-if="parseResultData && parseResultData.length > 0">
<el-alert
title="选择要导入的商品"
type="info"
:closable="false"
style="margin-bottom: 20px;">
</el-alert>
<el-table
:data="parseResultData"
border
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="商品图片" width="100" align="center">
<template slot-scope="scope">
<el-image
:src="scope.row.productImage"
fit="cover"
style="width: 60px; height: 60px;"
:lazy="true">
</el-image>
</template>
</el-table-column>
<el-table-column label="商品名称" prop="productName" min-width="200" show-overflow-tooltip />
<el-table-column label="官网价" prop="price" width="100" align="center">
<template slot-scope="scope">
¥{{ scope.row.price }}
</template>
</el-table-column>
<el-table-column label="到手价" width="120" align="center">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.finalPrice"
:min="0"
:precision="2"
size="small"
style="width: 100%"
placeholder="请输入">
</el-input-number>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button
type="primary"
@click="handleConfirmImport"
:disabled="selectedItems.length === 0">
导入选中项 ({{ selectedItems.length }})
</el-button>
</div>
</div>
<div v-else>
<el-alert
title="暂无解析结果数据"
type="warning"
:closable="false">
<div slot="default">
请先调用解析接口获取商品数据然后才能导入
</div>
</el-alert>
</div>
</el-dialog>
</div>
</template>
<script>
import { generateMarketingImage, batchGenerateMarketingImages } from '@/api/jarvis/marketingImage'
import { parseLineReport } from '@/api/jarvis/batchPublish'
export default {
name: 'MarketingImage',
data() {
return {
showHelp: false,
activeTab: 'single',
// 单张生成表单
singleForm: {
productImageUrl: '',
productName: '',
originalPrice: null,
finalPrice: null
},
// 批量生成列表
batchList: [],
// 预览图片列表
previewImages: [],
// 加载状态
generating: false,
batchGenerating: false,
// 预览对话框
previewDialogVisible: false,
previewLargeImageUrl: '',
// 导入对话框
importDialogVisible: false,
parseResultData: [],
selectedItems: [],
// 解析接口对话框
showParseDialog: false,
parseText: '',
parsing: false
}
},
mounted() {
// 尝试从localStorage获取解析结果
this.loadParseResultFromStorage()
},
methods: {
/** 切换标签 */
handleTabClick(tab) {
// 可以在这里添加切换逻辑
},
/** 生成单张图片 */
async handleGenerateSingle() {
// 验证表单
if (!this.singleForm.productImageUrl) {
this.$message.warning('请输入商品主图URL')
return
}
if (this.singleForm.originalPrice === null || this.singleForm.originalPrice <= 0) {
this.$message.warning('请输入有效的官网价')
return
}
if (this.singleForm.finalPrice === null || this.singleForm.finalPrice <= 0) {
this.$message.warning('请输入有效的到手价')
return
}
this.generating = true
try {
const res = await generateMarketingImage({
productImageUrl: this.singleForm.productImageUrl,
productName: this.singleForm.productName || undefined,
originalPrice: this.singleForm.originalPrice,
finalPrice: this.singleForm.finalPrice
})
if (res.code === 200 && res.data && res.data.imageBase64) {
this.previewImages.push({
imageBase64: res.data.imageBase64,
productName: this.singleForm.productName,
originalPrice: this.singleForm.originalPrice,
finalPrice: this.singleForm.finalPrice,
filename: `营销图片_${this.singleForm.productName || '商品'}_${Date.now()}.jpg`
})
this.$message.success('图片生成成功!')
// 滚动到预览区域
this.$nextTick(() => {
const previewSection = document.querySelector('.preview-section')
if (previewSection) {
previewSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
})
} else {
this.$message.error(res.msg || '生成失败')
}
} catch (error) {
console.error('生成图片失败', error)
this.$message.error('生成图片失败:' + (error.message || '未知错误'))
} finally {
this.generating = false
}
},
/** 重置单张表单 */
handleResetSingle() {
this.singleForm = {
productImageUrl: '',
productName: '',
originalPrice: null,
finalPrice: null
}
},
/** 添加批量项 */
handleAddBatchItem() {
this.batchList.push({
productImageUrl: '',
productName: '',
originalPrice: null,
finalPrice: null
})
},
/** 移除批量项 */
handleRemoveBatchItem(index) {
this.batchList.splice(index, 1)
},
/** 清空批量列表 */
handleClearBatch() {
this.$confirm('确定要清空批量列表吗?', '提示', {
type: 'warning'
}).then(() => {
this.batchList = []
this.$message.success('已清空')
}).catch(() => {})
},
/** 批量生成 */
async handleGenerateBatch() {
// 验证列表
const validList = this.batchList.filter(item =>
item.productImageUrl &&
item.originalPrice !== null && item.originalPrice > 0 &&
item.finalPrice !== null && item.finalPrice > 0
)
if (validList.length === 0) {
this.$message.warning('请至少添加一个有效的商品信息')
return
}
if (validList.length < this.batchList.length) {
this.$message.warning(`${this.batchList.length - validList.length} 个商品信息不完整,将跳过`)
}
this.batchGenerating = true
try {
const requests = validList.map(item => ({
productImageUrl: item.productImageUrl,
productName: item.productName || undefined,
originalPrice: item.originalPrice,
finalPrice: item.finalPrice
}))
const res = await batchGenerateMarketingImages({ requests })
if (res.code === 200 && res.data && res.data.results) {
const results = res.data.results
let successCount = 0
results.forEach((result, index) => {
if (result.success && result.imageBase64) {
const item = validList[result.index]
this.previewImages.push({
imageBase64: result.imageBase64,
productName: item.productName,
originalPrice: item.originalPrice,
finalPrice: item.finalPrice,
filename: `营销图片_${item.productName || '商品'}_${Date.now()}_${index}.jpg`
})
successCount++
}
})
this.$message.success(`批量生成完成!成功:${successCount},失败:${results.length - successCount}`)
// 滚动到预览区域
this.$nextTick(() => {
const previewSection = document.querySelector('.preview-section')
if (previewSection) {
previewSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
})
} else {
this.$message.error(res.msg || '批量生成失败')
}
} catch (error) {
console.error('批量生成失败', error)
this.$message.error('批量生成失败:' + (error.message || '未知错误'))
} finally {
this.batchGenerating = false
}
},
/** 预览图片 */
handlePreviewImage(url) {
if (!url) return
this.previewLargeImageUrl = url
this.previewDialogVisible = true
},
/** 预览大图 */
handlePreviewLargeImage(base64) {
this.previewLargeImageUrl = base64
this.previewDialogVisible = true
},
/** 关闭预览 */
handleClosePreview() {
this.previewDialogVisible = false
this.previewLargeImageUrl = ''
},
/** 下载图片 */
handleDownloadImage(base64, filename) {
try {
// 将base64转换为blob
const base64Data = base64.split(',')[1] || base64
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'image/jpeg' })
// 创建下载链接
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
this.$message.success('下载成功')
} catch (error) {
console.error('下载失败', error)
this.$message.error('下载失败')
}
},
/** 复制图片 */
async handleCopyImage(base64) {
try {
// 将base64转换为blob
const base64Data = base64.split(',')[1] || base64
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'image/jpeg' })
// 使用Clipboard API复制
if (navigator.clipboard && navigator.clipboard.write) {
await navigator.clipboard.write([
new ClipboardItem({ 'image/jpeg': blob })
])
this.$message.success('图片已复制到剪贴板')
} else {
// 降级方案:提示用户手动保存
this.$message.info('浏览器不支持直接复制图片,请使用下载功能')
}
} catch (error) {
console.error('复制失败', error)
this.$message.error('复制失败')
}
},
/** 下载全部 */
handleDownloadAll() {
if (this.previewImages.length === 0) {
this.$message.warning('没有可下载的图片')
return
}
this.previewImages.forEach((item, index) => {
setTimeout(() => {
this.handleDownloadImage(item.imageBase64, item.filename || `营销图片_${index + 1}.jpg`)
}, index * 200) // 延迟下载,避免浏览器阻止
})
this.$message.success(`开始下载 ${this.previewImages.length} 张图片`)
},
/** 清空预览 */
handleClearPreview() {
this.$confirm('确定要清空所有预览图片吗?', '提示', {
type: 'warning'
}).then(() => {
this.previewImages = []
this.$message.success('已清空')
}).catch(() => {})
},
/** 从解析结果导入(单张) */
handleImportFromParse() {
this.loadParseResultFromStorage()
if (!this.parseResultData || this.parseResultData.length === 0) {
this.$message.warning('请先调用解析接口获取商品数据')
return
}
this.importDialogVisible = true
},
/** 从解析结果批量导入 */
handleImportFromParseBatch() {
this.loadParseResultFromStorage()
if (!this.parseResultData || this.parseResultData.length === 0) {
this.$message.warning('请先调用解析接口获取商品数据')
return
}
this.importDialogVisible = true
},
/** 选择变化 */
handleSelectionChange(selection) {
this.selectedItems = selection
},
/** 确认导入 */
handleConfirmImport() {
if (this.selectedItems.length === 0) {
this.$message.warning('请至少选择一个商品')
return
}
if (this.activeTab === 'single') {
// 单张模式:导入第一个选中的
const item = this.selectedItems[0]
this.singleForm = {
productImageUrl: item.productImage || '',
productName: item.productName || '',
originalPrice: item.price || null,
finalPrice: item.finalPrice || null
}
this.$message.success('导入成功')
} else {
// 批量模式:导入所有选中的
this.selectedItems.forEach(item => {
if (item.finalPrice && item.finalPrice > 0) {
this.batchList.push({
productImageUrl: item.productImage || '',
productName: item.productName || '',
originalPrice: item.price || null,
finalPrice: item.finalPrice || null
})
}
})
this.$message.success(`成功导入 ${this.selectedItems.length} 个商品`)
}
this.importDialogVisible = false
this.selectedItems = []
},
/** 从localStorage加载解析结果 */
loadParseResultFromStorage() {
try {
const stored = localStorage.getItem('parseResultData')
if (stored) {
const data = JSON.parse(stored)
if (Array.isArray(data) && data.length > 0) {
// 为每个商品添加finalPrice字段用于输入到手价
this.parseResultData = data.map(item => ({
...item,
finalPrice: null
}))
}
}
} catch (error) {
console.error('加载解析结果失败', error)
}
},
/** 调用解析接口 */
async handleCallParse() {
if (!this.parseText || !this.parseText.trim()) {
this.$message.warning('请输入要解析的内容')
return
}
this.parsing = true
try {
const res = await parseLineReport({
message: this.parseText.trim()
})
if (res.code === 200 && res.data && Array.isArray(res.data) && res.data.length > 0) {
// 保存解析结果到localStorage
localStorage.setItem('parseResultData', JSON.stringify(res.data))
// 为每个商品添加finalPrice字段从输入文本中提取
const lines = this.parseText.trim().split('\n')
const priceMap = new Map()
lines.forEach(line => {
const parts = line.trim().split(/\s+/)
if (parts.length >= 2) {
const url = parts[0]
const price = parseFloat(parts[1])
if (!isNaN(price)) {
priceMap.set(url, price)
}
}
})
this.parseResultData = res.data.map(item => {
// 尝试从原始URL匹配价格
let finalPrice = null
if (item._raw && item._raw.originalUrl) {
finalPrice = priceMap.get(item._raw.originalUrl) || null
}
return {
...item,
finalPrice: finalPrice || item.finalPrice || null
}
})
this.$message.success(`解析成功!共 ${this.parseResultData.length} 个商品`)
this.showParseDialog = false
// 自动打开导入对话框
this.$nextTick(() => {
this.importDialogVisible = true
})
} else {
this.$message.error(res.msg || '解析失败')
}
} catch (error) {
console.error('调用解析接口失败', error)
this.$message.error('解析失败:' + (error.message || '未知错误'))
} finally {
this.parsing = false
}
}
}
}
</script>
<style scoped>
.marketing-image-container {
padding: 20px;
}
.card-title {
font-size: 18px;
font-weight: bold;
}
.help-section {
margin-bottom: 20px;
}
.help-section ul,
.help-section ol {
margin: 10px 0;
padding-left: 20px;
}
.help-section li {
margin: 5px 0;
}
.operation-section {
margin-top: 20px;
}
.single-form {
max-width: 800px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.batch-section {
margin-top: 20px;
}
.batch-toolbar {
margin-bottom: 20px;
}
.batch-toolbar .el-button {
margin-right: 10px;
}
.preview-section {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #EBEEF5;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.preview-item {
position: relative;
}
.preview-card {
border: 1px solid #EBEEF5;
border-radius: 4px;
overflow: hidden;
background: #fff;
transition: all 0.3s;
}
.preview-card:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.preview-image-wrapper {
position: relative;
width: 100%;
padding-top: 100%; /* 1:1 比例 */
background: #f5f7fa;
overflow: hidden;
}
.preview-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
}
.preview-image-wrapper:hover .preview-overlay {
opacity: 1;
}
.preview-info {
padding: 15px;
}
.preview-name {
font-size: 14px;
color: #303133;
margin-bottom: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-price {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.original-price {
font-size: 16px;
color: #909399;
text-decoration: line-through;
}
.arrow {
color: #909399;
font-size: 14px;
}
.final-price {
font-size: 20px;
color: #F56C6C;
font-weight: bold;
}
.preview-actions {
margin-top: 20px;
text-align: center;
}
.large-preview {
text-align: center;
max-height: 70vh;
overflow: auto;
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f7fa;
color: #909399;
font-size: 20px;
}
.dialog-footer {
text-align: right;
margin-top: 20px;
}
</style>