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
+287
View File
@@ -0,0 +1,287 @@
<template>
<view class="login-container">
<view class="logo-section">
<text class="logo">TradeMate</text>
<text class="subtitle">外贸小助手</text>
</view>
<view class="form-section">
<text class="form-title">{{ isRegister ? '注册' : '登录' }}</text>
<view class="input-group">
<input
class="input"
type="number"
placeholder="手机号"
v-model="phone"
@input="onPhoneInput"
/>
</view>
<view class="input-group" v-if="isRegister">
<input
class="input"
type="text"
placeholder="用户名"
v-model="username"
@input="onUsernameInput"
/>
</view>
<view class="input-group">
<input
class="input"
type="password"
placeholder="密码"
v-model="password"
@input="onPasswordInput"
/>
</view>
<view class="error" v-if="error">{{ error }}</view>
<button
class="submit-btn"
@click="handleSubmit"
:disabled="loading"
>
{{ loading ? '处理中...' : (isRegister ? '注册' : '登录') }}
</button>
<text class="toggle-mode" @click="toggleMode">
{{ isRegister ? '已有账号立即登录' : '没有账号立即注册' }}
</text>
<view class="divider">
<view class="line"></view>
<text class="text"></text>
<view class="line"></view>
</view>
<button class="wechat-btn" @click="handleWechatLogin">
<text class="wechat-icon">W</text>
微信一键登录
</button>
</view>
<view class="footer">
<text class="agreement">登录即表示同意</text>
<text class="link">用户协议</text>
<text class="agreement"></text>
<text class="link">隐私政策</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { authApi } from '@/utils/api.js'
const phone = ref('')
const password = ref('')
const username = ref('')
const isRegister = ref(false)
const loading = ref(false)
const error = ref('')
const toggleMode = () => {
isRegister.value = !isRegister.value
error.value = ''
}
const handleSubmit = async () => {
if (!phone.value || !password.value) {
error.value = '请输入手机号和密码'
return
}
if (isRegister.value && !username.value) {
error.value = '请输入用户名'
return
}
loading.value = true
error.value = ''
try {
if (isRegister.value) {
await authApi.register(phone.value, password.value, username.value)
uni.showToast({ title: '注册成功,请登录', icon: 'success' })
isRegister.value = false
} else {
const res = await authApi.login(phone.value, password.value)
uni.setStorageSync('token', res.access_token)
uni.setStorageSync('userInfo', res.user)
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
}
} catch (err) {
error.value = err.message || '操作失败,请重试'
} finally {
loading.value = false
}
}
const handleWechatLogin = () => {
uni.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
console.log('微信登录', res.userInfo)
uni.showToast({ title: '微信登录开发中', icon: 'none' })
},
fail: (err) => {
console.log('微信登录失败', err)
}
})
}
const onPhoneInput = (e) => { phone.value = e.detail.value }
const onPasswordInput = (e) => { password.value = e.detail.value }
const onUsernameInput = (e) => { username.value = e.detail.value }
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(180deg, #1890ff 0%, #e6f7ff 100%);
padding: 120rpx 60rpx 60rpx;
box-sizing: border-box;
}
.logo-section {
text-align: center;
margin-bottom: 80rpx;
}
.logo {
font-size: 60rpx;
font-weight: bold;
color: #fff;
letter-spacing: 4rpx;
}
.subtitle {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-top: 16rpx;
}
.form-section {
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
}
.form-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 48rpx;
display: block;
}
.input-group {
margin-bottom: 32rpx;
}
.input {
width: 100%;
height: 96rpx;
background: #f5f5f5;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.error {
color: #ff4d4f;
font-size: 24rpx;
margin-bottom: 24rpx;
text-align: center;
display: block;
}
.submit-btn {
width: 100%;
height: 96rpx;
background: #1890ff;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.submit-btn[disabled] {
background: #a0cfff;
}
.toggle-mode {
display: block;
text-align: center;
margin-top: 32rpx;
color: #666;
font-size: 26rpx;
}
.divider {
display: flex;
align-items: center;
margin: 48rpx 0;
}
.divider .line {
flex: 1;
height: 1rpx;
background: #e8e8e8;
}
.divider .text {
padding: 0 24rpx;
color: #999;
font-size: 24rpx;
}
.wechat-btn {
width: 100%;
height: 96rpx;
background: #07c160;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.wechat-icon {
font-size: 36rpx;
font-weight: bold;
margin-right: 16rpx;
}
.footer {
text-align: center;
margin-top: 60rpx;
}
.agreement {
color: #999;
font-size: 24rpx;
}
.link {
color: #1890ff;
font-size: 24rpx;
}
</style>