04f7ff0317
- 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
457 lines
9.9 KiB
Vue
457 lines
9.9 KiB
Vue
<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>
|