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:
TradeMate Dev
2026-05-08 18:17:12 +08:00
commit c6206787da
121 changed files with 11743 additions and 0 deletions
+319
View File
@@ -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>