mi-assessment/uniapp/pages/login/index.vue
zpc ddf1a092b1 feat(user): 登录后获取用户信息、App启动刷新、我的页面下拉刷新
- 用户store新增fetchUserInfo action,调用/userInfo接口获取完整用户信息
- 登录页修复:LoginResponse只有token和userId,登录成功后调用fetchUserInfo获取资料
- App.vue启动时若已登录自动刷新用户信息
- 我的页面onShow时刷新用户信息,新增下拉刷新支持
- pages.json为我的页面启用enablePullDownRefresh
2026-02-20 22:56:47 +08:00

387 lines
8.4 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-page">
<!-- 自定义导航栏 -->
<Navbar title="登录" :showBack="true" backgroundColor="#FFFFFF" />
<!-- 主内容区域 -->
<view class="login-content">
<!-- Logo区域 -->
<view class="logo-section">
<view class="logo-wrapper">
<image src="/static/logo.png" mode="aspectFit" class="logo-img" />
</view>
</view>
<!-- 底部登录区域 -->
<view class="bottom-section">
<!-- 登录按钮 -->
<button
class="btn-login"
:class="{ 'btn-disabled': !isAgreed }"
:disabled="!isAgreed"
open-type="getPhoneNumber"
@getphonenumber="handleGetPhoneNumber"
>
一键注册/登录
</button>
<!-- 协议勾选 -->
<view class="agreement-section" @click="toggleAgreement">
<view class="checkbox" :class="{ 'checkbox-checked': isAgreed }">
<image v-if="isAgreed" src="/static/ic_check_s.png" class="checkbox-icon" mode="aspectFit" />
</view>
<view class="agreement-text">
<text>注册即同意我们的</text>
<text class="link" @click.stop="goUserAgreement">《用户协议》</text>
<text>与</text>
<text class="link" @click.stop="goPrivacyPolicy">隐私政策</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { post } from '@/api/request.js'
import Navbar from '@/components/Navbar/index.vue'
const userStore = useUserStore()
// 是否同意协议
const isAgreed = ref(false)
// 登录后重定向地址
const redirectUrl = ref('')
/**
* 切换协议勾选状态
*/
function toggleAgreement() {
isAgreed.value = !isAgreed.value
}
/**
* 跳转用户协议
*/
function goUserAgreement() {
uni.navigateTo({
url: '/pages/agreement/user/index'
})
}
/**
* 跳转隐私政策
*/
function goPrivacyPolicy() {
uni.navigateTo({
url: '/pages/agreement/privacy/index'
})
}
/**
* 处理获取手机号回调
* @param {Object} e - 回调事件对象
*/
async function handleGetPhoneNumber(e) {
if (!isAgreed.value) {
uni.showToast({
title: '请先同意用户协议和隐私政策',
icon: 'none'
})
return
}
// 用户拒绝授权
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({
title: '需要授权手机号才能登录',
icon: 'none'
})
return
}
try {
// 1. 获取微信登录code
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject
})
})
if (!loginRes.code) {
uni.showToast({ title: '获取登录凭证失败', icon: 'none' })
return
}
uni.showLoading({ title: '登录中...' })
// 2. 调用后端登录接口,传递 code 和手机号 code
const res = await post('/login', {
code: loginRes.code,
phoneCode: e.detail.code // 手机号授权 code
}, { needAuth: false })
uni.hideLoading()
if (res && res.code === 0 && res.data) {
// 保存登录信息LoginResponse 只有 token 和 userId
userStore.login({
token: res.data.token,
refreshToken: res.data.refreshToken,
userInfo: {
userId: res.data.userId
}
})
// 登录成功后获取完整用户信息
await userStore.fetchUserInfo()
uni.showToast({ title: '登录成功', icon: 'success' })
// 延迟返回
setTimeout(() => {
if (redirectUrl.value) {
uni.redirectTo({
url: redirectUrl.value,
fail: () => {
uni.switchTab({
url: redirectUrl.value,
fail: () => {
uni.navigateBack()
}
})
}
})
} else {
uni.navigateBack()
}
}, 1000)
} else {
uni.showToast({
title: res?.message || '登录失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('登录失败:', error)
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
}
}
/**
* 处理登录(备用方法,用于不需要手机号的场景)
*/
async function handleLogin() {
if (!isAgreed.value) {
uni.showToast({
title: '请先同意用户协议和隐私政策',
icon: 'none'
})
return
}
try {
// 1. 获取微信登录code
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject
})
})
if (!loginRes.code) {
uni.showToast({ title: '获取登录凭证失败', icon: 'none' })
return
}
uni.showLoading({ title: '登录中...' })
// 2. 调用后端登录接口
const res = await post('/login', {
code: loginRes.code
}, { needAuth: false })
uni.hideLoading()
if (res && res.code === 0 && res.data) {
// 保存登录信息LoginResponse 只有 token 和 userId
userStore.login({
token: res.data.token,
refreshToken: res.data.refreshToken,
userInfo: {
userId: res.data.userId
}
})
// 登录成功后获取完整用户信息
await userStore.fetchUserInfo()
uni.showToast({ title: '登录成功', icon: 'success' })
// 延迟返回
setTimeout(() => {
if (redirectUrl.value) {
uni.redirectTo({
url: redirectUrl.value,
fail: () => {
uni.switchTab({
url: redirectUrl.value,
fail: () => {
uni.navigateBack()
}
})
}
})
} else {
uni.navigateBack()
}
}, 1000)
} else {
uni.showToast({
title: res?.message || '登录失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('登录失败:', error)
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
}
}
/**
* 页面加载
*/
onMounted(() => {
// 获取重定向参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
if (options.redirect) {
redirectUrl.value = decodeURIComponent(options.redirect)
}
})
</script>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
// 登录按钮橙色(设计图配色)
$login-btn-color: #F5A623;
$login-btn-active: #E09518;
.login-page {
min-height: 100vh;
background-color: $bg-white;
display: flex;
flex-direction: column;
}
.login-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 60rpx;
}
.logo-section {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
.logo-wrapper {
width: 360rpx;
height: 360rpx;
border: 2rpx solid $border-color;
border-radius: $border-radius-md;
display: flex;
align-items: center;
justify-content: center;
.logo-img {
width: 280rpx;
height: 280rpx;
}
}
}
.bottom-section {
padding-bottom: 120rpx;
.btn-login {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: $login-btn-color;
border-radius: 44rpx;
font-size: $font-size-lg;
color: $text-white;
border: none;
font-weight: $font-weight-medium;
letter-spacing: 2rpx;
&::after {
border: none;
}
&:active {
background-color: $login-btn-active;
}
}
.btn-disabled {
background-color: #CCCCCC;
pointer-events: none;
}
}
.agreement-section {
display: flex;
align-items: center;
justify-content: center;
margin-top: 32rpx;
.checkbox {
width: 28rpx;
height: 28rpx;
border: 2rpx solid $border-color;
border-radius: 50%;
margin-right: 10rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
&-checked {
border-color: $success-color;
background-color: $success-color;
}
.checkbox-icon {
width: 28rpx;
height: 28rpx;
}
}
.agreement-text {
font-size: $font-size-sm;
color: $text-placeholder;
line-height: 1.6;
.link {
color: $primary-color;
}
}
}
</style>