增加手机号登录

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) { export function register(data) {
return request({ return request({

View File

@ -1,13 +1,11 @@
import router from '@/router' import router from '@/router'
import { ElMessageBox, } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { login, logout, getInfo } from '@/api/login' import { login, logout, getInfo, loginByPhoneApi } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth' 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' import defAva from '@/assets/images/profile.jpg'
const useUserStore = defineStore( const useUserStore = defineStore('user', {
'user',
{
state: () => ({ state: () => ({
token: getToken(), token: getToken(),
id: '', id: '',
@ -25,11 +23,29 @@ const useUserStore = defineStore(
const code = userInfo.code const code = userInfo.code
const uuid = userInfo.uuid const uuid = userInfo.uuid
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => { login(username, password, code, uuid)
.then(res => {
setToken(res.token) setToken(res.token)
this.token = res.token this.token = res.token
resolve() resolve()
}).catch(error => { })
.catch(error => {
reject(error)
})
})
},
// 手机号登录
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) reject(error)
}) })
}) })
@ -37,13 +53,15 @@ const useUserStore = defineStore(
// 获取用户信息 // 获取用户信息
getInfo() { getInfo() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getInfo().then(res => { getInfo()
.then(res => {
const user = res.user const user = res.user
let avatar = user.avatar || "" let avatar = user.avatar || ''
if (!isHttp(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.roles = res.roles
this.permissions = res.permissions this.permissions = res.permissions
} else { } else {
@ -54,19 +72,32 @@ const useUserStore = defineStore(
this.nickName = user.nickName this.nickName = user.nickName
this.avatar = avatar this.avatar = avatar
/* 初始密码提示 */ /* 初始密码提示 */
if(res.isDefaultModifyPwd) { if (res.isDefaultModifyPwd) {
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {}) })
.catch(() => {})
} }
/* 过期密码提示 */ /* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) { if (!res.isDefaultModifyPwd && res.isPasswordExpired) {
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } }) router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {}) })
.catch(() => {})
} }
resolve(res) resolve(res)
}).catch(error => { })
.catch(error => {
reject(error) reject(error)
}) })
}) })
@ -74,18 +105,20 @@ const useUserStore = defineStore(
// 退出系统 // 退出系统
logOut() { logOut() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logout(this.token).then(() => { logout(this.token)
.then(() => {
this.token = '' this.token = ''
this.roles = [] this.roles = []
this.permissions = [] this.permissions = []
removeToken() removeToken()
resolve() resolve()
}).catch(error => { })
.catch(error => {
reject(error) reject(error)
}) })
}) })
} }
} }
}) })
export default useUserStore export default useUserStore

View File

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

View File

@ -2,17 +2,15 @@
<div class="login"> <div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ title }}</h3> <h3 class="title">{{ title }}</h3>
<!-- 账号密码登录 -->
<template v-if="!isPhoneLogin">
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
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> <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input
v-model="loginForm.password" v-model="loginForm.password"
@ -25,6 +23,7 @@
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template> <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="code" v-if="captchaEnabled"> <el-form-item prop="code" v-if="captchaEnabled">
<el-input <el-input
v-model="loginForm.code" v-model="loginForm.code"
@ -37,38 +36,72 @@
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template> <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input> </el-input>
<div class="login-code"> <div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/> <img :src="codeUrl" @click="getCode" class="login-code-img" />
</div> </div>
</el-form-item> </el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> </template>
<el-form-item style="width:100%;">
<!-- 手机号登录 -->
<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 <el-button
:loading="loading" :disabled="countdown > 0"
size="large"
type="primary" type="primary"
style="width:100%;" size="large"
@click.prevent="handleLogin" 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-if="!loading"> </span>
<span v-else> 中...</span> <span v-else> 中...</span>
</el-button> </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> <router-link class="link-type" :to="'/register'">立即注册</router-link>
</div> </div>
</el-form-item> </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> </el-form>
<!-- 底部 -->
<div class="el-login-footer"> <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>
</div> </div>
</template> </template>
<script setup> <script setup>
import { getCodeImg } from "@/api/login" import { getCodeImg } from '@/api/login'
import Cookies from "js-cookie" import Cookies from 'js-cookie'
import { encrypt, decrypt } from "@/utils/jsencrypt" import { encrypt, decrypt } from '@/utils/jsencrypt'
import useUserStore from '@/store/modules/user' 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 title = import.meta.env.VITE_APP_TITLE
const userStore = useUserStore() const userStore = useUserStore()
@ -76,100 +109,159 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
//
const loginForm = ref({ const loginForm = ref({
username: "admin", username: 'admin',
password: "admin123", password: 'admin123',
rememberMe: false, rememberMe: false,
code: "", code: '',
uuid: "" uuid: '',
phone: '',
smsCode: ''
}) })
const loginRules = { //
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }], const loginRules = reactive({
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }], username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
code: [{ required: true, trigger: "change", 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 loading = ref(false)
//
const captchaEnabled = ref(true) const captchaEnabled = ref(true)
//
const register = ref(false) const register = ref(false)
const redirect = ref(undefined) 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 redirect.value = newRoute.query && newRoute.query.redirect
}, { immediate: true }) },
{ immediate: true }
)
//
function handleLogin() { function handleLogin() {
proxy.$refs.loginRef.validate(valid => { proxy.$refs.loginRef.validate(valid => {
if (valid) { if (!valid) return
loading.value = true loading.value = true
// cookie const formData = loginForm.value
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 }) //
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 }) if (!isPhoneLogin.value) {
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 }) 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 { } else {
// Cookies.remove('username')
Cookies.remove("username") Cookies.remove('password')
Cookies.remove("password") Cookies.remove('rememberMe')
Cookies.remove("rememberMe")
} }
// action delete formData.phone
userStore.login(loginForm.value).then(() => { delete formData.smsCode
const query = route.query userStore.login(formData).then(handleLoginSuccess).catch(handleLoginFail)
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()
} }
//
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() { function getCode() {
getCodeImg().then(res => { getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
if (captchaEnabled.value) { if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img codeUrl.value = 'data:image/gif;base64,' + res.img
loginForm.value.uuid = res.uuid loginForm.value.uuid = res.uuid
} }
}) })
} }
function getCookie() { //
const username = Cookies.get("username") const sendSmsCode = async () => {
const password = Cookies.get("password") if (!loginForm.value.phone) {
const rememberMe = Cookies.get("rememberMe") ElMessage.error('请输入手机号')
loginForm.value = { return
username: username === undefined ? loginForm.value.username : username,
password: password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
} }
// 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() getCode()
getCookie() getCookie()
</script> </script>
<style lang='scss' scoped> <style lang="scss" scoped>
.login { .login {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%; height: 100%;
background-image: url("../assets/images/login-background.jpg"); background-image: url('../assets/images/login-background.jpg');
background-size: cover; background-size: cover;
} }
.title { .title {