feat: 营销页面结果旁显示中文翻译 + 朗读按钮

This commit is contained in:
TradeMate Dev
2026-05-17 16:52:27 +08:00
parent 1fabad0fe9
commit 32d2b57df7
+66 -2
View File
@@ -114,8 +114,10 @@
<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>
<text class="result-translation" v-if="item.translation">📖 {{ item.translation }}</text>
<view class="result-actions">
<text class="copy-btn" @click="copyText(item.content || item)">复制</text>
<text class="play-btn" @click="playTts(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>
@@ -146,7 +148,7 @@
<script setup>
import { ref, reactive, computed } from 'vue'
import { marketingApi, interactionApi } from '@/utils/api.js'
import { marketingApi, interactionApi, translateApi, BASE_URL } from '@/utils/api.js'
const tabConfig = {
copy: {
@@ -302,6 +304,15 @@ const generateContent = async () => {
resultsMap[tab] = res.results || []
if (res.results && res.results.length > 0) {
selectedStyle.value = res.results[0].style || formData.value.style
const items = res.results.filter(r => (r.content || r).match(/[a-zA-Z]{3,}/))
if (items.length > 0) {
Promise.all(items.map(async (r) => {
try {
const t = await translateApi.translate(r.content || r, 'zh')
r.translation = t.translated || t.translated_text || ''
} catch {}
}))
}
}
loadStats()
}
@@ -373,6 +384,43 @@ const sendToWhatsapp = (text) => {
uni.showToast({ title: '请先选择客户', icon: 'none' })
}
const playTts = (text) => {
if (!text) return
const hasChinese = /[\u4e00-\u9fa5]/.test(text)
const lang = hasChinese ? 'zh' : 'en'
const token = uni.getStorageSync('token')
uni.showLoading({ title: '语音生成中...' })
if (typeof window !== 'undefined' && window.Audio) {
uni.request({
url: `${BASE_URL}/translate/tts?text=${encodeURIComponent(text)}&lang=${lang}`,
method: 'GET',
header: { Authorization: `Bearer ${token}` },
responseType: 'arraybuffer',
success: (res) => {
uni.hideLoading()
if (res.statusCode === 200 && res.data) {
const blob = new Blob([res.data], { type: 'audio/mpeg' })
const url = URL.createObjectURL(blob)
const audio = new Audio(url)
audio.onended = () => { URL.revokeObjectURL(url) }
audio.play().catch(() => {
uni.showToast({ title: '播放失败', icon: 'none' })
})
} else {
uni.showToast({ title: '语音生成失败', icon: 'none' })
}
},
fail: () => {
uni.hideLoading()
uni.showToast({ title: '语音生成失败', icon: 'none' })
},
})
} else {
uni.showToast({ title: '当前环境不支持朗读', icon: 'none' })
}
}
const runCompetitorAnalysis = async () => {
if (!formData.value.product_name) {
uni.showToast({ title: '请先输入产品名称', icon: 'none' })
@@ -620,7 +668,18 @@ const runCompetitorAnalysis = async () => {
font-size: 26rpx;
line-height: 1.6;
display: block;
margin-bottom: 8rpx;
}
.result-translation {
font-size: 24rpx;
color: #999;
line-height: 1.5;
display: block;
margin-bottom: 16rpx;
padding: 12rpx;
background: #f0f5ff;
border-radius: 8rpx;
}
.result-actions {
@@ -628,7 +687,7 @@ const runCompetitorAnalysis = async () => {
gap: 20rpx;
}
.copy-btn, .send-btn, .competitor-btn {
.copy-btn, .send-btn, .competitor-btn, .play-btn {
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 6rpx;
@@ -639,6 +698,11 @@ const runCompetitorAnalysis = async () => {
color: #1890ff;
}
.play-btn {
background: #fff7e6;
color: #fa8c16;
}
.send-btn {
background: #07c160;
color: #fff;