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

918 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>
<div class="social-media-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span class="card-title">
<i class="el-icon-star-on"></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>AI智能提取商品关键词</li>
<li>AI生成多种风格的推广文案</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="input-section">
<el-form :model="form" label-width="120px" class="main-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="商品主图URL" required>
<el-input
v-model="form.productImageUrl"
placeholder="请输入商品主图URL"
clearable>
<el-button slot="append" @click="handlePreviewImage(form.productImageUrl)" :disabled="!form.productImageUrl">
预览
</el-button>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="商品名称" required>
<el-input
v-model="form.productName"
placeholder="请输入完整商品名称"
clearable>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="官网价">
<el-input-number
v-model="form.originalPrice"
:min="0"
:precision="2"
:step="1"
placeholder="官网原价"
style="width: 100%">
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="到手价" required>
<el-input-number
v-model="form.finalPrice"
:min="0"
:precision="2"
:step="1"
placeholder="到手价"
style="width: 100%">
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="文案风格">
<el-select v-model="form.style" placeholder="选择风格" style="width: 100%">
<el-option label="小红书风格" value="xhs">
<span style="float: left">小红书风格</span>
<span style="float: right; color: #8492a6; font-size: 13px">真实种草</span>
</el-option>
<el-option label="抖音风格" value="douyin">
<span style="float: left">抖音风格</span>
<span style="float: right; color: #8492a6; font-size: 13px">直接有冲击力</span>
</el-option>
<el-option label="通用风格" value="both">
<span style="float: left">通用风格</span>
<span style="float: right; color: #8492a6; font-size: 13px">适合多平台</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button
type="primary"
icon="el-icon-magic-stick"
size="medium"
@click="handleGenerateComplete"
:loading="generating">
一键生成全部
</el-button>
<el-button
type="success"
icon="el-icon-search"
@click="handleExtractKeywords"
:loading="extractingKeywords">
提取关键词
</el-button>
<el-button
type="warning"
icon="el-icon-edit"
@click="handleGenerateContent"
:loading="generatingContent">
生成文案
</el-button>
<el-button
icon="el-icon-refresh-left"
@click="handleReset">
重置
</el-button>
<el-button
type="info"
icon="el-icon-connection"
@click="showParseDialog = true">
从解析接口导入
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 关键词展示区域 -->
<div v-if="result.keywords && result.keywords.length > 0" class="keywords-section">
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold;">
<i class="el-icon-collection-tag"></i>
提取的关键词
</span>
</el-divider>
<div class="keywords-display">
<el-tag
v-for="(keyword, index) in result.keywords"
:key="index"
type="primary"
effect="dark"
size="medium"
style="margin: 5px; cursor: pointer;"
@click="handleCopyText(keyword)">
{{ keyword }}
<i class="el-icon-document-copy" style="margin-left: 5px;"></i>
</el-tag>
<el-button
type="text"
icon="el-icon-document-copy"
@click="handleCopyText(result.keywordsText)"
style="margin-left: 10px;">
复制全部关键词
</el-button>
</div>
</div>
<!-- 文案展示区域 -->
<div v-if="result.content" class="content-section">
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold;">
<i class="el-icon-edit-outline"></i>
生成的文案
</span>
<el-button
type="text"
icon="el-icon-document-copy"
@click="handleCopyText(result.content)"
style="float: right; margin-top: -5px;">
复制文案
</el-button>
</el-divider>
<div class="content-display">
<el-input
v-model="result.content"
type="textarea"
:rows="8"
readonly
class="content-textarea">
</el-input>
<div class="content-actions">
<el-button
size="small"
icon="el-icon-document-copy"
@click="handleCopyText(result.content)">
复制文案
</el-button>
<el-button
size="small"
icon="el-icon-refresh"
@click="handleRegenerateContent">
重新生成
</el-button>
</div>
</div>
</div>
<!-- 图片展示区域 -->
<div v-if="result.imageBase64" class="image-section">
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold;">
<i class="el-icon-picture"></i>
生成的营销图片
</span>
</el-divider>
<div class="image-display">
<div class="image-wrapper">
<img
:src="result.imageBase64"
alt="营销图片"
class="result-image"
@click="handlePreviewLargeImage(result.imageBase64)">
<div class="image-overlay">
<el-button-group>
<el-button
type="primary"
size="small"
icon="el-icon-download"
@click="handleDownloadImage(result.imageBase64, `营销图片_${form.productName || '商品'}_${Date.now()}.jpg`)">
下载
</el-button>
<el-button
type="success"
size="small"
icon="el-icon-document-copy"
@click="handleCopyImage(result.imageBase64)">
复制图片
</el-button>
<el-button
type="info"
size="small"
icon="el-icon-zoom-in"
@click="handlePreviewLargeImage(result.imageBase64)">
查看大图
</el-button>
</el-button-group>
</div>
</div>
</div>
</div>
<!-- 快捷操作区域 -->
<div v-if="result.content || result.imageBase64" class="quick-actions-section">
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold;">
<i class="el-icon-s-operation"></i>
快捷操作
</span>
</el-divider>
<div class="quick-actions">
<el-button
type="primary"
icon="el-icon-document-copy"
@click="handleCopyAll">
复制全部文案+图片链接
</el-button>
<el-button
type="success"
icon="el-icon-download"
@click="handleDownloadAll">
下载图片
</el-button>
<el-button
icon="el-icon-delete"
@click="handleClearResult">
清空结果
</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>
</div>
</template>
<script>
import { extractKeywords, generateContent, generateComplete } from '@/api/jarvis/socialMedia'
import { parseLineReport } from '@/api/jarvis/batchPublish'
export default {
name: 'SocialMedia',
data() {
return {
showHelp: false,
// 表单数据
form: {
productImageUrl: '',
productName: '',
originalPrice: null,
finalPrice: null,
style: 'xhs'
},
// 结果数据
result: {
keywords: [],
keywordsText: '',
content: '',
imageBase64: ''
},
// 加载状态
generating: false,
extractingKeywords: false,
generatingContent: false,
parsing: false,
// 预览对话框
previewDialogVisible: false,
previewLargeImageUrl: '',
// 解析对话框
showParseDialog: false,
parseText: ''
}
},
mounted() {
// 尝试从localStorage加载上次的数据
this.loadFromStorage()
},
methods: {
/** 一键生成全部 */
async handleGenerateComplete() {
// 验证表单
if (!this.form.productName) {
this.$message.warning('请输入商品名称')
return
}
if (this.form.finalPrice === null || this.form.finalPrice <= 0) {
this.$message.warning('请输入有效的到手价')
return
}
this.generating = true
try {
const res = await generateComplete({
productImageUrl: this.form.productImageUrl || undefined,
productName: this.form.productName,
originalPrice: this.form.originalPrice || undefined,
finalPrice: this.form.finalPrice,
style: this.form.style
})
if (res.code === 200 && res.data) {
const data = res.data
if (data.success) {
this.result = {
keywords: data.keywords || [],
keywordsText: data.keywordsText || '',
content: data.content || '',
imageBase64: data.imageBase64 || ''
}
// 保存到localStorage
this.saveToStorage()
this.$message.success('生成成功!')
// 滚动到结果区域
this.$nextTick(() => {
if (this.result.keywords && this.result.keywords.length > 0) {
const keywordsSection = document.querySelector('.keywords-section')
if (keywordsSection) {
keywordsSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
})
} else {
this.$message.error(data.error || '生成失败')
}
} else {
this.$message.error(res.msg || '生成失败')
}
} catch (error) {
console.error('生成失败', error)
this.$message.error('生成失败:' + (error.message || '未知错误'))
} finally {
this.generating = false
}
},
/** 提取关键词 */
async handleExtractKeywords() {
if (!this.form.productName) {
this.$message.warning('请输入商品名称')
return
}
this.extractingKeywords = true
try {
const res = await extractKeywords({
productName: this.form.productName
})
if (res.code === 200 && res.data) {
const data = res.data
if (data.success) {
this.result.keywords = data.keywords || []
this.result.keywordsText = data.keywordsText || ''
this.$message.success('关键词提取成功!')
} else {
this.$message.warning(data.error || '提取失败,已使用降级方案')
}
} else {
this.$message.error(res.msg || '提取失败')
}
} catch (error) {
console.error('提取关键词失败', error)
this.$message.error('提取失败:' + (error.message || '未知错误'))
} finally {
this.extractingKeywords = false
}
},
/** 生成文案 */
async handleGenerateContent() {
if (!this.form.productName) {
this.$message.warning('请输入商品名称')
return
}
this.generatingContent = true
try {
const res = await generateContent({
productName: this.form.productName,
originalPrice: this.form.originalPrice || undefined,
finalPrice: this.form.finalPrice || undefined,
keywords: this.result.keywordsText || undefined,
style: this.form.style
})
if (res.code === 200 && res.data) {
const data = res.data
if (data.success) {
this.result.content = data.content || ''
this.$message.success('文案生成成功!')
} else {
this.$message.error(data.error || '生成失败')
}
} else {
this.$message.error(res.msg || '生成失败')
}
} catch (error) {
console.error('生成文案失败', error)
this.$message.error('生成失败:' + (error.message || '未知错误'))
} finally {
this.generatingContent = false
}
},
/** 重新生成文案 */
async handleRegenerateContent() {
await this.handleGenerateContent()
},
/** 复制文本 */
handleCopyText(text) {
if (!text) {
this.$message.warning('没有可复制的内容')
return
}
// 创建临时文本域
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
try {
document.execCommand('copy')
this.$message.success('复制成功!')
} catch (error) {
// 降级方案
textarea.select()
try {
document.execCommand('copy')
this.$message.success('复制成功!')
} catch (e) {
this.$message.error('复制失败,请手动复制')
}
}
document.body.removeChild(textarea)
},
/** 复制图片 */
async handleCopyImage(base64) {
try {
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' })
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('复制失败')
}
},
/** 下载图片 */
handleDownloadImage(base64, filename) {
try {
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('下载失败')
}
},
/** 复制全部 */
handleCopyAll() {
let allText = ''
if (this.result.keywordsText) {
allText += '关键词:' + this.result.keywordsText + '\n\n'
}
if (this.result.content) {
allText += this.result.content + '\n\n'
}
if (this.result.imageBase64) {
allText += '[营销图片已生成,请查看图片]'
}
if (allText) {
this.handleCopyText(allText)
} else {
this.$message.warning('没有可复制的内容')
}
},
/** 下载全部 */
handleDownloadAll() {
if (this.result.imageBase64) {
this.handleDownloadImage(
this.result.imageBase64,
`营销图片_${this.form.productName || '商品'}_${Date.now()}.jpg`
)
} else {
this.$message.warning('没有可下载的图片')
}
},
/** 清空结果 */
handleClearResult() {
this.$confirm('确定要清空所有结果吗?', '提示', {
type: 'warning'
}).then(() => {
this.result = {
keywords: [],
keywordsText: '',
content: '',
imageBase64: ''
}
this.$message.success('已清空')
}).catch(() => {})
},
/** 预览图片 */
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 = ''
},
/** 重置表单 */
handleReset() {
this.form = {
productImageUrl: '',
productName: '',
originalPrice: null,
finalPrice: null,
style: 'xhs'
}
this.result = {
keywords: [],
keywordsText: '',
content: '',
imageBase64: ''
}
},
/** 调用解析接口 */
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) {
// 导入第一个商品
const item = res.data[0]
this.form = {
productImageUrl: item.productImage || '',
productName: item.productName || '',
originalPrice: item.price || null,
finalPrice: null // 需要手动输入
}
// 尝试从输入文本中提取价格
const lines = this.parseText.trim().split('\n')
lines.forEach(line => {
const parts = line.trim().split(/\s+/)
if (parts.length >= 2 && parts[0].includes(item._raw?.originalUrl || '')) {
const price = parseFloat(parts[1])
if (!isNaN(price)) {
this.form.finalPrice = price
}
}
})
this.$message.success('导入成功!请检查信息后生成内容')
this.showParseDialog = false
} else {
this.$message.error(res.msg || '解析失败')
}
} catch (error) {
console.error('调用解析接口失败', error)
this.$message.error('解析失败:' + (error.message || '未知错误'))
} finally {
this.parsing = false
}
},
/** 保存到localStorage */
saveToStorage() {
try {
localStorage.setItem('socialMediaForm', JSON.stringify(this.form))
localStorage.setItem('socialMediaResult', JSON.stringify(this.result))
} catch (error) {
console.error('保存失败', error)
}
},
/** 从localStorage加载 */
loadFromStorage() {
try {
const formStr = localStorage.getItem('socialMediaForm')
const resultStr = localStorage.getItem('socialMediaResult')
if (formStr) {
this.form = { ...this.form, ...JSON.parse(formStr) }
}
if (resultStr) {
this.result = { ...this.result, ...JSON.parse(resultStr) }
}
} catch (error) {
console.error('加载失败', error)
}
}
}
}
</script>
<style scoped>
.social-media-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;
}
.input-section {
margin-top: 20px;
}
.main-form {
max-width: 100%;
}
.keywords-section,
.content-section,
.image-section,
.quick-actions-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #EBEEF5;
}
.keywords-display {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 15px;
background: #f5f7fa;
border-radius: 4px;
}
.content-display {
position: relative;
}
.content-textarea {
font-size: 14px;
line-height: 1.6;
}
.content-actions {
margin-top: 10px;
text-align: right;
}
.image-display {
display: flex;
justify-content: center;
padding: 20px;
background: #f5f7fa;
border-radius: 4px;
}
.image-wrapper {
position: relative;
max-width: 500px;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.result-image {
width: 100%;
height: auto;
display: block;
cursor: pointer;
transition: transform 0.3s;
}
.result-image:hover {
transform: scale(1.02);
}
.image-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;
}
.image-wrapper:hover .image-overlay {
opacity: 1;
}
.quick-actions {
text-align: center;
padding: 20px;
}
.quick-actions .el-button {
margin: 0 10px;
}
.large-preview {
text-align: center;
max-height: 70vh;
overflow: auto;
}
.dialog-footer {
text-align: right;
margin-top: 20px;
}
</style>