This commit is contained in:
Leo
2025-11-29 22:47:44 +08:00
parent a21f6f77a3
commit ec921d313c
2 changed files with 946 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import request from '@/utils/request'
// 提取关键词
export function extractKeywords(data) {
return request({
url: '/jarvis/social-media/extract-keywords',
method: 'post',
data: data
})
}
// 生成文案
export function generateContent(data) {
return request({
url: '/jarvis/social-media/generate-content',
method: 'post',
data: data
})
}
// 一键生成完整内容(关键词 + 文案 + 图片)
export function generateComplete(data) {
return request({
url: '/jarvis/social-media/generate-complete',
method: 'post',
data: data
})
}

View File

@@ -0,0 +1,917 @@
<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({
text: 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>