1
This commit is contained in:
29
src/api/jarvis/socialMedia.js
Normal file
29
src/api/jarvis/socialMedia.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
917
src/views/system/social-media/index.vue
Normal file
917
src/views/system/social-media/index.vue
Normal 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>
|
||||||
|
|
||||||
Reference in New Issue
Block a user