增加手机号登录

This commit is contained in:
bb_pan 2025-11-07 13:11:21 +08:00
parent 822b5fada3
commit f9c1a3e82d
4 changed files with 304 additions and 162 deletions

View File

@ -19,6 +19,19 @@ export function login(username, password, code, uuid) {
})
}
// 手机号登录方法
export function loginByPhoneApi(data) {
return request({
url: '/loginByPhone',
headers: {
isToken: false,
repeatSubmit: false
},
method: 'post',
data: data
})
}
// 注册方法
export function register(data) {
return request({
@ -57,4 +70,4 @@ export function getCodeImg() {
method: 'get',
timeout: 20000
})
}
}

View File

@ -1,49 +1,67 @@
import router from '@/router'
import { ElMessageBox, } from 'element-plus'
import { login, logout, getInfo } from '@/api/login'
import { ElMessageBox } from 'element-plus'
import { login, logout, getInfo, loginByPhoneApi } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import { isHttp, isEmpty } from '@/utils/validate'
import defAva from '@/assets/images/profile.jpg'
const useUserStore = defineStore(
'user',
{
state: () => ({
token: getToken(),
id: '',
name: '',
nickName: '',
avatar: '',
roles: [],
permissions: []
}),
actions: {
// 登录
login(userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
const useUserStore = defineStore('user', {
state: () => ({
token: getToken(),
id: '',
name: '',
nickName: '',
avatar: '',
roles: [],
permissions: []
}),
actions: {
// 登录
login(userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid)
.then(res => {
setToken(res.token)
this.token = res.token
resolve()
}).catch(error => {
})
.catch(error => {
reject(error)
})
})
},
// 获取用户信息
getInfo() {
return new Promise((resolve, reject) => {
getInfo().then(res => {
})
},
// 手机号登录
loginByPhone(phoneInfo) {
const phone = phoneInfo.phone.trim()
const code = phoneInfo.code.trim()
return new Promise((resolve, reject) => {
loginByPhoneApi({ phone, code })
.then(res => {
setToken(res.token)
this.token = res.token
resolve()
})
.catch(error => {
reject(error)
})
})
},
// 获取用户信息
getInfo() {
return new Promise((resolve, reject) => {
getInfo()
.then(res => {
const user = res.user
let avatar = user.avatar || ""
let avatar = user.avatar || ''
if (!isHttp(avatar)) {
avatar = (isEmpty(avatar)) ? defAva : import.meta.env.VITE_APP_BASE_API + avatar
avatar = isEmpty(avatar) ? defAva : import.meta.env.VITE_APP_BASE_API + avatar
}
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
if (res.roles && res.roles.length > 0) {
// 验证返回的roles是否是一个非空数组
this.roles = res.roles
this.permissions = res.permissions
} else {
@ -54,38 +72,53 @@ const useUserStore = defineStore(
this.nickName = user.nickName
this.avatar = avatar
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
if (res.isDefaultModifyPwd) {
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
})
.catch(() => {})
}
/* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
if (!res.isDefaultModifyPwd && res.isPasswordExpired) {
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
})
.catch(() => {})
}
resolve(res)
}).catch(error => {
})
.catch(error => {
reject(error)
})
})
},
// 退出系统
logOut() {
return new Promise((resolve, reject) => {
logout(this.token).then(() => {
})
},
// 退出系统
logOut() {
return new Promise((resolve, reject) => {
logout(this.token)
.then(() => {
this.token = ''
this.roles = []
this.permissions = []
removeToken()
resolve()
}).catch(error => {
})
.catch(error => {
reject(error)
})
})
}
})
}
})
}
})
export default useUserStore

View File

@ -7,6 +7,7 @@ import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import useUserStore from '@/store/modules/user'
import { decryptWithSM4, encryptWithSM4, hashWithSM3AndSalt } from '@/utils/sm'
import router from '@/router'
let downloadLoadingInstance
// 是否显示重新登录
@ -106,7 +107,10 @@ service.interceptors.response.use(
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
// 判断当前是否在 login 页面
const isLogin = router.currentRoute.value.path == '/login'
console.log('🚀 ~ isLogin:', isLogin)
if (!isRelogin.show && !isLogin) {
isRelogin.show = true
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
confirmButtonText: '重新登录',
@ -118,7 +122,7 @@ service.interceptors.response.use(
useUserStore()
.logOut()
.then(() => {
location.href = '/index'
location.href = '/login'
})
})
.catch(() => {

View File

@ -2,73 +2,106 @@
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ title }}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
size="large"
auto-complete="off"
placeholder="账号"
>
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
size="large"
auto-complete="off"
placeholder="密码"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="large"
type="primary"
style="width:100%;"
@click.prevent="handleLogin"
>
<!-- 账号密码登录 -->
<template v-if="!isPhoneLogin">
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
size="large"
auto-complete="off"
placeholder="密码"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</el-form-item>
</template>
<!-- 手机号登录 -->
<template v-else>
<el-form-item prop="phone">
<el-input v-model="loginForm.phone" type="text" size="large" placeholder="手机号" maxlength="11">
<template #prefix><svg-icon icon-class="phone" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="smsCode">
<el-input v-model="loginForm.smsCode" size="large" placeholder="短信验证码" style="width: 63%">
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<el-button
:disabled="countdown > 0"
type="primary"
size="large"
style="width: 33%; margin-left: 4%"
@click="sendSmsCode"
>
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</el-button>
</el-form-item>
</template>
<el-checkbox v-if="!isPhoneLogin" v-model="loginForm.rememberMe" style="margin: 0px 0px 25px 0px">
记住密码
</el-checkbox>
<!-- 登录按钮 -->
<el-form-item style="width: 100%">
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<div style="float: right" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
<!-- 切换登录方式 -->
<el-form-item style="width: 100%">
<el-button size="large" type="" style="width: 100%" @click="toggleLoginType">
<span>{{ isPhoneLogin ? '使用账号登录' : '使用手机号登录' }}</span>
</el-button>
</el-form-item>
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span>
<!-- <span>Copyright © 2018-2025 ruoyi.vip All Rights Reserved.</span> -->
</div>
</div>
</template>
<script setup>
import { getCodeImg } from "@/api/login"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import { getCodeImg } from '@/api/login'
import Cookies from 'js-cookie'
import { encrypt, decrypt } from '@/utils/jsencrypt'
import useUserStore from '@/store/modules/user'
import { ElMessage } from 'element-plus'
import { Iphone } from '@element-plus/icons-vue'
const title = import.meta.env.VITE_APP_TITLE
const userStore = useUserStore()
@ -76,100 +109,159 @@ const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
//
const loginForm = ref({
username: "admin",
password: "admin123",
username: 'admin',
password: 'admin123',
rememberMe: false,
code: "",
uuid: ""
code: '',
uuid: '',
phone: '',
smsCode: ''
})
const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
}
//
const loginRules = reactive({
username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
code: [{ required: true, trigger: 'change', message: '请输入验证码' }],
phone: [{ required: true, trigger: 'blur', message: '请输入手机号' }],
smsCode: [{ required: true, trigger: 'blur', message: '请输入短信验证码' }]
})
const codeUrl = ref("")
const codeUrl = ref('')
const loading = ref(false)
//
const captchaEnabled = ref(true)
//
const register = ref(false)
const redirect = ref(undefined)
const isPhoneLogin = ref(false)
const countdown = ref(0)
let timer = null
watch(route, (newRoute) => {
watch(
route,
newRoute => {
redirect.value = newRoute.query && newRoute.query.redirect
}, { immediate: true })
},
{ immediate: true }
)
//
function handleLogin() {
proxy.$refs.loginRef.validate(valid => {
if (valid) {
loading.value = true
// cookie
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 })
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 })
if (!valid) return
loading.value = true
const formData = loginForm.value
//
if (!isPhoneLogin.value) {
if (formData.rememberMe) {
Cookies.set('username', formData.username, { expires: 30 })
Cookies.set('password', encrypt(formData.password), { expires: 30 })
Cookies.set('rememberMe', formData.rememberMe, { expires: 30 })
} else {
//
Cookies.remove("username")
Cookies.remove("password")
Cookies.remove("rememberMe")
Cookies.remove('username')
Cookies.remove('password')
Cookies.remove('rememberMe')
}
// action
userStore.login(loginForm.value).then(() => {
const query = route.query
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== "redirect") {
acc[cur] = query[cur]
}
return acc
}, {})
router.push({ path: redirect.value || "/", query: otherQueryParams })
}).catch(() => {
loading.value = false
//
if (captchaEnabled.value) {
getCode()
}
})
delete formData.phone
delete formData.smsCode
userStore.login(formData).then(handleLoginSuccess).catch(handleLoginFail)
}
//
else {
userStore
.loginByPhone({
phone: formData.phone,
code: formData.smsCode
})
.then(handleLoginSuccess)
.catch()
}
})
}
function handleLoginSuccess() {
const query = route.query
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== 'redirect') acc[cur] = query[cur]
return acc
}, {})
router.push({ path: redirect.value || '/', query: otherQueryParams })
}
function handleLoginFail() {
loading.value = false
if (captchaEnabled.value && !isPhoneLogin.value) getCode()
}
//
function toggleLoginType() {
isPhoneLogin.value = !isPhoneLogin.value
if (!isPhoneLogin.value && captchaEnabled.value) getCode()
}
//
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img
codeUrl.value = 'data:image/gif;base64,' + res.img
loginForm.value.uuid = res.uuid
}
})
}
function getCookie() {
const username = Cookies.get("username")
const password = Cookies.get("password")
const rememberMe = Cookies.get("rememberMe")
loginForm.value = {
username: username === undefined ? loginForm.value.username : username,
password: password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
//
const sendSmsCode = async () => {
if (!loginForm.value.phone) {
ElMessage.error('请输入手机号')
return
}
// API
try {
ElMessage.success('验证码已发送')
startCountdown()
} catch (error) {
console.log('🚀 ~ sendSmsCode ~ error:', error)
}
}
//
const startCountdown = () => {
countdown.value = 60
timer && clearInterval(timer)
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
}
// cookie
function getCookie() {
const username = Cookies.get('username')
const password = Cookies.get('password')
const rememberMe = Cookies.get('rememberMe')
loginForm.value.username = username || loginForm.value.username
loginForm.value.password = password ? decrypt(password) : loginForm.value.password
loginForm.value.rememberMe = rememberMe ? Boolean(rememberMe) : false
}
getCode()
getCookie()
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-image: url('../assets/images/login-background.jpg');
background-size: cover;
}
.title {