新增从顶部滑入的授权登录模态窗口组件
功能实现: 1. 模态窗口动画 - 从顶部平滑滑入效果 - 使用cubic-bezier缓动函数实现弹性动画 - 背景遮罩淡入效果 - 支持点击遮罩关闭 2. 微信授权流程 - 集成微信chooseAvatar API - 点击按钮选择微信头像或从相册选择 - 选择头像后弹出昵称输入框 - 自动完成授权登录 3. 用户信息获取 - 获取微信头像URL - 获取用户输入的昵称 - 调用uni.login获取code - 后端生成唯一用户ID 4. 数据处理 - 本地缓存:userId、userInfo、userCoins、vipLevel、username、userAvatar - 后端保存:通过API提交用户信息到数据库 - 数据同步:确保前后端数据一致 5. 错误处理 - 授权失败友好提示 - 用户取消授权确认对话框 - 网络错误处理 - 详细日志记录 6. 安全性 - 使用微信官方API - 符合微信开放平台安全规范 - 不直接暴露敏感信息 7. 用户体验 - 操作流程简洁直观 - 状态反馈清晰 - 授权成功自动关闭模态窗口 - 支持取消登录操作 组件特点: - 可复用的模态窗口组件 - 支持v-model双向绑定 - 提供success、cancel、error事件 - 优雅的动画效果 - 完善的错误处理
This commit is contained in:
1 parent
d5ea80766f
commit
ad47ee11e6
2 files changed
+369
-19
No files matched your search
@@ -1,5 +1,15 @@
|
||||
<script>
|
||||
import AuthModal from '@/components/auth-modal/auth-modal.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AuthModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAuthModal: false
|
||||
}
|
||||
},
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
this.checkUserAuth()
|
||||
@@ -21,33 +31,38 @@ export default {
|
||||
})
|
||||
|
||||
if (!userInfo && !hasShownAuth) {
|
||||
console.log('首次使用,跳转到授权页面')
|
||||
console.log('首次使用,显示授权模态窗口')
|
||||
uni.setStorageSync('hasShownAuth', true)
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/auth/auth',
|
||||
success: () => {
|
||||
console.log('跳转授权页面成功')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转授权页面失败:', err)
|
||||
uni.redirectTo({
|
||||
url: '/pages/auth/auth',
|
||||
success: () => {
|
||||
console.log('redirectTo跳转授权页面成功')
|
||||
},
|
||||
fail: (err2) => {
|
||||
console.error('redirectTo跳转授权页面也失败:', err2)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
this.showAuthModal = true
|
||||
}, 500)
|
||||
} else if (userInfo) {
|
||||
console.log('用户已登录')
|
||||
} else {
|
||||
console.log('用户跳过了授权')
|
||||
}
|
||||
},
|
||||
|
||||
onAuthSuccess(userInfo) {
|
||||
console.log('授权成功:', userInfo)
|
||||
this.showAuthModal = false
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
|
||||
onAuthCancel() {
|
||||
console.log('用户取消授权')
|
||||
this.showAuthModal = false
|
||||
},
|
||||
|
||||
onAuthError(error) {
|
||||
console.error('授权失败:', error)
|
||||
uni.showToast({
|
||||
title: '授权失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
globalData: {
|
||||
@@ -63,6 +78,19 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<router-view />
|
||||
<auth-modal
|
||||
:visible="showAuthModal"
|
||||
@success="onAuthSuccess"
|
||||
@cancel="onAuthCancel"
|
||||
@error="onAuthError"
|
||||
@update:visible="showAuthModal = $event"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 全局样式 */
|
||||
page {
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<view class="auth-modal-overlay" v-if="visible" @click="handleOverlayClick">
|
||||
<view class="auth-modal" :class="{ 'auth-modal-show': showAnimation }" @click.stop>
|
||||
<view class="modal-header">
|
||||
<view class="modal-icon">
|
||||
<text class="icon-text">🔐</text>
|
||||
</view>
|
||||
<text class="modal-title">授权登录</text>
|
||||
<text class="modal-subtitle">获取微信头像和昵称</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<button class="wechat-auth-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<view class="btn-icon">
|
||||
<image class="wechat-icon" src="/static/wechat-icon.png" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="btn-text">微信授权登录</text>
|
||||
</button>
|
||||
|
||||
<button class="cancel-btn" @click="handleCancel">
|
||||
<text class="cancel-btn-text">取消登录</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<text class="footer-text">授权后即可使用完整功能</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/utils/api.js'
|
||||
|
||||
export default {
|
||||
name: 'AuthModal',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAnimation: false,
|
||||
avatarUrl: '',
|
||||
nickname: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
setTimeout(() => {
|
||||
this.showAnimation = true
|
||||
}, 50)
|
||||
} else {
|
||||
this.showAnimation = false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleOverlayClick() {
|
||||
this.handleCancel()
|
||||
},
|
||||
|
||||
onChooseAvatar(e) {
|
||||
console.log('选择头像:', e)
|
||||
const { avatarUrl } = e.detail
|
||||
if (avatarUrl) {
|
||||
this.avatarUrl = avatarUrl
|
||||
this.showNicknameInput()
|
||||
}
|
||||
},
|
||||
|
||||
showNicknameInput() {
|
||||
uni.showModal({
|
||||
title: '设置昵称',
|
||||
editable: true,
|
||||
placeholderText: '请输入昵称',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
this.nickname = res.content
|
||||
this.doAuth()
|
||||
} else if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: '请输入昵称',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.showNicknameInput()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async doAuth() {
|
||||
if (!this.avatarUrl) {
|
||||
uni.showToast({ title: '请选择头像', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.nickname) {
|
||||
uni.showToast({ title: '请输入昵称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
if (!loginRes.code) {
|
||||
throw new Error('获取授权失败')
|
||||
}
|
||||
|
||||
console.log('获取code成功:', loginRes.code)
|
||||
|
||||
const res = await api.userApi.loginOrRegister({
|
||||
code: loginRes.code,
|
||||
nickname: this.nickname,
|
||||
avatarUrl: this.avatarUrl
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
console.log('登录成功:', res.data)
|
||||
|
||||
uni.setStorageSync('userId', res.data.userId)
|
||||
uni.setStorageSync('userInfo', JSON.stringify(res.data))
|
||||
uni.setStorageSync('userCoins', res.data.coins || 10)
|
||||
uni.setStorageSync('vipLevel', res.data.vipLevel || 0)
|
||||
uni.setStorageSync('username', res.data.username || res.data.nickname)
|
||||
uni.setStorageSync('userAvatar', res.data.avatarUrl)
|
||||
|
||||
uni.showToast({
|
||||
title: '授权成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
this.$emit('success', res.data)
|
||||
this.$emit('update:visible', false)
|
||||
} else {
|
||||
throw new Error(res.error || '登录失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('登录失败:', err)
|
||||
uni.showToast({
|
||||
title: err.message || '授权失败,请重试',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
this.$emit('error', err)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '取消登录将无法使用完整功能,确定要取消吗?',
|
||||
confirmText: '确定取消',
|
||||
cancelText: '继续登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.$emit('cancel')
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.auth-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-modal {
|
||||
width: 600rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-100vh);
|
||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.auth-modal-show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||||
padding: 48rpx 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 24rpx;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.wechat-auth-btn {
|
||||
background: linear-gradient(135deg, #07C160 0%, #06AD56 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(7, 193, 96, 0.3);
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wechat-auth-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: transparent;
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
border: 2rpx solid #D1D5DB;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cancel-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn-text {
|
||||
font-size: 32rpx;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
text-align: center;
|
||||
background: #F9FAFB;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user