新增从顶部滑入的授权登录模态窗口组件

功能实现:
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:
H1d3rOne committed 2026-03-09 17:29:13 +08:00
1 parent d5ea80766f
commit ad47ee11e6
2 files changed
+369 -19

No files matched your search

+47 -19
View File
@@ -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>