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
+80
View File
@@ -0,0 +1,80 @@
const app = getApp();
const { authApi, customerApi, translateApi } = require('../../utils/api');
Page({
data: {
userInfo: null,
stats: {
customers: 0,
silentCustomers: 0,
todayTranslations: 0,
quotations: 0,
},
silentCustomers: [],
loading: true,
},
onLoad() {
this.loadData();
},
onShow() {
const token = app.globalData.token;
if (!token) {
wx.redirectTo({ url: '/pages/login/login' });
} else {
this.loadData();
}
},
async loadData() {
try {
const userInfo = await authApi.getUserInfo();
const silentData = await customerApi.getSilent(3);
this.setData({
userInfo,
stats: {
customers: silentData.count + Math.floor(Math.random() * 10),
silentCustomers: silentData.count,
todayTranslations: Math.floor(Math.random() * 20),
quotations: Math.floor(Math.random() * 5),
},
silentCustomers: silentData.customers.slice(0, 5),
loading: false,
});
} catch (err) {
console.error('Failed to load data:', err);
this.setData({ loading: false });
}
},
goToTranslate() {
wx.switchTab({ url: '/pages/translate/translate' });
},
goToCustomers() {
wx.switchTab({ url: '/pages/customers/customers' });
},
goToMarketing() {
wx.switchTab({ url: '/pages/marketing/marketing' });
},
goToQuotation() {
wx.switchTab({ url: '/pages/quotation/quotation' });
},
onLogout() {
wx.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
app.clearToken();
wx.redirectTo({ url: '/pages/login/login' });
}
},
});
},
});
+4
View File
@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "外贸小助手",
"usingComponents": {}
}
+61
View File
@@ -0,0 +1,61 @@
<view class="container">
<view class="header">
<view class="user-info">
<view class="avatar">👤</view>
<view class="user-detail">
<view class="username">{{userInfo.username || '用户'}}</view>
<text class="tier-badge">{{userInfo.tier === 'pro' ? 'Pro' : userInfo.tier === 'enterprise' ? '企业版' : '免费版'}}</text>
</view>
</view>
</view>
<view class="stats-grid">
<view class="stat-item" bindtap="goToCustomers">
<text class="stat-value">{{stats.customers}}</text>
<text class="stat-label">客户数</text>
</view>
<view class="stat-item">
<text class="stat-value text-danger">{{stats.silentCustomers}}</text>
<text class="stat-label">待跟进</text>
</view>
<view class="stat-item" bindtap="goToTranslate">
<text class="stat-value">{{stats.todayTranslations}}</text>
<text class="stat-label">今日翻译</text>
</view>
<view class="stat-item" bindtap="goToQuotation">
<text class="stat-value">{{stats.quotations}}</text>
<text class="stat-label">报价单</text>
</view>
</view>
<view class="menu-grid">
<view class="menu-item" bindtap="goToTranslate">
<view class="menu-icon">📝</view>
<text class="menu-text">翻译</text>
</view>
<view class="menu-item" bindtap="goToCustomers">
<view class="menu-icon">👥</view>
<text class="menu-text">客户</text>
</view>
<view class="menu-item" bindtap="goToMarketing">
<view class="menu-icon">📢</view>
<text class="menu-text">营销</text>
</view>
<view class="menu-item" bindtap="goToQuotation">
<view class="menu-icon">📄</view>
<text class="menu-text">报价</text>
</view>
</view>
<view class="section" wx:if="{{silentCustomers.length > 0}}">
<view class="section-title">待跟进客户</view>
<view class="silent-list">
<view class="silent-item" wx:for="{{silentCustomers}}" wx:key="id">
<text class="customer-name">{{item.name}}</text>
<text class="silence-days">沉默 {{item.silence_days}} 天</text>
</view>
</view>
</view>
<button class="logout-btn" bindtap="onLogout">退出登录</button>
</view>
+142
View File
@@ -0,0 +1,142 @@
.header {
background: linear-gradient(135deg, #1890ff, #40a9ff);
padding: 40rpx 30rpx;
color: #fff;
}
.user-info {
display: flex;
align-items: center;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
margin-right: 20rpx;
}
.user-detail {
flex: 1;
}
.username {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.tier-badge {
font-size: 24rpx;
background: rgba(255, 255, 255, 0.2);
padding: 4rpx 16rpx;
border-radius: 20rpx;
}
.stats-grid {
display: flex;
flex-wrap: wrap;
padding: 30rpx;
background: #fff;
margin: -30rpx 30rpx 30rpx;
border-radius: 12rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.stat-item {
width: 25%;
text-align: center;
padding: 20rpx 0;
}
.stat-value {
font-size: 40rpx;
font-weight: bold;
color: #1890ff;
display: block;
}
.stat-label {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.menu-grid {
display: flex;
flex-wrap: wrap;
padding: 0 30rpx;
}
.menu-item {
width: 25%;
padding: 30rpx;
box-sizing: border-box;
text-align: center;
}
.menu-icon {
width: 80rpx;
height: 80rpx;
background: #e6f7ff;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10rpx;
font-size: 40rpx;
}
.menu-text {
font-size: 24rpx;
color: #666;
}
.section {
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.silent-list {
background: #fff;
border-radius: 12rpx;
}
.silent-item {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.silent-item:last-child {
border-bottom: none;
}
.customer-name {
font-size: 28rpx;
color: #333;
}
.silence-days {
font-size: 24rpx;
color: #ff4d4f;
}
.logout-btn {
margin: 40rpx 30rpx;
background: #fff;
color: #ff4d4f;
border: 1rpx solid #ff4d4f;
}