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,319 @@
|
||||
<template>
|
||||
<view class="translate-container">
|
||||
<view class="mode-switch">
|
||||
<view
|
||||
class="mode-item"
|
||||
:class="{ active: mode === 'translate' }"
|
||||
@click="mode = 'translate'"
|
||||
>
|
||||
文本翻译
|
||||
</view>
|
||||
<view
|
||||
class="mode-item"
|
||||
:class="{ active: mode === 'reply' }"
|
||||
@click="mode = 'reply'"
|
||||
>
|
||||
回复建议
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="input-section">
|
||||
<view class="input-header">
|
||||
<text class="input-label">{{ mode === 'translate' ? '输入原文' : '客户询盘' }}</text>
|
||||
<picker :range="targetLangs" range-key="name" @change="onTargetChange">
|
||||
<text class="target-lang">{{ targetLangs[targetIndex].name }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<textarea
|
||||
class="input-area"
|
||||
v-model="inputText"
|
||||
:placeholder="mode === 'translate' ? '输入需要翻译的文本...' : '输入客户的询盘内容...'"
|
||||
@input="onInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="action-section">
|
||||
<button class="translate-btn" @click="handleTranslate" :disabled="loading">
|
||||
{{ loading ? '翻译中...' : '翻译' }}
|
||||
</button>
|
||||
<button class="clear-btn" @click="clearAll">清空</button>
|
||||
</view>
|
||||
|
||||
<view class="result-section" v-if="result">
|
||||
<view class="result-header">
|
||||
<text class="result-label">翻译结果</text>
|
||||
<text class="copy-btn" @click="copyResult">复制</text>
|
||||
</view>
|
||||
<view class="result-content">
|
||||
<text class="result-text">{{ result }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="suggestions-section" v-if="suggestions.length > 0">
|
||||
<view class="suggestions-header">
|
||||
<text class="suggestions-label">回复建议</text>
|
||||
</view>
|
||||
<view class="suggestions-list">
|
||||
<view
|
||||
class="suggestion-item"
|
||||
v-for="(item, index) in suggestions"
|
||||
:key="index"
|
||||
@click="selectSuggestion(index)"
|
||||
>
|
||||
<text class="suggestion-text">{{ item.text }}</text>
|
||||
<text class="suggestion-tone">{{ item.tone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="keyboard-height" :style="{ height: keyboardHeight + 'px' }"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { translateApi } from '@/utils/api.js'
|
||||
|
||||
const mode = ref('translate')
|
||||
const inputText = ref('')
|
||||
const result = ref('')
|
||||
const suggestions = ref([])
|
||||
const loading = ref(false)
|
||||
const targetIndex = ref(1)
|
||||
const keyboardHeight = ref(0)
|
||||
|
||||
const targetLangs = ref([
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'zh', name: '中文' },
|
||||
{ code: 'es', name: 'Español' },
|
||||
])
|
||||
|
||||
const onTargetChange = (e) => {
|
||||
targetIndex.value = e.detail.value
|
||||
}
|
||||
|
||||
const onInput = () => {
|
||||
// Real-time input handling
|
||||
}
|
||||
|
||||
const handleTranslate = async () => {
|
||||
if (!inputText.value.trim()) {
|
||||
uni.showToast({ title: '请输入内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
if (mode.value === 'translate') {
|
||||
const res = await translateApi.translate(
|
||||
inputText.value,
|
||||
targetLangs[targetIndex.value].code
|
||||
)
|
||||
result.value = res.translated
|
||||
} else {
|
||||
const res = await translateApi.getReply(inputText.value, 'professional', 3)
|
||||
suggestions.value = res.suggestions || []
|
||||
result.value = ''
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: err.message || '翻译失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
inputText.value = ''
|
||||
result.value = ''
|
||||
suggestions.value = []
|
||||
}
|
||||
|
||||
const copyResult = () => {
|
||||
uni.setClipboardData({
|
||||
data: result.value,
|
||||
success: () => {
|
||||
uni.showToast({ title: '已复制', icon: 'success' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const selectSuggestion = (index) => {
|
||||
const selected = suggestions.value[index]
|
||||
uni.setClipboardData({
|
||||
data: selected.text,
|
||||
success: () => {
|
||||
uni.showToast({ title: '已复制建议内容', icon: 'success' })
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.translate-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mode-item.active {
|
||||
color: #1890ff;
|
||||
border-bottom: 4rpx solid #1890ff;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.input-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.target-lang {
|
||||
font-size: 26rpx;
|
||||
color: #1890ff;
|
||||
padding: 8rpx 16rpx;
|
||||
background: #e6f7ff;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.translate-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 160rpx;
|
||||
height: 88rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid #d9d9d9;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
font-size: 24rpx;
|
||||
color: #1890ff;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
padding: 20rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.suggestions-section {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.suggestions-header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.suggestions-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.suggestions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 20rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.suggestion-text {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.suggestion-tone {
|
||||
font-size: 22rpx;
|
||||
color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.keyboard-height {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user