ruoyi-geek-App/pages/login.vue

640 lines
14 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="login-header">
<image class="logo" src="/static/logo.png" mode="aspectFit"></image>
<text class="app-name">{{ appName }}</text>
</view>
<view class="login-content">
<!-- 登录方式切换 -->
<view class="login-tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'account' }"
@click="switchTab('account')"
>
账号登录
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'phone' }"
@click="switchTab('phone')"
>
手机登录
</view>
</view>
<!-- 账号密码登录 -->
<view v-if="activeTab === 'account'" class="login-form">
<view class="form-item">
<text class="label">账号</text>
<input
v-model="accountForm.username"
class="input"
placeholder="请输入用户名/手机号/邮箱"
placeholder-class="placeholder"
/>
</view>
<view class="form-item">
<text class="label">密码</text>
<input
v-model="accountForm.password"
class="input"
password
placeholder="请输入密码"
placeholder-class="placeholder"
@confirm="handleAccountLogin"
/>
</view>
<view class="form-item" v-if="accountForm.showCaptcha">
<text class="label">验证码</text>
<view class="captcha-input">
<input
v-model="accountForm.code"
class="input"
placeholder="请输入验证码"
placeholder-class="placeholder"
/>
<image
:src="accountForm.captchaImg"
class="captcha-img"
@click="getCaptcha"
mode="aspectFit"
></image>
</view>
</view>
<view class="form-options">
<text class="forget-pwd" @click="goForgetPwd">忘记密码?</text>
</view>
<button class="login-btn" @click="handleAccountLogin" :disabled="accountLogining">
{{ accountLogining ? '登录中...' : '登录' }}
</button>
</view>
<!-- 手机验证码登录 -->
<view v-if="activeTab === 'phone'" class="login-form">
<view class="form-item">
<text class="label">手机号</text>
<input
v-model="phoneForm.phone"
class="input"
type="number"
placeholder="请输入手机号"
placeholder-class="placeholder"
/>
</view>
<view class="form-item">
<text class="label">验证码</text>
<view class="code-input">
<input
v-model="phoneForm.code"
class="input"
type="number"
placeholder="请输入验证码"
placeholder-class="placeholder"
/>
<button
class="send-code-btn"
:disabled="phoneForm.countdown > 0"
@click="handlesendPhoneCode"
>
{{ phoneForm.countdown > 0 ? `${phoneForm.countdown}s` : '获取验证码' }}
</button>
</view>
</view>
<button class="login-btn" @click="handlePhoneLogin" :disabled="phoneLogining">
{{ phoneLogining ? '登录中...' : '登录' }}
</button>
</view>
<!-- 快捷登录方式 -->
<view class="quick-login">
<view class="divider">
<text class="divider-text">快捷登录</text>
</view>
<view class="login-methods">
<view class="method-item" @click="handleOneClickLogin">
<view class="method-icon one-click">
<text class="iconfont"></text>
</view>
<text class="method-text">本机一键登录</text>
</view>
<view class="method-item" @click="handleWechatLogin">
<view class="method-icon wechat">
<text class="iconfont">💬</text>
</view>
<text class="method-text">微信登录</text>
</view>
</view>
</view>
<!-- 注册链接 -->
<view class="register-link">
<text>还没有账号? </text>
<text class="link" @click="goRegister">立即注册</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import modal from '@/plugins/modal'
import { getCodeImg, login, sendPhoneCode, verifyPhoneCode } from '@/api/login'
import { getWxCode } from '@/utils/geek'
import { wxLogin } from '@/api/oauth'
import useUserStore from '@/store/modules/user'
import { setToken } from '@/utils/auth'
import config from '@/config.js'
const userStore = useUserStore()
const appName = config.appName || '系统名称'
// 响应式数据
const activeTab = ref('account')
const accountLogining = ref(false)
const phoneLogining = ref(false)
// 账号登录表单
const accountForm = reactive({
username: '',
password: '',
code: '',
captchaImg: '',
uuid: '',
showCaptcha: false
})
// 手机登录表单
const phoneForm = reactive({
phone: '',
code: '',
countdown: 0,
timer: null
})
// 切换登录方式
const switchTab = (tab) => {
activeTab.value = tab
if (tab === 'account' && !accountForm.captchaImg) {
getCaptcha()
}
}
// 获取验证码
const getCaptcha = async () => {
try {
const res = await getCodeImg()
accountForm.captchaImg = 'data:image/gif;base64,' + res.img
accountForm.uuid = res.uuid
accountForm.showCaptcha = true
} catch (error) {
modal.alert('验证码获取失败')
}
}
// 发送手机验证码
const handlesendPhoneCode = async () => {
if (!phoneForm.phone) {
modal.alert('请输入手机号')
return
}
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
modal.alert('请输入正确的手机号')
return
}
try {
await sendPhoneCode({ phone: phoneForm.phone }, 'login')
modal.alert('验证码已发送')
// 开始倒计时
phoneForm.countdown = 60
phoneForm.timer = setInterval(() => {
phoneForm.countdown--
if (phoneForm.countdown <= 0) {
clearInterval(phoneForm.timer)
}
}, 1000)
} catch (error) {
modal.alert(error.message || '验证码发送失败')
}
}
// 账号密码登录
const handleAccountLogin = async () => {
if (!accountForm.username) {
modal.alert('请输入账号')
return
}
if (!accountForm.password) {
modal.alert('请输入密码')
return
}
if (accountForm.showCaptcha && !accountForm.code) {
modal.alert('请输入验证码')
return
}
accountLogining.value = true
try {
const params = {
username: accountForm.username,
password: accountForm.password
}
if (accountForm.showCaptcha) {
params.code = accountForm.code
params.uuid = accountForm.uuid
}
const res = await login(params.username, params.password, params.code, params.uuid)
if (res.token) {
setToken(res.token)
await userStore.getInfo()
modal.alert('登录成功', () => {
uni.reLaunch({
url: '/pages/index'
})
})
}
} catch (error) {
// 如果需要验证码,重新获取
if (error.code === 401 || error.message?.includes('验证码')) {
getCaptcha()
}
modal.alert(error.message || '登录失败')
} finally {
accountLogining.value = false
}
}
// 手机验证码登录
const handlePhoneLogin = async () => {
if (!phoneForm.phone) {
modal.alert('请输入手机号')
return
}
if (!phoneForm.code) {
modal.alert('请输入验证码')
return
}
phoneLogining.value = true
try {
// 验证验证码
await verifyPhoneCode({
phone: phoneForm.phone,
code: phoneForm.code
}, 'login')
// 验证成功后执行登录
const res = await login(phoneForm.phone, '', '', '')
if (res.token) {
setToken(res.token)
await userStore.getInfo()
modal.alert('登录成功', () => {
uni.reLaunch({
url: '/pages/index'
})
})
}
} catch (error) {
modal.alert(error.message || '登录失败')
} finally {
phoneLogining.value = false
}
}
// 本机一键登录
const handleOneClickLogin = async () => {
try {
// 这里需要调用uni的一键登录API
// 由于不同平台实现不同,这里使用模拟实现
modal.confirm('是否使用本机号码一键登录?', async () => {
try {
// 获取本机号码实际项目中需要调用运营商SDK
const phoneNumber = '' // 从SDK获取
if (!phoneNumber) {
modal.alert('无法获取本机号码')
return
}
// 执行一键登录
const res = await login(phoneNumber, '', '', '')
if (res.token) {
setToken(res.token)
await userStore.getInfo()
modal.alert('登录成功', () => {
uni.reLaunch({
url: '/pages/index'
})
})
}
} catch (error) {
modal.alert(error.message || '一键登录失败')
}
})
} catch (error) {
modal.alert('一键登录功能暂不可用')
}
}
// 微信登录
const handleWechatLogin = async () => {
try {
const code = await getWxCode()
const res = await wxLogin('miniapp', code)
if (res.token) {
setToken(res.token)
await userStore.getInfo()
modal.alert('登录成功', () => {
uni.reLaunch({
url: '/pages/index'
})
})
}
} catch (error) {
if (error.code === 400) {
// 需要绑定微信
modal.confirm('首次使用微信登录,需要绑定账号', () => {
uni.navigateTo({
url: '/pages_system/pages/login/bind-account'
})
})
} else {
modal.alert(error.message || '微信登录失败')
}
}
}
// 跳转注册页面
const goRegister = () => {
uni.navigateTo({
url: '/pages_system/pages/register/index'
})
}
// 跳转忘记密码页面
const goForgetPwd = () => {
uni.navigateTo({
url: '/pages_system/pages/forget/index'
})
}
// 生命周期
onMounted(() => {
getCaptcha()
})
onLoad(() => {
// 检查是否有token如果有则直接跳转
const token = uni.getStorageSync('token')
if (token) {
uni.reLaunch({
url: '/pages/index'
})
}
})
</script>
<style scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60rpx 40rpx;
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 80rpx;
}
.logo {
width: 120rpx;
height: 120rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
}
.app-name {
font-size: 36rpx;
color: #fff;
font-weight: bold;
}
.login-content {
background: #fff;
border-radius: 20rpx;
padding: 40rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
}
.login-tabs {
display: flex;
margin-bottom: 40rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx;
font-size: 32rpx;
color: #999;
position: relative;
}
.tab-item.active {
color: #007aff;
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: -2rpx;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #007aff;
border-radius: 2rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.input {
width: 100%;
height: 80rpx;
border: 2rpx solid #e0e0e0;
border-radius: 10rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.placeholder {
color: #ccc;
}
.captcha-input, .code-input {
display: flex;
align-items: center;
gap: 20rpx;
}
.captcha-img {
width: 200rpx;
height: 80rpx;
border-radius: 10rpx;
border: 2rpx solid #e0e0e0;
}
.send-code-btn {
width: 200rpx;
height: 80rpx;
background: #007aff;
color: #fff;
border: none;
border-radius: 10rpx;
font-size: 24rpx;
white-space: nowrap;
}
.send-code-btn[disabled] {
background: #ccc;
}
.form-options {
display: flex;
justify-content: flex-end;
margin-bottom: 40rpx;
}
.forget-pwd {
color: #007aff;
font-size: 26rpx;
}
.login-btn {
width: 100%;
height: 88rpx;
background: #007aff;
color: #fff;
border: none;
border-radius: 44rpx;
font-size: 32rpx;
margin-bottom: 40rpx;
}
.login-btn[disabled] {
background: #ccc;
}
.quick-login {
margin-bottom: 40rpx;
}
.divider {
position: relative;
text-align: center;
margin: 40rpx 0;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1rpx;
background: #e0e0e0;
}
.divider-text {
background: #fff;
padding: 0 20rpx;
color: #999;
font-size: 24rpx;
}
.login-methods {
display: flex;
justify-content: center;
gap: 80rpx;
}
.method-item {
display: flex;
flex-direction: column;
align-items: center;
}
.method-icon {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
font-size: 40rpx;
}
.method-icon.one-click {
background: #ff9500;
color: #fff;
}
.method-icon.wechat {
background: #07c160;
color: #fff;
}
.method-text {
font-size: 24rpx;
color: #666;
}
.register-link {
text-align: center;
font-size: 26rpx;
color: #666;
}
.link {
color: #007aff;
}
</style>