consumer-app/pages/login/login.vue

617 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="login-page">
<!-- 状态栏占位 -->
<!-- <view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view> -->
<NavHeader title="登录" />
<!-- 登录内容区 -->
<view class="login-content">
<!-- Logo区域 -->
<view class="logo-section">
<image class="logo" src="/static/home/logo.png" mode="aspectFit"></image>
</view>
<!-- 登录表单 -->
<view class="login-form">
<!-- <view class="form-item">
<view class="input-wrapper">
<text class="input-label">手机号</text>
<input
class="input-field"
type="number"
v-model="formData.mobile"
placeholder="请输入手机号"
maxlength="11"
/>
</view>
</view> -->
<!-- <view class="form-item">
<view class="input-wrapper">
<text class="input-label">密码</text>
<input
class="input-field"
:type="showPassword ? 'text' : 'password'"
v-model="formData.password"
placeholder="请输入密码"
/>
<view class="password-toggle" @click="togglePassword">
<text class="toggle-icon">{{ showPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
</view> -->
<!-- 登录按钮 -->
<!-- <button
class="login-btn"
:class="{ disabled: !canLogin }"
:disabled="!canLogin || loading"
@click="handleLogin"
>
{{ loading ? '登录中...' : '登录' }}
</button> -->
<!-- 分割线 -->
<!-- <view class="divider">
<view class="divider-line"></view>
<text class="divider-text">或</text>
<view class="divider-line"></view>
</view> -->
<!-- 小程序一键授权登录 -->
<!-- #ifdef MP-WEIXIN -->
<button
class="wechat-login-btn"
open-type="getPhoneNumber"
@getphonenumber="handleGetPhoneNumber"
:disabled="loading"
>
<text>一键授权手机号快速登录</text>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="wechat-login-tip">
<text>小程序一键授权登录仅支持微信小程序环境</text>
</view>
<!-- #endif -->
</view>
</view>
</view>
</template>
<script>
import { login, loginByPhone } from '@/api/auth.js'
import { getUserInfo } from '@/api/profile.js'
export default {
data() {
return {
statusBarHeight: 0,
showBack: false,
loading: false,
showPassword: false,
formData: {
mobile: '',
password: ''
},
// 登录相关参数
state: '', // state 参数
inviteCode: '' // 邀请码
}
},
computed: {
// 判断是否可以登录
canLogin() {
return (
this.formData.mobile.length === 11 &&
this.formData.password.length > 0
)
}
},
onLoad(options) {
// 检查是否已登录
const token = uni.getStorageSync('token')
if (token) {
// 已登录,跳转到首页
uni.switchTab({
url: '/pages/index/index',
fail: () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
})
return
}
// 保存页面参数state 和 inviteCode
if (options.state) {
this.state = options.state
}
if (options.inviteCode) {
this.inviteCode = options.inviteCode
}
// 判断是否显示返回按钮(从其他页面跳转过来时显示)
const pages = getCurrentPages()
if (pages.length > 1) {
this.showBack = true
// 从其他页面跳转过来,说明可能有弹窗需要关闭
// 延迟一下,确保页面已经加载完成,弹窗会自动关闭
setTimeout(() => {
// 尝试隐藏可能存在的弹窗(虽然 uni 没有直接关闭 showModal 的 API
// 但页面跳转后,弹窗应该会自动关闭
}, 100)
}
this.getSystemInfo()
},
methods: {
// 获取系统信息
getSystemInfo() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight || 0
},
// 返回上一页
handleBack() {
uni.navigateBack()
},
// 切换密码显示/隐藏
togglePassword() {
this.showPassword = !this.showPassword
},
// 账户密码登录
async handleLogin() {
if (!this.canLogin) {
uni.showToast({
title: '请填写完整的登录信息',
icon: 'none'
})
return
}
// 验证手机号格式
const mobileReg = /^1[3-9]\d{9}$/
if (!mobileReg.test(this.formData.mobile)) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return
}
this.loading = true
try {
const res = await login({
mobile: this.formData.mobile,
password: this.formData.password
})
// 登录成功保存token兼容不同的响应格式
console.log('[登录] 登录接口返回数据:', res)
const token = res?.accessToken || res?.token || res?.data?.accessToken || res?.data?.token
if (token) {
uni.setStorageSync('token', token)
console.log('[登录] Token 已保存,长度:', token.length)
// 验证保存是否成功
const savedToken = uni.getStorageSync('token')
console.log('[登录] 验证保存的 token:', savedToken ? '成功' : '失败')
} else {
console.error('[登录] 未找到 token返回数据:', res)
}
// 保存refreshToken用于刷新accessToken
const refreshToken = res?.refreshToken || res?.data?.refreshToken
if (refreshToken) {
uni.setStorageSync('refreshToken', refreshToken)
}
// 保存token过期时间
const expiresTime = res?.expiresTime || res?.data?.expiresTime
if (expiresTime) {
uni.setStorageSync('tokenExpiresTime', expiresTime)
}
// 保存用户ID
const userId = res?.userId || res?.data?.userId
if (userId) {
uni.setStorageSync('userId', userId)
}
// 保存用户信息(如果有)
const userInfo = res?.userInfo || res?.data?.userInfo || res?.user || res?.data?.user
if (userInfo) {
uni.setStorageSync('userInfo', userInfo)
}
// 登录成功后,调用个人信息接口获取完整用户信息
try {
const userInfoRes = await getUserInfo()
if (userInfoRes && userInfoRes.data) {
// 根据接口返回的数据结构,保存 res.data
uni.setStorageSync('userInfo', userInfoRes.data)
} else if (userInfoRes) {
// 兼容直接返回数据的情况
uni.setStorageSync('userInfo', userInfoRes)
}
} catch (error) {
console.error('获取用户信息失败:', error)
// 获取用户信息失败不影响登录流程,继续执行
}
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 延迟跳转,让用户看到成功提示,统一跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index',
fail: () => {
// 如果 switchTab 失败(可能不在 tabBar 页面),使用 reLaunch
uni.reLaunch({
url: '/pages/index/index'
})
}
})
}, 1500)
} catch (error) {
console.error('登录失败:', error)
// 错误信息已在request中统一处理这里不需要再次提示
} finally {
this.loading = false
}
},
// 小程序一键授权手机号登录
async handleGetPhoneNumber(e) {
console.log('获取手机号授权结果:', e)
if (e.detail.errMsg === 'getPhoneNumber:ok') {
this.loading = true
try {
// 1. 获取微信临时登录code
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject
})
})
if (loginRes.errMsg !== 'login:ok') {
uni.showToast({
title: '获取登录凭证失败',
icon: 'none'
})
this.loading = false
return
}
// 2. 调用登录接口
// 根据接口文档:/app-api/member/auth/weixin-mini-app-login
// 参数phoneCode手机code、loginCode登录code、state必填回调用的随机值、inviteCode可选
// 生成随机 state 值(用于回调)
const state = this.generateState()
const loginParams = {
phoneCode: e.detail.code, // 手机 code, 通过 wx.getPhoneNumber 获得
loginCode: loginRes.code, // 登录 code, 通过 wx.login 获得
state: state, // state 是必填项,用于回调的随机值
}
// 如果有邀请码(例如从页面参数或存储中获取)
const inviteCode = this.getInviteCode()
if (inviteCode) {
loginParams.inviteCode = inviteCode
}
const res = await loginByPhone(loginParams)
// 登录成功保存token
// 根据接口返回:{ code: 0, data: { accessToken, refreshToken, expiresTime, userId, ... } }
// request 函数在 code === 0 时返回 res.data.data || res.data所以 res 就是 data 对象
console.log('[登录] 登录接口返回数据:', res)
const token = res?.accessToken || res?.data?.accessToken || res?.token || res?.data?.token
if (token) {
uni.setStorageSync('token', token)
console.log('[登录] Token 已保存,长度:', token.length)
// 验证保存是否成功
const savedToken = uni.getStorageSync('token')
console.log('[登录] 验证保存的 token:', savedToken ? '成功' : '失败')
} else {
console.error('[登录] 未找到 token返回数据:', res)
}
// 保存refreshToken用于刷新accessToken
const refreshToken = res?.refreshToken || res?.data?.refreshToken
if (refreshToken) {
uni.setStorageSync('refreshToken', refreshToken)
console.log('RefreshToken 已保存:', refreshToken)
}
// 保存token过期时间
const expiresTime = res?.expiresTime || res?.data?.expiresTime
if (expiresTime) {
uni.setStorageSync('tokenExpiresTime', expiresTime)
}
// 保存用户ID
const userId = res?.userId || res?.data?.userId
if (userId) {
uni.setStorageSync('userId', userId)
}
// 保存 openid如果有
const openid = res?.openid || res?.data?.openid
if (openid) {
uni.setStorageSync('openid', openid)
}
// 登录成功后,调用个人信息接口获取完整用户信息
try {
const userInfoRes = await getUserInfo()
if (userInfoRes && userInfoRes.data) {
// 根据接口返回的数据结构,保存 res.data
uni.setStorageSync('userInfo', userInfoRes.data)
} else if (userInfoRes) {
// 兼容直接返回数据的情况
uni.setStorageSync('userInfo', userInfoRes)
}
} catch (error) {
console.error('获取用户信息失败:', error)
// 获取用户信息失败不影响登录流程,继续执行
}
uni.showToast({
title: '登录成功',
icon: 'success'
})
// 延迟跳转,让用户看到成功提示,统一跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index',
fail: () => {
// 如果 switchTab 失败(可能不在 tabBar 页面),使用 reLaunch
uni.reLaunch({
url: '/pages/index/index'
})
}
})
}, 1500)
} catch (error) {
console.error('一键登录失败:', error)
// 错误信息已在request中统一处理
} finally {
this.loading = false
}
} else {
uni.showToast({
title: '授权失败,请重试',
icon: 'none'
})
}
},
// 生成随机 state 值(用于回调)
generateState() {
// 生成 UUID 格式的随机字符串类似9b2ffbc1-7425-4155-9894-9d5c08541d62
// 格式xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
const state = generateUUID()
// 保存 state 到本地存储,以便后续回调时使用
uni.setStorageSync('loginState', state)
return state
},
// 获取邀请码
getInviteCode() {
// 优先从页面参数获取
if (this.inviteCode) {
return this.inviteCode
}
// 也可以从本地存储获取
const storedInviteCode = uni.getStorageSync('inviteCode')
if (storedInviteCode) {
return storedInviteCode
}
// 也可以从全局变量获取
const app = getApp()
if (app && app.globalData && app.globalData.inviteCode) {
return app.globalData.inviteCode
}
return ''
}
}
}
</script>
<style lang="scss" scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(180deg, #e2e8f1 0%, #ffffff 100%);
}
.status-bar {
background-color: transparent;
}
.header {
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 20rpx 0;
background-color: transparent;
.back-btn {
position: absolute;
left: 30rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.back-icon {
font-size: 60rpx;
color: #333333;
line-height: 1;
}
}
.header-title {
font-size: 36rpx;
font-weight: 500;
color: #1a1819;
}
}
.login-content {
padding: 60rpx 60rpx 0;
}
.logo-section {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 120rpx;
margin-top: 80rpx;
.logo {
width: 306rpx;
height: 72rpx;
}
}
.login-form {
.form-item {
margin-bottom: 40rpx;
.input-wrapper {
position: relative;
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.input-label {
width: 120rpx;
font-size: 28rpx;
color: #333333;
font-weight: 500;
flex-shrink: 0;
}
.input-field {
flex: 1;
font-size: 28rpx;
color: #1a1819;
}
.password-toggle {
width: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.toggle-icon {
font-size: 32rpx;
}
}
}
}
.login-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #004294 0%, #0066cc 100%);
border-radius: 44rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border: none;
margin-top: 40rpx;
display: flex;
align-items: center;
justify-content: center;
&::after {
border: none;
}
&.disabled {
background: #cccccc;
color: #999999;
}
}
.divider {
display: flex;
align-items: center;
margin: 60rpx 0 40rpx;
padding: 0 20rpx;
.divider-line {
flex: 1;
height: 1rpx;
background-color: #e0e0e0;
}
.divider-text {
margin: 0 20rpx;
font-size: 24rpx;
color: #999999;
}
}
.wechat-login-btn {
width: 100%;
height: 88rpx;
background-color: #07c160;
border-radius: 44rpx;
color: #ffffff;
font-size: 28rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
&::after {
border: none;
}
.wechat-icon {
font-size: 36rpx;
}
}
.wechat-login-tip {
text-align: center;
padding: 40rpx 0;
font-size: 24rpx;
color: #999999;
}
}
</style>