Files
trade-assistant/uni-app/src/pages/login/login.vue
T
TradeMate Dev 04f7ff0317 fix: CORS/API 500 issues, switch to native tabbar, restore quick-actions
- Backend: guest UUID format fix, /auth/me guest branch, UUID validation in deps.py, CORS config fix
- Frontend: switch to native tabbar (custom: false), cleanup App.vue, redesign quick-actions with colored icons, conditional wechat login, proxy API requests via Vite
2026-05-13 17:54:13 +08:00

457 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="login-container">
<view class="welcome-section">
<text class="logo">TradeMate</text>
<text class="subtitle">外贸小助手</text>
<text class="slogan">让外贸更简单 · 让沟通更高效</text>
</view>
<view class="features-section">
<view class="feature-card">
<text class="feature-icon">🌐</text>
<text class="feature-title">智能翻译</text>
<text class="feature-desc">支持中英双语商务翻译精准理解外贸术语</text>
</view>
<view class="feature-card">
<text class="feature-icon">💬</text>
<text class="feature-title">智能回复</text>
<text class="feature-desc">AI 生成专业商务回复多种风格可选</text>
</view>
<view class="feature-card">
<text class="feature-icon">📊</text>
<text class="feature-title">客户管理</text>
<text class="feature-desc">客户信息跟进提醒数据分析一体化</text>
</view>
<view class="feature-card">
<text class="feature-icon">💰</text>
<text class="feature-title">报价单生成</text>
<text class="feature-desc">快速生成专业报价单支持导出 PDF</text>
</view>
</view>
<view class="actions-section">
<button class="try-btn" @click="goToQuickTry">
<text class="try-icon">🎯</text>
<text class="try-text">快速体验</text>
</button>
<view class="divider">
<view class="line"></view>
<text class="text">已有账号登录继续</text>
<view class="line"></view>
</view>
<view class="form-section" v-if="showForm">
<view class="input-group">
<input
class="input"
type="number"
placeholder="手机号"
v-model="phone"
/>
</view>
<view class="input-group" v-if="isRegister">
<input
class="input"
type="text"
placeholder="用户名"
v-model="username"
/>
</view>
<view class="input-group">
<input
class="input"
type="password"
placeholder="密码"
v-model="password"
/>
</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" v-if="isWechatAvailable">
<view class="line"></view>
<text class="text"></text>
<view class="line"></view>
</view>
<button class="wechat-btn" @click="handleWechatLogin" v-if="isWechatAvailable">
<text class="wechat-icon">W</text>
微信一键登录
</button>
</view>
<button class="show-login-btn" @click="toggleShowForm" v-if="!showForm">
登录 / 注册
</button>
</view>
<view class="footer">
<text class="agreement">登录即表示同意</text>
<text class="link" @click="goToAgreement('terms')">用户协议</text>
<text class="agreement"></text>
<text class="link" @click="goToAgreement('privacy')">隐私政策</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 showForm = ref(false)
const isWechatAvailable = ref(false)
// #ifdef MP-WEIXIN
isWechatAvailable.value = true
// #endif
const toggleMode = () => {
isRegister.value = !isRegister.value
error.value = ''
}
const toggleShowForm = () => {
showForm.value = !showForm.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.setStorageSync('hasLogin', true)
uni.setStorageSync('isGuest', false)
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.login({
provider: 'weixin',
success: async (loginRes) => {
try {
loading.value = true
const res = await authApi.wechatLogin(loginRes.code)
uni.setStorageSync('token', res.access_token)
uni.setStorageSync('userInfo', res.user)
uni.setStorageSync('hasLogin', true)
uni.setStorageSync('isGuest', false)
uni.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
}, 1000)
} catch (err) {
error.value = err.message || '微信登录失败'
} finally {
loading.value = false
}
},
fail: (err) => {
console.log('微信登录失败', err)
error.value = '微信登录取消或失败'
}
})
}
const goToAgreement = (type) => {
uni.navigateTo({ url: `/pages/agreement/${type}` })
}
const goToQuickTry = async () => {
uni.setStorageSync('isGuest', true)
try {
const res = await authApi.guestLogin()
if (res.access_token) {
uni.setStorageSync('token', res.access_token)
if (res.user) {
uni.setStorageSync('userInfo', res.user)
}
}
} catch (e) {
console.log('Guest login failed, continuing without token:', e)
}
uni.switchTab({ url: '/pages/index/index' })
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 50%);
padding: 40rpx 40rpx 60rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.welcome-section {
text-align: center;
margin-bottom: 40rpx;
padding-top: 20rpx;
}
.logo {
font-size: 56rpx;
font-weight: bold;
color: #1890ff;
letter-spacing: 4rpx;
display: block;
}
.subtitle {
font-size: 28rpx;
color: #666;
margin-top: 8rpx;
display: block;
}
.slogan {
font-size: 24rpx;
color: #999;
margin-top: 16rpx;
display: block;
}
.features-section {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 40rpx;
}
.feature-card {
background: #fff;
border-radius: 20rpx;
padding: 32rpx 24rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(24, 144, 255, 0.08);
border: 2rpx solid #e6f7ff;
}
.feature-icon {
font-size: 48rpx;
display: block;
margin-bottom: 16rpx;
}
.feature-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.feature-desc {
font-size: 22rpx;
color: #999;
line-height: 1.5;
display: block;
}
.actions-section {
flex: 1;
display: flex;
flex-direction: column;
}
.try-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #69c0ff 100%);
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3);
margin-bottom: 24rpx;
}
.try-icon {
font-size: 36rpx;
margin-right: 12rpx;
}
.try-text {
color: #fff;
}
.divider {
display: flex;
align-items: center;
margin: 24rpx 0;
}
.divider .line {
flex: 1;
height: 1rpx;
background: #e8e8e8;
}
.divider .text {
padding: 0 24rpx;
color: #bbb;
font-size: 24rpx;
}
.form-section {
background: #fff;
border-radius: 24rpx;
padding: 40rpx 32rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05);
}
.show-login-btn {
width: 100%;
height: 88rpx;
background: #fff;
color: #1890ff;
border: 2rpx solid #1890ff;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 500;
}
.input-group {
margin-bottom: 28rpx;
}
.input {
width: 100%;
height: 96rpx;
background: #fafafa;
border-radius: 16rpx;
padding: 0 28rpx;
font-size: 28rpx;
box-sizing: border-box;
border: 2rpx solid transparent;
&:focus {
border-color: #1890ff;
background: #fff;
}
}
.error {
color: #ff4d4f;
font-size: 24rpx;
margin-bottom: 20rpx;
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: 28rpx;
color: #1890ff;
font-size: 26rpx;
}
.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;
margin-top: 20rpx;
}
.wechat-icon {
font-size: 36rpx;
font-weight: bold;
margin-right: 12rpx;
}
.footer {
text-align: center;
margin-top: 40rpx;
padding-top: 20rpx;
}
.agreement {
color: #999;
font-size: 22rpx;
}
.link {
color: #1890ff;
font-size: 22rpx;
}
</style>