709 lines
18 KiB
Vue
709 lines
18 KiB
Vue
<template>
|
||
<view class="marketing-container">
|
||
<view class="tabs">
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'copy' }"
|
||
@click="switchTab('copy')"
|
||
>
|
||
开发信
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'whatsapp' }"
|
||
@click="switchTab('whatsapp')"
|
||
>
|
||
WhatsApp话术
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'product' }"
|
||
@click="switchTab('product')"
|
||
>
|
||
产品描述
|
||
</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: activeTab === 'keywords' }"
|
||
@click="switchTab('keywords')"
|
||
>
|
||
关键词
|
||
</view>
|
||
</view>
|
||
|
||
<view class="stats-section" v-if="stats">
|
||
<view class="stat-card">
|
||
<text class="stat-value">{{ stats.today_copy || 0 }}</text>
|
||
<text class="stat-label">今日复制</text>
|
||
</view>
|
||
<view class="stat-card">
|
||
<text class="stat-value">{{ stats.today_send || 0 }}</text>
|
||
<text class="stat-label">今日发送</text>
|
||
</view>
|
||
<view class="stat-card">
|
||
<text class="stat-value">{{ stats.weekly_total || 0 }}</text>
|
||
<text class="stat-label">本周共</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-section">
|
||
<view class="form-item">
|
||
<text class="form-label">产品名称</text>
|
||
<input class="form-input" v-model="formData.product_name" :placeholder="tabConfig[activeTab].namePlaceholder" />
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label">{{ tabConfig[activeTab].descLabel }}</text>
|
||
<textarea class="form-textarea" v-model="formData.description" :placeholder="tabConfig[activeTab].descPlaceholder" />
|
||
</view>
|
||
<view class="form-item" v-if="tabConfig[activeTab].showTarget">
|
||
<text class="form-label">目标市场</text>
|
||
<picker :range="targetMarkets" @change="onTargetChange">
|
||
<view class="picker-value">{{ formData.target || '选择目标市场' }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="form-item" v-if="tabConfig[activeTab].showStyle">
|
||
<text class="form-label">文案风格</text>
|
||
<view class="style-options">
|
||
<view
|
||
class="style-option"
|
||
:class="{ active: formData.style === 'professional' }"
|
||
@click="formData.style = 'professional'"
|
||
>
|
||
专业正式
|
||
</view>
|
||
<view
|
||
class="style-option"
|
||
:class="{ active: formData.style === 'friendly' }"
|
||
@click="formData.style = 'friendly'"
|
||
>
|
||
亲切友好
|
||
</view>
|
||
<view
|
||
class="style-option"
|
||
:class="{ active: formData.style === 'persuasive' }"
|
||
@click="formData.style = 'persuasive'"
|
||
>
|
||
促销风格
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<button class="generate-btn" @click="generateContent" :disabled="loading">
|
||
{{ loading ? statusMessage || '生成中...' : tabConfig[activeTab].btnText }}
|
||
</button>
|
||
</view>
|
||
|
||
<view class="results-section" v-if="resultsMap[activeTab] && resultsMap[activeTab].length > 0">
|
||
<view class="results-header">
|
||
<text class="results-title">{{ tabConfig[activeTab].resultTitle }}</text>
|
||
<view class="results-actions">
|
||
<text class="refresh-btn" @click="generateContent">换一批</text>
|
||
<text class="export-btn" @click="exportCsv" v-if="activeTab !== 'keywords'">导出CSV</text>
|
||
</view>
|
||
</view>
|
||
<view class="style-tabs" v-if="activeTab !== 'keywords' && availableStyles.length > 1">
|
||
<view
|
||
class="style-tab"
|
||
v-for="s in availableStyles"
|
||
:key="s"
|
||
:class="{ active: selectedStyle === s }"
|
||
@click="selectedStyle = s"
|
||
>
|
||
{{ styleLabels[s] || s }}
|
||
</view>
|
||
</view>
|
||
<view class="results-list" v-if="activeTab !== 'keywords'">
|
||
<view class="result-item" v-for="(item, index) in filteredResults" :key="index">
|
||
<text class="result-text">{{ item.content || item }}</text>
|
||
<view class="result-actions">
|
||
<text class="copy-btn" @click="copyText(item.content || item)">复制</text>
|
||
<text class="send-btn" @click="sendToWhatsapp(item.content || item)" v-if="activeTab !== 'product'">发送</text>
|
||
<text class="competitor-btn" @click="runCompetitorAnalysis" v-if="activeTab === 'copy'">竞品分析</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="keywords-list" v-if="activeTab === 'keywords' && resultsMap.keywords && resultsMap.keywords.length > 0">
|
||
<view class="keyword-tag" v-for="(kw, idx) in resultsMap.keywords" :key="idx" @click="copyText(kw)">
|
||
{{ kw }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="competitor-section" v-if="competitorResult">
|
||
<view class="competitor-header">
|
||
<text class="competitor-title">竞品分析结果</text>
|
||
<text class="competitor-close" @click="competitorResult = null">×</text>
|
||
</view>
|
||
<view class="competitor-content">
|
||
<text class="competitor-text">{{ competitorResult }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="empty" v-if="!loading && (!resultsMap[activeTab] || resultsMap[activeTab].length === 0)">
|
||
<text>{{ tabConfig[activeTab].emptyHint }}</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed } from 'vue'
|
||
import { marketingApi, interactionApi } from '@/utils/api.js'
|
||
|
||
const tabConfig = {
|
||
copy: {
|
||
label: '开发信',
|
||
category: 'sales_letter',
|
||
namePlaceholder: '如: 户外折叠椅',
|
||
descLabel: '产品描述',
|
||
descPlaceholder: '描述产品的特点、规格、优势...',
|
||
btnText: '生成开发信',
|
||
resultTitle: '生成的开发信',
|
||
emptyHint: '输入产品信息,点击生成开发信',
|
||
showTarget: true,
|
||
showStyle: true,
|
||
},
|
||
whatsapp: {
|
||
label: 'WhatsApp话术',
|
||
category: 'whatsapp',
|
||
namePlaceholder: '如: 户外折叠椅',
|
||
descLabel: '产品及沟通场景',
|
||
descPlaceholder: '描述产品特点,以及和客户沟通的具体场景...',
|
||
btnText: '生成话术',
|
||
resultTitle: '生成的WhatsApp话术',
|
||
emptyHint: '输入产品信息,点击生成话术',
|
||
showTarget: true,
|
||
showStyle: true,
|
||
},
|
||
product: {
|
||
label: '产品描述',
|
||
category: 'product_description',
|
||
namePlaceholder: '如: 户外折叠椅',
|
||
descLabel: '产品详细规格',
|
||
descPlaceholder: '描述产品的材质、尺寸、承重、颜色、包装等规格...',
|
||
btnText: '生成描述',
|
||
resultTitle: '生成的产品描述',
|
||
emptyHint: '输入产品信息,点击生成描述',
|
||
showTarget: false,
|
||
showStyle: false,
|
||
},
|
||
keywords: {
|
||
label: '关键词',
|
||
category: '',
|
||
namePlaceholder: '如: 户外折叠椅',
|
||
descLabel: '产品卖点',
|
||
descPlaceholder: '描述产品的核心卖点和目标客户群体...',
|
||
btnText: '生成关键词',
|
||
resultTitle: '关键词建议',
|
||
emptyHint: '输入产品信息,点击生成关键词',
|
||
showTarget: false,
|
||
showStyle: false,
|
||
},
|
||
}
|
||
|
||
const activeTab = ref('copy')
|
||
const loading = ref(false)
|
||
const statusMessage = ref('')
|
||
const resultsMap = reactive({ copy: [], whatsapp: [], product: [], keywords: [] })
|
||
const competitorResult = ref(null)
|
||
const stats = ref(null)
|
||
const selectedStyle = ref('professional')
|
||
|
||
const styleLabels = {
|
||
professional: '专业正式',
|
||
friendly: '亲切友好',
|
||
urgent: '紧急催促',
|
||
benefit_focused: '利益导向',
|
||
storytelling: '故事叙述',
|
||
}
|
||
|
||
const availableStyles = computed(() => {
|
||
const items = resultsMap[activeTab.value]
|
||
if (!items || items.length === 0 || typeof items[0] === 'string') return []
|
||
const styles = [...new Set(items.map(i => i.style).filter(Boolean))]
|
||
return styles
|
||
})
|
||
|
||
const filteredResults = computed(() => {
|
||
const items = resultsMap[activeTab.value]
|
||
if (!items || items.length === 0) return []
|
||
if (typeof items[0] === 'string') return items
|
||
const style = selectedStyle.value
|
||
const filtered = items.filter(i => i.style === style)
|
||
return filtered.length > 0 ? filtered : items
|
||
})
|
||
|
||
const formData = ref({
|
||
product_name: '',
|
||
description: '',
|
||
target: 'US importers',
|
||
style: 'professional',
|
||
})
|
||
|
||
const targetMarkets = ref([
|
||
'US importers',
|
||
'European buyers',
|
||
'Southeast Asia',
|
||
'Latin America',
|
||
'Middle East',
|
||
])
|
||
|
||
const onTargetChange = (e) => {
|
||
formData.value.target = targetMarkets.value[e.detail.value]
|
||
}
|
||
|
||
const switchTab = (tab) => {
|
||
activeTab.value = tab
|
||
loadStats()
|
||
}
|
||
|
||
const loadStats = async () => {
|
||
try {
|
||
const res = await interactionApi.getMarketingEffectStats()
|
||
stats.value = res
|
||
} catch (err) {
|
||
console.error('加载统计失败', err)
|
||
}
|
||
}
|
||
|
||
const generateContent = async () => {
|
||
if (!formData.value.product_name) {
|
||
uni.showToast({ title: '请输入产品名称', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
statusMessage.value = 'AI 准备中...'
|
||
const tab = activeTab.value
|
||
|
||
try {
|
||
if (tab === 'keywords') {
|
||
statusMessage.value = '正在提取关键词...'
|
||
const res = await marketingApi.getKeywords(
|
||
formData.value.product_name,
|
||
formData.value.description,
|
||
''
|
||
)
|
||
resultsMap[tab] = res.keywords || []
|
||
} else {
|
||
const cfg = tabConfig[tab]
|
||
statusMessage.value = 'AI 正在生成文案(约15-30秒)...'
|
||
setTimeout(() => {
|
||
if (loading.value) statusMessage.value = 'AI 处理中,请稍候...'
|
||
}, 8000)
|
||
setTimeout(() => {
|
||
if (loading.value) statusMessage.value = '即将完成...'
|
||
}, 20000)
|
||
const res = await marketingApi.generate(
|
||
formData.value.product_name,
|
||
formData.value.description,
|
||
cfg.category,
|
||
formData.value.target,
|
||
formData.value.style
|
||
)
|
||
resultsMap[tab] = res.results || []
|
||
if (res.results && res.results.length > 0) {
|
||
selectedStyle.value = res.results[0].style || formData.value.style
|
||
}
|
||
loadStats()
|
||
}
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '生成失败', icon: 'none' })
|
||
} finally {
|
||
loading.value = false
|
||
statusMessage.value = ''
|
||
}
|
||
}
|
||
|
||
const copyText = (text) => {
|
||
if (!text) {
|
||
uni.showToast({ title: '内容为空', icon: 'none' })
|
||
return
|
||
}
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
interactionApi.trackMarketingEffect({ action: 'copy', content_preview: text.slice(0, 100) }).catch(() => {})
|
||
loadStats()
|
||
uni.showToast({ title: '已复制', icon: 'success' })
|
||
}).catch(() => fallbackCopy(text))
|
||
} else {
|
||
fallbackCopy(text)
|
||
}
|
||
}
|
||
|
||
const fallbackCopy = (text) => {
|
||
const ta = document.createElement('textarea')
|
||
ta.value = text
|
||
ta.style.position = 'fixed'
|
||
ta.style.opacity = '0'
|
||
document.body.appendChild(ta)
|
||
ta.select()
|
||
try {
|
||
document.execCommand('copy')
|
||
interactionApi.trackMarketingEffect({ action: 'copy', content_preview: text.slice(0, 100) }).catch(() => {})
|
||
loadStats()
|
||
uni.showToast({ title: '已复制', icon: 'success' })
|
||
} catch {
|
||
uni.showToast({ title: '复制失败,请手动选择复制', icon: 'none' })
|
||
}
|
||
document.body.removeChild(ta)
|
||
}
|
||
|
||
const exportCsv = () => {
|
||
const items = resultsMap[activeTab.value]
|
||
if (!items || items.length === 0) return
|
||
let csv = 'Content\n'
|
||
items.forEach(r => {
|
||
const text = typeof r === 'string' ? r : (r.content || '')
|
||
csv += `"${text.replace(/"/g, '""')}"\n`
|
||
})
|
||
const blob = new Blob([csv], { type: 'text/csv' })
|
||
const url = URL.createObjectURL(blob)
|
||
uni.downloadFile({
|
||
url,
|
||
success: (res) => {
|
||
uni.saveFile({ tempFilePath: res.tempFilePath })
|
||
uni.showToast({ title: '导出成功', icon: 'success' })
|
||
},
|
||
fail: () => { uni.showToast({ title: '导出失败', icon: 'none' }) },
|
||
})
|
||
}
|
||
|
||
const sendToWhatsapp = (text) => {
|
||
interactionApi.trackMarketingEffect({ action: 'send', content_preview: text.slice(0, 100) }).catch(() => {})
|
||
loadStats()
|
||
uni.showToast({ title: '请先选择客户', icon: 'none' })
|
||
}
|
||
|
||
const runCompetitorAnalysis = async () => {
|
||
if (!formData.value.product_name) {
|
||
uni.showToast({ title: '请先输入产品名称', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
uni.showLoading({ title: '分析中...' })
|
||
const res = await marketingApi.competitorAnalysis(
|
||
formData.value.product_name,
|
||
formData.value.description,
|
||
'',
|
||
formData.value.target
|
||
)
|
||
uni.hideLoading()
|
||
const analysis = res.analysis
|
||
if (!analysis || Object.keys(analysis).length === 0) {
|
||
competitorResult.value = '暂无分析结果,请确认产品信息后重试'
|
||
return
|
||
}
|
||
const lines = []
|
||
if (analysis.price_range) lines.push(`💰 价格范围:${analysis.price_range}`)
|
||
if (analysis.market_trends) lines.push(`📈 市场趋势:${analysis.market_trends}`)
|
||
if (analysis.key_selling_points && analysis.key_selling_points.length) {
|
||
lines.push(`\n🏆 核心卖点:\n ${analysis.key_selling_points.map(p => `• ${p}`).join('\n ')}`)
|
||
}
|
||
if (analysis.common_keywords && analysis.common_keywords.length) {
|
||
lines.push(`\n🔑 常见关键词:\n ${analysis.common_keywords.map(k => `• ${k}`).join('\n ')}`)
|
||
}
|
||
if (analysis.suggestions && analysis.suggestions.length) {
|
||
lines.push(`\n💡 建议:\n ${analysis.suggestions.map(s => `• ${s}`).join('\n ')}`)
|
||
}
|
||
competitorResult.value = lines.length ? lines.join('\n') : '暂无分析结果'
|
||
} catch (err) {
|
||
uni.hideLoading()
|
||
uni.showToast({ title: err.message || '分析失败', icon: 'none' })
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.marketing-container {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
margin-bottom: 20rpx;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
min-width: 160rpx;
|
||
text-align: center;
|
||
padding: 24rpx 16rpx;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tab-item.active {
|
||
color: #1890ff;
|
||
border-bottom: 4rpx solid #1890ff;
|
||
}
|
||
|
||
.stats-section {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
color: #1890ff;
|
||
display: block;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 22rpx;
|
||
color: #999;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.form-section {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
display: block;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
height: 72rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.form-textarea {
|
||
width: 100%;
|
||
min-height: 160rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
padding: 20rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.picker-value {
|
||
height: 72rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
padding: 0 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.style-options {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.style-option {
|
||
flex: 1;
|
||
padding: 16rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
text-align: center;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.style-option.active {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
border: 2rpx solid #1890ff;
|
||
}
|
||
|
||
.style-tabs {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
margin-bottom: 20rpx;
|
||
overflow-x: auto;
|
||
padding-bottom: 4rpx;
|
||
}
|
||
|
||
.style-tab {
|
||
flex-shrink: 0;
|
||
padding: 12rpx 24rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.style-tab.active {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
border: 2rpx solid #1890ff;
|
||
}
|
||
|
||
.generate-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: #1890ff;
|
||
color: #fff;
|
||
border-radius: 12rpx;
|
||
font-size: 30rpx;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.results-section {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.results-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.results-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.results-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.refresh-btn {
|
||
font-size: 24rpx;
|
||
color: #1890ff;
|
||
}
|
||
|
||
.export-btn {
|
||
font-size: 24rpx;
|
||
color: #52c41a;
|
||
}
|
||
|
||
.results-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.result-item {
|
||
padding: 20rpx;
|
||
background: #f9f9f9;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.result-text {
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
display: block;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.result-actions {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.copy-btn, .send-btn, .competitor-btn {
|
||
font-size: 24rpx;
|
||
padding: 8rpx 20rpx;
|
||
border-radius: 6rpx;
|
||
}
|
||
|
||
.copy-btn {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
}
|
||
|
||
.send-btn {
|
||
background: #07c160;
|
||
color: #fff;
|
||
}
|
||
|
||
.competitor-btn {
|
||
background: #f9f0ff;
|
||
color: #722ed1;
|
||
}
|
||
|
||
.keywords-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.keyword-tag {
|
||
padding: 12rpx 24rpx;
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
border-radius: 20rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.competitor-section {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.competitor-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.competitor-title {
|
||
font-size: 26rpx;
|
||
color: #722ed1;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.competitor-close {
|
||
font-size: 36rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.competitor-content {
|
||
padding: 16rpx;
|
||
background: #f9f0ff;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.competitor-text {
|
||
font-size: 24rpx;
|
||
line-height: 1.5;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.empty {
|
||
text-align: center;
|
||
color: #999;
|
||
padding: 100rpx;
|
||
}
|
||
</style>
|