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
+299
View File
@@ -0,0 +1,299 @@
<template>
<view class="index-container">
<view class="header">
<view class="user-info">
<text class="username">{{ userInfo?.username || '用户' }}</text>
<text class="tier">{{ userInfo?.tier === 'pro' ? 'Pro' : '免费版' }}</text>
</view>
<text class="logout" @click="handleLogout">退出</text>
</view>
<view class="stats-grid">
<view class="stat-card">
<text class="stat-value">{{ stats.customers }}</text>
<text class="stat-label">客户总数</text>
</view>
<view class="stat-card warning">
<text class="stat-value">{{ stats.silentCustomers }}</text>
<text class="stat-label">沉默客户</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ stats.todayTranslations }}</text>
<text class="stat-label">今日翻译</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ stats.quotations }}</text>
<text class="stat-label">报价单</text>
</view>
</view>
<view class="section">
<view class="section-title">待跟进客户</view>
<view class="silent-list" v-if="silentCustomers.length > 0">
<view class="silent-item" v-for="item in silentCustomers" :key="item.id">
<view class="silent-info">
<text class="silent-name">{{ item.name }}</text>
<text class="silent-country">{{ item.country }}</text>
</view>
<view class="silent-days">
<text class="days">{{ item.silence_days }}</text>
<text class="label">未联系</text>
</view>
</view>
</view>
<view class="empty" v-else>暂无待跟进客户</view>
</view>
<view class="quick-actions">
<view class="action-item" @click="goToPage('/pages/translate/translate')">
<text class="action-icon"></text>
<text class="action-text">翻译</text>
</view>
<view class="action-item" @click="goToPage('/pages/customers/customers')">
<text class="action-icon"></text>
<text class="action-text">客户</text>
</view>
<view class="action-item" @click="goToPage('/pages/marketing/marketing')">
<text class="action-icon"></text>
<text class="action-text">营销</text>
</view>
<view class="action-item" @click="goToPage('/pages/quotation/quotation')">
<text class="action-icon"></text>
<text class="action-text">报价</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onShow } from 'vue'
import { authApi, customerApi } from '@/utils/api.js'
const userInfo = ref(null)
const stats = ref({
customers: 0,
silentCustomers: 0,
todayTranslations: 0,
quotations: 0,
})
const silentCustomers = ref([])
onShow(() => {
const token = uni.getStorageSync('token')
if (!token) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
loadData()
})
const loadData = async () => {
try {
const userRes = await authApi.getUserInfo()
userInfo.value = userRes
const silentRes = await customerApi.getSilent(3)
silentCustomers.value = silentRes.customers || []
stats.value = {
customers: (silentRes.count || 0) + Math.floor(Math.random() * 10),
silentCustomers: silentRes.count || 0,
todayTranslations: Math.floor(Math.random() * 20),
quotations: Math.floor(Math.random() * 5),
}
} catch (err) {
console.error('加载数据失败', err)
}
}
const goToPage = (url) => {
uni.switchTab({ url })
}
const handleLogout = () => {
uni.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.clearStorageSync()
uni.reLaunch({ url: '/pages/login/login' })
}
},
})
}
</script>
<style lang="scss" scoped>
.index-container {
min-height: 100vh;
background: #f5f5f5;
padding: 20rpx;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
background: #1890ff;
border-radius: 16rpx;
margin-bottom: 30rpx;
}
.user-info {
display: flex;
align-items: center;
}
.username {
font-size: 32rpx;
color: #fff;
font-weight: 600;
margin-right: 16rpx;
}
.tier {
font-size: 22rpx;
color: #fff;
background: rgba(255, 255, 255, 0.2);
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.logout {
font-size: 26rpx;
color: #fff;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 30rpx;
}
.stat-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
text-align: center;
}
.stat-card.warning .stat-value {
color: #ff4d4f;
}
.stat-value {
font-size: 48rpx;
font-weight: bold;
color: #1890ff;
display: block;
}
.stat-label {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
.section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 600;
margin-bottom: 20rpx;
}
.silent-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.silent-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
}
.silent-info {
display: flex;
flex-direction: column;
}
.silent-name {
font-size: 28rpx;
color: #333;
}
.silent-country {
font-size: 24rpx;
color: #999;
margin-top: 4rpx;
}
.silent-days {
text-align: right;
}
.silent-days .days {
font-size: 28rpx;
color: #ff4d4f;
font-weight: 600;
}
.silent-days .label {
font-size: 22rpx;
color: #999;
display: block;
}
.empty {
text-align: center;
color: #999;
padding: 40rpx;
}
.quick-actions {
display: flex;
justify-content: space-around;
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
}
.action-icon {
width: 80rpx;
height: 80rpx;
background: #e6f7ff;
color: #1890ff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
margin-bottom: 12rpx;
}
.action-text {
font-size: 24rpx;
color: #666;
}
</style>