1068 lines
32 KiB
Vue
1068 lines
32 KiB
Vue
<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>
|
||
|