Files
trade-assistant/uni-app/src/pages/login/login.vue
T
TradeMate Dev 4755cc75ba feat: 管理后台完整可用 + 注册登录记日志 + 提取信息结构化展示 + 微信配置就绪
- 管理后台用户/统计/日志/配置四页签全部对接真实后端API
- auth注册/登录/游客/微信登录事件写入usage_logs表
- 提取信息结果从原始JSON改为卡片式字段列表(中文标签)
- 管理后台搜索按钮增加加载态和结果数提示
- 配置WECHAT_APP_ID/WECHAT_APP_SECRET
- 客户/产品/报价单CRUD页面完整(导出导入批量操作)
2026-05-18 23:50:48 +08:00

532 lines
12 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="silent-loading" v-if="silentLoading">
<text class="silent-loading-text">正在自动登录...</text>
</view>
<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="text"
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, onMounted } 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 silentLoading = ref(true)
const error = ref('')
const showForm = ref(false)
const isWechatAvailable = ref(false)
const doWechatLogin = async (code) => {
const res = await authApi.wechatLogin(code)
uni.setStorageSync('token', res.access_token)
uni.setStorageSync('userInfo', res.user)
uni.setStorageSync('hasLogin', true)
uni.setStorageSync('isGuest', false)
uni.switchTab({ url: '/pages/index/index' })
}
onMounted(async () => {
// #ifdef MP-WEIXIN
// 微信小程序:静默登录
isWechatAvailable.value = true
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
await doWechatLogin(loginRes.code)
} catch (_) {
silentLoading.value = false
}
// #endif
// #ifdef H5
// H5 微信内置浏览器:OAuth 静默登录
const isWechatBrowser = /MicroMessenger/i.test(navigator.userAgent)
if (isWechatBrowser) {
try {
const cfg = await authApi.wechatConfig()
if (cfg.available && cfg.app_id) {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
if (code) {
// OAuth 回调,携带 code
await doWechatLogin(code)
// 清除 URL 中的 code 参数
window.history.replaceState({}, '', window.location.pathname)
return
} else {
// 跳转微信 OAuth 授权
const redirectUri = encodeURIComponent(window.location.href.split('?')[0])
window.location.href =
`https://open.weixin.qq.com/connect/oauth2/authorize` +
`?appid=${cfg.app_id}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code` +
`&scope=snsapi_base` +
`&state=STATE#wechat_redirect`
return
}
}
} catch (_) {}
}
silentLoading.value = false
// #endif
// #ifndef MP-WEIXIN
if (!/MicroMessenger/i.test(navigator.userAgent)) {
silentLoading.value = false
}
// #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.reLaunch({ url: '/pages/index/index' })
}
} catch (err) {
console.error('登录失败', err)
error.value = (err.errMsg || err.message || '操作失败,请重试')
if (err.statusCode === 401) {
error.value = '手机号或密码错误'
}
} finally {
loading.value = false
}
}
const handleWechatLogin = () => {
uni.login({
provider: 'weixin',
success: async (loginRes) => {
try {
loading.value = true
await doWechatLogin(loginRes.code)
} 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;
}
.silent-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.silent-loading-text {
font-size: 28rpx;
color: #999;
}
.footer {
text-align: center;
margin-top: 40rpx;
padding-top: 20rpx;
}
.agreement {
color: #999;
font-size: 22rpx;
}
.link {
color: #1890ff;
font-size: 22rpx;
}
</style>