Initial commit: TradeMate 外贸小助手 MVP
项目结构: - backend/ Python FastAPI 后端 - uni-app/ uni-app跨端前端 - docs/ 设计文档 - docker-compose.yml Docker编排 - nginx/scripts/systemd 运维配置 已完成功能: - 用户认证 (JWT) - 智能翻译 + 回复建议 - 营销素材生成 - 客户管理 + 沉默检测 - 报价单管理 - 产品库管理 - 汇率换算 - 推送通知 (uni-push) - WhatsApp Webhook框架 - Celery定时任务
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<view class="marketing-container">
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'copy' }"
|
||||
@click="activeTab = 'copy'"
|
||||
>
|
||||
开发信
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'whatsapp' }"
|
||||
@click="activeTab = 'whatsapp'"
|
||||
>
|
||||
WhatsApp话术
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'product' }"
|
||||
@click="activeTab = 'product'"
|
||||
>
|
||||
产品描述
|
||||
</view>
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'keywords' }"
|
||||
@click="activeTab = 'keywords'"
|
||||
>
|
||||
关键词
|
||||
</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="如: 户外折叠椅" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">产品描述</text>
|
||||
<textarea class="form-textarea" v-model="formData.description" placeholder="描述产品的特点、规格、优势..." />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">目标市场</text>
|
||||
<picker :range="targetMarkets" @change="onTargetChange">
|
||||
<view class="picker-value">{{ formData.target || '选择目标市场' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<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 ? '生成中...' : '生成文案' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="results-section" v-if="results.length > 0">
|
||||
<view class="results-header">
|
||||
<text class="results-title">生成的文案</text>
|
||||
<text class="refresh-btn" @click="generateContent">换一批</text>
|
||||
</view>
|
||||
<view class="results-list">
|
||||
<view class="result-item" v-for="(item, index) in results" :key="index">
|
||||
<text class="result-text">{{ item }}</text>
|
||||
<view class="result-actions">
|
||||
<text class="copy-btn" @click="copyText(item)">复制</text>
|
||||
<text class="send-btn" @click="sendToWhatsapp(item)">发送</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="history-section" v-if="activeTab === 'keywords' && keywords.length > 0">
|
||||
<view class="history-header">
|
||||
<text class="history-title">关键词建议</text>
|
||||
</view>
|
||||
<view class="keywords-list">
|
||||
<view class="keyword-tag" v-for="(kw, idx) in keywords" :key="idx" @click="copyText(kw)">
|
||||
{{ kw }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" v-if="!loading && results.length === 0 && activeTab !== 'keywords'">
|
||||
<text>输入产品信息,点击生成文案</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { marketingApi } from '@/utils/api.js'
|
||||
|
||||
const activeTab = ref('copy')
|
||||
const loading = ref(false)
|
||||
const results = ref([])
|
||||
const keywords = ref([])
|
||||
|
||||
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 generateContent = async () => {
|
||||
if (!formData.value.product_name) {
|
||||
uni.showToast({ title: '请输入产品名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
if (activeTab.value === 'keywords') {
|
||||
const res = await marketingApi.getKeywords(
|
||||
formData.value.product_name,
|
||||
formData.value.description,
|
||||
''
|
||||
)
|
||||
keywords.value = res.keywords || []
|
||||
} else {
|
||||
const res = await marketingApi.generate(
|
||||
formData.value.product_name,
|
||||
formData.value.description,
|
||||
'',
|
||||
formData.value.target,
|
||||
formData.value.style
|
||||
)
|
||||
results.value = res.results || []
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: err.message || '生成失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const copyText = (text) => {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.showToast({ title: '已复制', icon: 'success' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const sendToWhatsapp = (text) => {
|
||||
uni.showToast({ title: '请先选择客户', 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
font-size: 24rpx;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.history-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.keywords-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
padding: 12rpx 24rpx;
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
border-radius: 20rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 100rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user