mirror of
https://gitee.com/xiaonuobase/snowy
synced 2025-12-16 11:13:59 +08:00
【更新】底座增加动态口令登录,完善单点登录客户端用于未来无缝接入统一认证平台,优化诸多代码,更新sql
This commit is contained in:
@@ -53,5 +53,13 @@ export default {
|
||||
// 注册用户
|
||||
register(data) {
|
||||
return request('register', data)
|
||||
},
|
||||
// B端动态口令登录
|
||||
loginByOtp(data) {
|
||||
return request('doLoginByOtp', data, 'post', false)
|
||||
},
|
||||
// B端判断是否登录
|
||||
isLogin(data) {
|
||||
return request('isLogin', data, 'get')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
import { baseRequest } from '@/utils/request'
|
||||
|
||||
const request = (url, ...arg) => baseRequest(`/auth/third/` + url, ...arg)
|
||||
const request = (url, ...arg) => baseRequest(`/auth/sso/b/` + url, ...arg)
|
||||
/**
|
||||
* 三方登录
|
||||
*
|
||||
@@ -18,12 +18,13 @@ const request = (url, ...arg) => baseRequest(`/auth/third/` + url, ...arg)
|
||||
* @date 2022-09-22 22:33:20
|
||||
*/
|
||||
export default {
|
||||
// 第三方登录页面渲染
|
||||
thirdRender(data) {
|
||||
return request('render', data, 'get')
|
||||
|
||||
// B端获取认证中心地址
|
||||
getSsoAuthUrl(data) {
|
||||
return request('getSsoAuthUrl', data, 'get')
|
||||
},
|
||||
// 第三方登录授权回调
|
||||
thirdCallback(data) {
|
||||
return request('callback', data, 'get')
|
||||
// B端根据ticket执行单点登录
|
||||
doLoginByTicket(data) {
|
||||
return request('doLoginByTicket', data)
|
||||
}
|
||||
}
|
||||
@@ -29,5 +29,9 @@ export default {
|
||||
// 第三方登录授权回调
|
||||
thirdCallback(data) {
|
||||
return request('callback', data, 'get')
|
||||
},
|
||||
// 第三方登录绑定账号
|
||||
thirdBindAccount(data) {
|
||||
return request('bindAccount', data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,5 +155,21 @@ export default {
|
||||
// 获取修改密码验证方式及配置
|
||||
userGetUpdatePasswordValidConfig(data) {
|
||||
return request('getUpdatePasswordValidConfig', data, 'get')
|
||||
}
|
||||
},
|
||||
// 获取动态口令绑定状态
|
||||
userCenterGetOtpInfoBindStatus(data) {
|
||||
return request('getOtpInfoBindStatus', data, 'get')
|
||||
},
|
||||
// 获取动态口令信息
|
||||
userCenterGetOtpInfo(data) {
|
||||
return request('getOtpInfo', data, 'get')
|
||||
},
|
||||
// 绑定动态口令
|
||||
userCenterBindOtp(data) {
|
||||
return request('bindOtp', data)
|
||||
},
|
||||
// 解绑动态口令
|
||||
userCenterUnBindOtp(data) {
|
||||
return request('unBindOtp', data)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -38,11 +38,14 @@ export default {
|
||||
accountError: 'Please input a user account',
|
||||
PWPlaceholder: 'Please input a password',
|
||||
PWError: 'Please input a password',
|
||||
validLaceholder: 'Please input a valid',
|
||||
validPlaceholder: 'Please input a valid',
|
||||
validError: 'Please input a valid',
|
||||
accountPassword: 'Account Password',
|
||||
phoneSms: 'Phone SMS',
|
||||
phoneLogin: 'Phone Login',
|
||||
emailLogin: 'Email Login',
|
||||
otpLogin: 'OTP Login',
|
||||
thirdLogin: 'Third Login',
|
||||
bindAccount: 'Bind Account',
|
||||
phonePlaceholder: 'Please input a phone',
|
||||
phoneInputNumberPlaceholder: 'Please input a phone 11-digit',
|
||||
smsCodePlaceholder: 'Please input a SMS code',
|
||||
@@ -56,6 +59,7 @@ export default {
|
||||
emailPlaceholder: 'Please input a correct email',
|
||||
emailCodePlaceholder: 'Please input a Email code',
|
||||
emailValidPlaceholder: 'Please input a email',
|
||||
otpCodePlaceholder: 'Please input a OTP code',
|
||||
restPhoneType: 'For phone rest',
|
||||
restEmailType: 'For email rest',
|
||||
register: 'Register',
|
||||
@@ -63,7 +67,10 @@ export default {
|
||||
notAccountPleaseRegister: 'Not Account? Register!',
|
||||
haveAccountPleaseLogin: 'Have Account? Go Login!',
|
||||
enterAgainPassword: 'Please re-enter your password',
|
||||
enteredPasswordsDiffer: 'Entered passwords differ'
|
||||
enteredPasswordsDiffer: 'Entered passwords differ',
|
||||
paramError: 'Param Error',
|
||||
thirdLoginError: 'Third Login Error',
|
||||
frontLogin: 'Front Login',
|
||||
},
|
||||
user: {
|
||||
userStatus: 'User Status',
|
||||
@@ -76,7 +83,7 @@ export default {
|
||||
exportUserInfo: 'Export UserInfo',
|
||||
placeholderNameAndSearchKey: 'Please enter your name or keyword',
|
||||
placeholderUserStatus: 'Please select status',
|
||||
popconfirmDeleteUser: 'Are you sure you want to delete it?',
|
||||
popconfirmResatUserPwd: 'Are you sure you want to reset?'
|
||||
popConfirmDeleteUser: 'Are you sure you want to delete it?',
|
||||
popConfirmResatUserPwd: 'Are you sure you want to reset?'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,14 @@ export default {
|
||||
accountError: '请输入账号',
|
||||
PWPlaceholder: '请输入密码',
|
||||
PWError: '请输入密码',
|
||||
validLaceholder: '请输入验证码',
|
||||
validPlaceholder: '请输入验证码',
|
||||
validError: '请输入验证码',
|
||||
accountPassword: '账号密码',
|
||||
phoneSms: '手机号登录',
|
||||
emailLogin: '邮箱号登录',
|
||||
phoneLogin: '手机号登录',
|
||||
emailLogin: '邮箱登录',
|
||||
otpLogin: '动态口令登录',
|
||||
thirdLogin: '三方登录',
|
||||
bindAccount: '绑定账号',
|
||||
phonePlaceholder: '请输入手机号',
|
||||
phoneInputNumberPlaceholder: '请输入11位手机号',
|
||||
smsCodePlaceholder: '请输入短信验证码',
|
||||
@@ -55,9 +58,10 @@ export default {
|
||||
newPwdPlaceholder: '请输入新密码',
|
||||
backLogin: '返回登录',
|
||||
restPassword: '重置密码',
|
||||
emailPlaceholder: '请输入邮箱号',
|
||||
emailPlaceholder: '请输入邮箱',
|
||||
emailCodePlaceholder: '请输入邮件验证码',
|
||||
emailValidPlaceholder: '请输入正确的邮箱号',
|
||||
otpCodePlaceholder: '请输入动态口令',
|
||||
restPhoneType: '手机号找回',
|
||||
restEmailType: '邮箱找回',
|
||||
register: '注册',
|
||||
@@ -65,7 +69,10 @@ export default {
|
||||
notAccountPleaseRegister: '没有账号?前往注册!',
|
||||
haveAccountPleaseLogin: '已有账号?去登录!',
|
||||
enterAgainPassword: '请再次输入密码',
|
||||
enteredPasswordsDiffer: '两次输入密码不一致'
|
||||
enteredPasswordsDiffer: '两次输入密码不一致',
|
||||
paramError: '参数错误',
|
||||
thirdLoginError: '登录失败',
|
||||
frontLogin: '前台登录',
|
||||
},
|
||||
user: {
|
||||
userStatus: '用户状态',
|
||||
@@ -78,7 +85,7 @@ export default {
|
||||
exportUserInfo: '导出信息',
|
||||
placeholderNameAndSearchKey: '请输入姓名或关键词',
|
||||
placeholderUserStatus: '请选择状态',
|
||||
popconfirmDeleteUser: '确定要删除吗?',
|
||||
popconfirmResatUserPwd: '确定要重置吗?'
|
||||
popConfirmDeleteUser: '确定要删除吗?',
|
||||
popConfirmResatUserPwd: '确定要重置吗?'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ import tool from '@/utils/tool'
|
||||
import routerUtil from '@/utils/routerUtil'
|
||||
|
||||
const Layout = () => import('@/layout/index.vue')
|
||||
const Sso = () => import('@/views/auth/sso/index.vue')
|
||||
const Login = () => import('@/views/auth/login/login.vue')
|
||||
const FindPwd = () => import('@/views/auth/findPwd/index.vue')
|
||||
const Callback = () => import('@/views/auth/login/callback.vue')
|
||||
const Register = () => import('@/views/auth/login/register.vue')
|
||||
|
||||
// 系统路由
|
||||
const routes = [
|
||||
{
|
||||
@@ -27,6 +27,13 @@ const routes = [
|
||||
redirect: tool.data.get('MENU') ? routerUtil.getIndexMenu(tool.data.get('MENU')).path : config.DASHBOARD_URL,
|
||||
children: []
|
||||
},
|
||||
{
|
||||
path: '/sso',
|
||||
component: Sso,
|
||||
meta: {
|
||||
title: '单点登录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: Login,
|
||||
@@ -49,12 +56,12 @@ const routes = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/callback',
|
||||
path: '/callback/:platform',
|
||||
component: Callback,
|
||||
meta: {
|
||||
title: '三方登录'
|
||||
title: '三方登录回调'
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
export default routes
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
|
||||
*/
|
||||
const constRouters = [
|
||||
{
|
||||
path: '/sso'
|
||||
},
|
||||
{
|
||||
path: '/findpwd'
|
||||
},
|
||||
@@ -16,7 +19,7 @@ const constRouters = [
|
||||
path: '/register'
|
||||
},
|
||||
{
|
||||
path: '/callback'
|
||||
path: '/callback/:platform'
|
||||
},
|
||||
{
|
||||
path: '/other',
|
||||
|
||||
@@ -21,13 +21,68 @@
|
||||
<div class="login-form">
|
||||
<a-card>
|
||||
<div class="login-header">
|
||||
<h2>三方登录</h2>
|
||||
<h2>{{tipText}}</h2>
|
||||
</div>
|
||||
<a-spin tip="正在登录中...">
|
||||
<a-spin :tip="tipText" v-if="showLoading">
|
||||
<div class="h-[300px]">
|
||||
<a-skeleton />
|
||||
</div>
|
||||
</a-spin>
|
||||
<a-empty :description="tipText" v-if="!showLoading && !showBind"></a-empty>
|
||||
<a-form ref="loginForm" :model="ruleForm" :rules="rules" v-if="showBind">
|
||||
<a-form-item name="account">
|
||||
<a-input
|
||||
v-model:value="ruleForm.account"
|
||||
:placeholder="$t('login.accountPlaceholder')"
|
||||
size="large"
|
||||
@keyup.enter="bindAccount"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="password">
|
||||
<a-input-password
|
||||
v-model:value="ruleForm.password"
|
||||
:placeholder="$t('login.PWPlaceholder')"
|
||||
size="large"
|
||||
autocomplete="off"
|
||||
@keyup.enter="bindAccount"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item name="validCode" v-if="captchaOpen === 'true'">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="ruleForm.validCode"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
@keyup.enter="bindAccount"
|
||||
>
|
||||
<template #prefix>
|
||||
<verified-outlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="7">
|
||||
<img :src="validCodeBase64" class="login-validCode-img" @click="loginCaptcha" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a href="/login" class="xn-color-0d84ff">{{ $t('login.signIn') }}</a>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" class="w-full" :loading="loading" round size="large" @click="bindAccount"
|
||||
>{{ $t('login.bindAccount') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,20 +91,53 @@
|
||||
|
||||
<script setup name="loginCallback">
|
||||
import { message } from 'ant-design-vue'
|
||||
import tool from '@/utils/tool'
|
||||
import router from '@/router'
|
||||
import thirdApi from '@/api/auth/thirdApi'
|
||||
import loginApi from '@/api/auth/loginApi'
|
||||
import userCenterApi from '@/api/sys/userCenterApi'
|
||||
import dictApi from '@/api/dev/dictApi'
|
||||
import { globalStore } from '@/store'
|
||||
|
||||
import {afterLogin} from "@/views/auth/login/util";
|
||||
import router from '@/router'
|
||||
import {required} from "@/utils/formRules";
|
||||
import tool from "@/utils/tool";
|
||||
import loginApi from "@/api/auth/loginApi";
|
||||
import smCrypto from '@/utils/smCrypto'
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = router.currentRoute.value
|
||||
const showLoading = ref(true)
|
||||
const showBind = ref(false)
|
||||
const tipText = ref(proxy.$t('login.thirdLogin'))
|
||||
const thirdId = ref(null)
|
||||
const store = globalStore()
|
||||
const sysBaseConfig = computed(() => {
|
||||
return store.sysBaseConfig
|
||||
return tool.data.get('SNOWY_SYS_BASE_CONFIG') || store.sysBaseConfig
|
||||
})
|
||||
const captchaOpen = ref(sysBaseConfig.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B)
|
||||
|
||||
const loading = ref(false)
|
||||
const validCodeBase64 = ref('')
|
||||
|
||||
const ruleForm = reactive({
|
||||
validCode: '',
|
||||
validCodeReqNo: '',
|
||||
autologin: false
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
account: [required(proxy.$t('login.accountError'), 'blur')],
|
||||
password: [required(proxy.$t('login.PWError'), 'blur')]
|
||||
})
|
||||
|
||||
const showError = (msg, alert) => {
|
||||
if(alert) {
|
||||
message.error(msg)
|
||||
}
|
||||
tipText.value = msg
|
||||
showLoading.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if(!route.params.platform) {
|
||||
showError(proxy.$t('login.paramError'), true)
|
||||
return
|
||||
}
|
||||
// 获取当前url
|
||||
const url = new URL(window.location.href)
|
||||
let argLength = 0
|
||||
@@ -60,37 +148,71 @@
|
||||
})
|
||||
// 当然了,不可能只有一个参数
|
||||
if (argLength < 2) {
|
||||
window.location.href = '/login'
|
||||
showError(proxy.$t('login.paramError'), true)
|
||||
return
|
||||
}
|
||||
|
||||
// 平台
|
||||
params.platform = route.params.platform
|
||||
thirdApi
|
||||
.thirdCallback(params)
|
||||
.then((data) => {
|
||||
tool.data.set('TOKEN', data)
|
||||
// 获取登录的用户信息
|
||||
loginApi.getLoginUser().then((loginUser) => {
|
||||
tool.data.set('USER_INFO', loginUser)
|
||||
})
|
||||
userCenterApi.userLoginMenu().then((menu) => {
|
||||
const indexMenu = menu[0].children[0].path
|
||||
tool.data.set('MENU', menu)
|
||||
// 重置系统默认应用
|
||||
tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
|
||||
router.replace({
|
||||
path: indexMenu
|
||||
})
|
||||
message.success('登录成功')
|
||||
dictApi.dictTree().then((dictData) => {
|
||||
// 设置字典到store中
|
||||
tool.data.set('DICT_TYPE_TREE_DATA', dictData)
|
||||
})
|
||||
})
|
||||
.then(async (data) => {
|
||||
if (data.startsWith('needBind')) {
|
||||
showError(proxy.$t('login.bindAccount'), true)
|
||||
thirdId.value = data.split(":")[1];
|
||||
showBind.value = true
|
||||
refreshSwitch()
|
||||
} else {
|
||||
await afterLogin(data)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.location.href = '/login'
|
||||
.catch((err) => {
|
||||
showError(proxy.$t('login.thirdLoginError'), false)
|
||||
console.log(err)
|
||||
})
|
||||
})
|
||||
// 通过开关加载内容
|
||||
const refreshSwitch = () => {
|
||||
// 判断是否开启验证码
|
||||
if (captchaOpen.value === 'true') {
|
||||
// 加载验证码
|
||||
loginCaptcha()
|
||||
// 加入校验
|
||||
rules.validCode = [required(proxy.$t('login.validError'), 'blur')]
|
||||
}
|
||||
}
|
||||
// 获取验证码
|
||||
const loginCaptcha = () => {
|
||||
loginApi.getPicCaptcha().then((data) => {
|
||||
validCodeBase64.value = data.validCodeBase64
|
||||
ruleForm.validCodeReqNo = data.validCodeReqNo
|
||||
})
|
||||
}
|
||||
// 绑定账号
|
||||
const loginForm = ref()
|
||||
const bindAccount = async () => {
|
||||
loginForm.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
loading.value = true
|
||||
const loginData = {
|
||||
thirdId: thirdId.value,
|
||||
account: ruleForm.account,
|
||||
// 密码进行SM2加密,传输过程中看到的只有密文,后端存储使用hash
|
||||
password: smCrypto.doSm2Encrypt(ruleForm.password),
|
||||
validCode: ruleForm.validCode,
|
||||
validCodeReqNo: ruleForm.validCodeReqNo
|
||||
}
|
||||
const loginToken = await thirdApi.thirdBindAccount(loginData)
|
||||
await afterLogin(loginToken)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
loading.value = false
|
||||
if (captchaOpen.value === 'true') {
|
||||
loginCaptcha()
|
||||
}}
|
||||
)
|
||||
}
|
||||
// logo链接
|
||||
const handleLink = (e) => {
|
||||
if (!sysBaseConfig.value.SNOWY_SYS_COPYRIGHT_URL) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a-form-item name="emailValidCode">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="16">
|
||||
<a-input v-model:value="emailFormData.emailValidCode" :placeholder="$t('login.validError')" size="large">
|
||||
<a-input v-model:value="emailFormData.emailValidCode" :placeholder="$t('login.emailCodePlaceholder')" size="large">
|
||||
<template #prefix>
|
||||
<mail-outlined class="text-black text-opacity-25" />
|
||||
</template>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
target="_blank"
|
||||
@click="handleLink"
|
||||
>
|
||||
<img :alt="sysBaseConfig.SNOWY_SYS_NAME" :src="sysBaseConfig.SNOWY_SYS_LOGO" />
|
||||
<label>{{ sysBaseConfig.SNOWY_SYS_NAME }}</label>
|
||||
<img :alt="systemName" :src="sysBaseConfig.SNOWY_SYS_LOGO" />
|
||||
<label>{{ systemName }}</label>
|
||||
</a>
|
||||
</div>
|
||||
<div class="version">
|
||||
@@ -74,7 +74,7 @@
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="ruleForm.validCode"
|
||||
:placeholder="$t('login.validLaceholder')"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
@keyup.enter="login"
|
||||
>
|
||||
@@ -104,17 +104,26 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="userSms" :tab="$t('login.phoneSms')" force-render v-if="phoneLogin === 'true'">
|
||||
<a-tab-pane key="userSms" :tab="$t('login.phoneLogin')" force-render v-if="loginTypes.phoneLogin === 'true'">
|
||||
<phone-login-form />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="userEmail" :tab="$t('login.emailLogin')" force-render v-if="emailLogin === 'true'">
|
||||
<a-tab-pane key="userEmail" :tab="$t('login.emailLogin')" force-render v-if="loginTypes.emailLogin === 'true'">
|
||||
<email-login-form />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="userOtp" :tab="$t('login.otpLogin')" force-render v-if="loginTypes.otpLogin === 'true'">
|
||||
<otp-login-form :captchaOpen="captchaOpen" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div v-if="configData.FRONT_BACK_LOGIN_URL_SHOW">
|
||||
<a href="/front/client/index" class="xn-color-0d84ff">前台登录</a>
|
||||
<a href="/front/client/index" class="xn-color-0d84ff">{{ $t('login.frontLogin') }}</a>
|
||||
</div>
|
||||
<three-login v-if="configData.THREE_LOGIN_SHOW" />
|
||||
<three-login v-if="configData.THREE_LOGIN_SHOW && !appId" />
|
||||
<three-login-for-app ref="threeLoginForAppRef"
|
||||
v-if="configData.THREE_LOGIN_SHOW && appId"
|
||||
:appId="appId"
|
||||
:loginTypes="loginTypes"
|
||||
@updateLoginTypes="updateLoginTypes"
|
||||
@updateSystemName="updateSystemName"/>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,7 +133,9 @@
|
||||
import loginApi from '@/api/auth/loginApi'
|
||||
const PhoneLoginForm = defineAsyncComponent(() => import('./phoneLoginForm.vue'))
|
||||
const EmailLoginForm = defineAsyncComponent(() => import('./emailLoginForm.vue'))
|
||||
const OtpLoginForm = defineAsyncComponent(() => import('./otpLoginForm.vue'))
|
||||
import ThreeLogin from './threeLogin.vue'
|
||||
import ThreeLoginForApp from './threeLoginForApp.vue'
|
||||
import smCrypto from '@/utils/smCrypto'
|
||||
import { required } from '@/utils/formRules'
|
||||
import { afterLogin } from './util'
|
||||
@@ -132,13 +143,28 @@
|
||||
import configApi from '@/api/dev/configApi'
|
||||
import tool from '@/utils/tool'
|
||||
import { globalStore, iframeStore, keepAliveStore, viewTagsStore } from '@/store'
|
||||
import router from '@/router'
|
||||
const route = router.currentRoute.value
|
||||
const appId = computed(() => {
|
||||
return route.query.appId
|
||||
})
|
||||
const threeLoginForAppRef = ref(null)
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const activeKey = ref('userAccount')
|
||||
|
||||
const captchaOpen = ref(configData.SYS_BASE_CONFIG.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B)
|
||||
const registerOpen = ref('false')
|
||||
const phoneLogin = ref('false')
|
||||
const emailLogin = ref('false')
|
||||
const loginTypes = reactive({
|
||||
phoneLogin: 'false',
|
||||
emailLogin: 'false',
|
||||
otpLogin: 'false'
|
||||
})
|
||||
const updateLoginTypes = (newTypes) => {
|
||||
Object.assign(loginTypes, newTypes)
|
||||
}
|
||||
const updateSystemName = (newSystemName) => {
|
||||
systemName.value = newSystemName
|
||||
}
|
||||
const validCodeBase64 = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -185,7 +211,7 @@
|
||||
const sysBaseConfig = computed(() => {
|
||||
return store.sysBaseConfig
|
||||
})
|
||||
|
||||
const systemName = ref(sysBaseConfig.value.SNOWY_SYS_NAME)
|
||||
onMounted(() => {
|
||||
let formData = ref(configData.SYS_BASE_CONFIG)
|
||||
configApi
|
||||
@@ -197,11 +223,15 @@
|
||||
})
|
||||
captchaOpen.value = formData.value.SNOWY_SYS_DEFAULT_CAPTCHA_OPEN_FLAG_FOR_B
|
||||
registerOpen.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_REGISTER_FLAG_FOR_B
|
||||
phoneLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B
|
||||
emailLogin.value = formData.value.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B
|
||||
loginTypes.phoneLogin = formData.value.SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B
|
||||
loginTypes.emailLogin = formData.value.SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B
|
||||
loginTypes.otpLogin = formData.value.SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B
|
||||
tool.data.set('SNOWY_SYS_BASE_CONFIG', formData.value)
|
||||
setSysBaseConfig(formData.value)
|
||||
refreshSwitch()
|
||||
if (threeLoginForAppRef.value) {
|
||||
threeLoginForAppRef.value.init(appId)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -245,7 +275,7 @@
|
||||
ruleForm.validCodeReqNo = data.validCodeReqNo
|
||||
})
|
||||
}
|
||||
//登陆
|
||||
// 登录
|
||||
const loginForm = ref()
|
||||
const login = async () => {
|
||||
loginForm.value
|
||||
@@ -259,18 +289,16 @@
|
||||
validCode: ruleForm.validCode,
|
||||
validCodeReqNo: ruleForm.validCodeReqNo
|
||||
}
|
||||
// 获取token
|
||||
try {
|
||||
const loginToken = await loginApi.login(loginData)
|
||||
await afterLogin(loginToken)
|
||||
} catch (err) {
|
||||
loading.value = false
|
||||
if (captchaOpen.value === 'true') {
|
||||
loginCaptcha()
|
||||
}
|
||||
}
|
||||
const loginToken = await loginApi.login(loginData)
|
||||
await afterLogin(loginToken)
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
loading.value = false
|
||||
if (captchaOpen.value === 'true') {
|
||||
loginCaptcha()
|
||||
}}
|
||||
)
|
||||
}
|
||||
const configLang = (key) => {
|
||||
config.value.lang = key
|
||||
|
||||
115
snowy-admin-web/src/views/auth/login/otpLoginForm.vue
Normal file
115
snowy-admin-web/src/views/auth/login/otpLoginForm.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<a-form ref="otpLoginFormRef" :model="ruleForm" :rules="formRules">
|
||||
<a-form-item name="accountForOtp">
|
||||
<a-input v-model:value="ruleForm.accountForOtp" :placeholder="$t('login.accountPlaceholder')" size="large">
|
||||
<template #prefix>
|
||||
<user-outlined class="text-black text-opacity-25" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="otpCode">
|
||||
<a-input v-model:value="ruleForm.otpCode" :placeholder="$t('login.otpCodePlaceholder')" size="large">
|
||||
<template #prefix>
|
||||
<mail-outlined class="text-black text-opacity-25" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="validCodeForOtp" v-if="captchaOpen === 'true'">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="ruleForm.validCodeForOtp"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
<verified-outlined class="login-icon-gray" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="7">
|
||||
<img :src="validCodeBase64" class="login-validCode-img" @click="loginCaptcha" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" class="xn-wd" :loading="loading" round size="large" @click="submitLogin">
|
||||
{{ $t('login.signIn') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script setup name="otpLoginForm">
|
||||
import loginApi from '@/api/auth/loginApi'
|
||||
import { afterLogin } from './util'
|
||||
import {required} from "@/utils/formRules";
|
||||
const { proxy } = getCurrentInstance()
|
||||
const props = defineProps({
|
||||
captchaOpen: {
|
||||
type: String,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
const otpLoginFormRef = ref()
|
||||
const validCodeBase64 = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const ruleForm = reactive({
|
||||
accountForOtp: '',
|
||||
otpCode: '',
|
||||
validCodeForOtp: '',
|
||||
validCodeReqNo: ''
|
||||
})
|
||||
|
||||
const formRules = reactive({
|
||||
accountForOtp: [required(proxy.$t('login.accountError'), 'blur')],
|
||||
otpCode: [required(proxy.$t('login.otpCodePlaceholder'), 'blur')]
|
||||
})
|
||||
|
||||
// 通过开关加载内容
|
||||
const refreshSwitch = () => {
|
||||
// 判断是否开启验证码
|
||||
if (props.captchaOpen === 'true') {
|
||||
// 加载验证码
|
||||
loginCaptcha()
|
||||
// 加入校验
|
||||
formRules.validCodeForOtp = [required(proxy.$t('login.validError'), 'blur')]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
const loginCaptcha = () => {
|
||||
loginApi.getPicCaptcha().then((data) => {
|
||||
validCodeBase64.value = data.validCodeBase64
|
||||
ruleForm.validCodeReqNo = data.validCodeReqNo
|
||||
})
|
||||
}
|
||||
|
||||
// 通过开关加载内容
|
||||
refreshSwitch()
|
||||
|
||||
// 点击登录按钮
|
||||
const submitLogin = async () => {
|
||||
const validate = await otpLoginFormRef.value.validate().catch(() => {})
|
||||
if (!validate) return false
|
||||
const loginData = {
|
||||
account: ruleForm.accountForOtp,
|
||||
otpCode: ruleForm.otpCode,
|
||||
validCode: ruleForm.validCodeForOtp,
|
||||
validCodeReqNo: ruleForm.validCodeReqNo
|
||||
}
|
||||
loading.value = true
|
||||
loginApi
|
||||
.loginByOtp(loginData)
|
||||
.then(async (loginToken) => {
|
||||
await afterLogin(loginToken)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
if (props.captchaOpen === 'true') {
|
||||
loginCaptcha()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -51,7 +51,7 @@
|
||||
<a-col :span="17">
|
||||
<a-input
|
||||
v-model:value="phoneFormModalData.validCode"
|
||||
:placeholder="$t('login.validLaceholder')"
|
||||
:placeholder="$t('login.validPlaceholder')"
|
||||
size="large"
|
||||
>
|
||||
<template #prefix>
|
||||
|
||||
@@ -2,23 +2,32 @@
|
||||
<a-divider>{{ $t('login.signInOther') }}</a-divider>
|
||||
<div class="login-oauth layout-center">
|
||||
<a-space align="start">
|
||||
<a @click="getLoginRenderUrl('gitee')"><GiteeIcon /></a>
|
||||
<a-button type="primary" shape="circle">
|
||||
<wechat-filled />
|
||||
</a-button>
|
||||
<a @click="getLoginRenderUrl('gitee')">
|
||||
<GiteeIcon />
|
||||
</a>
|
||||
<a @click="getLoginRenderUrl('wechat')">
|
||||
<wechat-outlined class="bind-icon" :style="{ color: '#1AAD19' }" />
|
||||
</a>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="threeLogin">
|
||||
import thirdApi from '@/api/auth/thirdApi'
|
||||
import WechatOutlined from "@ant-design/icons-vue/WechatOutlined";
|
||||
|
||||
const getLoginRenderUrl = (platform) => {
|
||||
const param = {
|
||||
platform: platform
|
||||
platform: platform,
|
||||
clientType: 'B'
|
||||
}
|
||||
thirdApi.thirdRender(param).then((data) => {
|
||||
window.location.href = data.authorizeUrl
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.bind-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
66
snowy-admin-web/src/views/auth/login/threeLoginForApp.vue
Normal file
66
snowy-admin-web/src/views/auth/login/threeLoginForApp.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<a-divider>{{ $t('login.signInOther') }}</a-divider>
|
||||
<div class="login-oauth layout-center">
|
||||
<a-space align="start">
|
||||
<a @click="renderAuthSource(record)" v-for="record in appAuthSourceList" :key="record.authSourceId">
|
||||
<img :src="record.authSourceLogo" class="record-img"/>
|
||||
</a>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="threeLoginForApp">
|
||||
/*import authLoginApi from '@/api/iam/auth/authLoginApi'
|
||||
import authSourceApi from '@/api/iam/auth/authSourceApi'*/
|
||||
// 定义emit事件
|
||||
const emit = defineEmits({ updateLoginTypes: null, updateSystemName: null })
|
||||
|
||||
const props = defineProps({
|
||||
appId: {
|
||||
type: String,
|
||||
default: () => {}
|
||||
},
|
||||
loginTypes: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const appAuthSourceList = ref([])
|
||||
const init = () => {
|
||||
const param = {
|
||||
appId: props.appId
|
||||
}
|
||||
/*authLoginApi.getAppAuthSourceList(param).then((data) => {
|
||||
const appName = data.appName
|
||||
const authAppLinkResultList = data.authAppLinkResultList
|
||||
let phoneLogin = authAppLinkResultList.filter((item) => item.authSourceTemplateCode === 'PHONE').length > 0?'true':'false';
|
||||
let emailLogin = authAppLinkResultList.filter((item) => item.authSourceTemplateCode === 'EMAIL').length > 0?'true':'false';
|
||||
let otpLogin = authAppLinkResultList.filter((item) => item.authSourceTemplateCode === 'OTP').length > 0?'true':'false';
|
||||
appAuthSourceList.value = authAppLinkResultList.filter((item) => !item.isBuildIn);
|
||||
phoneLogin = props.loginTypes.phoneLogin === 'true' && phoneLogin === 'true'?'true':'false'
|
||||
emailLogin = props.loginTypes.emailLogin === 'true' && emailLogin === 'true'?'true':'false'
|
||||
otpLogin = props.loginTypes.otpLogin === 'true' && otpLogin === 'true'?'true':'false'
|
||||
emit('updateLoginTypes', { phoneLogin, emailLogin, otpLogin })
|
||||
emit('updateSystemName', appName)
|
||||
})*/
|
||||
}
|
||||
const renderAuthSource = (record) => {
|
||||
const param = {
|
||||
appId: props.appId,
|
||||
authSourceId: record.authSourceId,
|
||||
clientType: 'B'
|
||||
}
|
||||
/*authSourceApi.authSourceRender(param).then((data) => {
|
||||
window.location.href = data
|
||||
})*/
|
||||
}
|
||||
defineExpose({
|
||||
init
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.record-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@ import { useMenuStore } from '@/store/menu'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
export const afterLogin = async (loginToken) => {
|
||||
const route = router.currentRoute.value
|
||||
const menuStore = useMenuStore()
|
||||
tool.data.set('TOKEN', loginToken)
|
||||
// 初始化用户信息
|
||||
@@ -20,7 +21,7 @@ export const afterLogin = async (loginToken) => {
|
||||
|
||||
// 重置系统默认应用
|
||||
tool.data.set('SNOWY_MENU_MODULE_ID', menu[0].id)
|
||||
message.success('登录成功')
|
||||
|
||||
if (tool.data.get('LAST_VIEWS_PATH')) {
|
||||
// 如果有缓存,将其登录跳转到最后访问的路由
|
||||
indexMenu = tool.data.get('LAST_VIEWS_PATH')
|
||||
@@ -44,13 +45,40 @@ export const afterLogin = async (loginToken) => {
|
||||
// 设置字典到store中
|
||||
tool.data.set('DICT_TYPE_TREE_DATA', data)
|
||||
})
|
||||
await router.replace({
|
||||
path: indexMenu
|
||||
})
|
||||
// 判断用户密码是否过期
|
||||
userCenterApi.userCenterIsUserPasswordExpired().then((expired) => {
|
||||
if (expired) {
|
||||
message.warning('当前登录密码已过期,请及时更改!')
|
||||
}
|
||||
})
|
||||
|
||||
// 此处判断是否存在跳转页面,如存在则跳转,否则走原来逻辑
|
||||
if(route.query.redirect_uri) {
|
||||
// 跳转到回调页
|
||||
message.success('登录成功,即将跳转...')
|
||||
setTimeout(function () {
|
||||
window.location.href = route.query.redirect_uri;
|
||||
}, 500);
|
||||
} else if(route.query.redirect) {
|
||||
// 跳转到回调页
|
||||
message.success('登录成功,即将跳转...')
|
||||
setTimeout(function () {
|
||||
window.location.href = route.query.redirect;
|
||||
}, 500);
|
||||
} else if(route.query.back) {
|
||||
// 跳转到回调页
|
||||
message.success('登录成功,即将跳转...')
|
||||
setTimeout(function () {
|
||||
window.location.href = route.query.back;
|
||||
}, 500);
|
||||
} else {
|
||||
message.success('登录成功,即将跳转...')
|
||||
setTimeout(function () {
|
||||
// 跳转到首页
|
||||
router.replace({
|
||||
path: indexMenu
|
||||
}).then(() => {
|
||||
// 判断用户密码是否过期
|
||||
userCenterApi.userCenterIsUserPasswordExpired().then((expired) => {
|
||||
if (expired) {
|
||||
message.warning('当前登录密码已过期,请及时更改!')
|
||||
}
|
||||
})
|
||||
})
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
204
snowy-admin-web/src/views/auth/sso/index.vue
Normal file
204
snowy-admin-web/src/views/auth/sso/index.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div class="content-wrapper">
|
||||
<div class="content-box">
|
||||
<div class="content-form">
|
||||
<div class="content-header">
|
||||
<!-- 加载状态容器 -->
|
||||
<div class="loading-container" v-if="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">{{ tipText }}</p>
|
||||
</div>
|
||||
<!-- 错误状态容器 -->
|
||||
<div class="error-container" v-else>
|
||||
<p class="error-text">{{ tipText }}</p>
|
||||
<button class="retry-btn" @click="tryJump">重试</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="ssoLogin">
|
||||
import { useRoute } from "vue-router";
|
||||
import ssoApi from "@/api/auth/ssoApi";
|
||||
import { afterLogin } from "@/views/auth/login/util";
|
||||
import { ref, onMounted } from 'vue';
|
||||
import tool from "@/utils/tool";
|
||||
import loginApi from "@/api/auth/loginApi";
|
||||
|
||||
const route = useRoute();
|
||||
const tipText = ref('加载中...');
|
||||
const loading = ref(true); // 新增加载状态控制
|
||||
|
||||
// 从url中查询到指定名称的参数值
|
||||
const getParam = (name, defaultValue) => {
|
||||
const query = window.location.search.substring(1);
|
||||
const vars = query.split("&");
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const pair = vars[i].split("=");
|
||||
if (pair[0] === name) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return defaultValue === undefined ? null : defaultValue;
|
||||
};
|
||||
|
||||
const ticket = getParam('ticket') || route.query.ticket;
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await tryJump();
|
||||
});
|
||||
|
||||
// 跳转
|
||||
const tryJump = async () => {
|
||||
// 重置加载状态
|
||||
loading.value = true;
|
||||
tipText.value = '加载中...';
|
||||
|
||||
try {
|
||||
let existToken = tool.data.get('TOKEN');
|
||||
if (existToken) {
|
||||
const isLogin = await loginApi.isLogin();
|
||||
if (isLogin) {
|
||||
await goHome(existToken);
|
||||
} else {
|
||||
await redirectSsoAuthUrl(window.location.href);
|
||||
}
|
||||
} else {
|
||||
if (ticket) {
|
||||
await doLoginByTicket(ticket);
|
||||
} else {
|
||||
await redirectSsoAuthUrl(window.location.href);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
loading.value = false;
|
||||
tipText.value = '处理失败,请重试';
|
||||
console.error('SSO登录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转首页
|
||||
const goHome = async (loginToken) => {
|
||||
tipText.value = '验证成功,即将跳转...';
|
||||
setTimeout(async () => {
|
||||
await afterLogin(loginToken);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 处理SSO登录回调
|
||||
const doLoginByTicket = async (ticket) => {
|
||||
const loginToken = await ssoApi.doLoginByTicket({ ticket: ticket });
|
||||
tipText.value = '验证成功,即将跳转...';
|
||||
setTimeout(async () => {
|
||||
await afterLogin(loginToken);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 重定向到SSO登录页
|
||||
const redirectSsoAuthUrl = async (redirectUrl) => {
|
||||
const authUrl = await ssoApi.getSsoAuthUrl({ redirectUrl: redirectUrl });
|
||||
tipText.value = '即将跳转至SSO登录页...';
|
||||
setTimeout(() => {
|
||||
window.location.href = authUrl;
|
||||
}, 500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0f5cb3 0%, #1677ff 50%, #4096ff 100%);
|
||||
box-shadow: inset 0 0 200px rgba(255, 255, 255, 0.1);
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-form {
|
||||
width: 450px;
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto 20px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@@ -105,7 +105,7 @@
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a @click="formRef.onOpen(record)" v-if="hasPerm('bizUserEdit')">{{ $t('common.editButton') }}</a>
|
||||
<a-divider type="vertical" v-if="hasPerm(['bizUserEdit', 'bizUserDelete'], 'and')" />
|
||||
<a-popconfirm :title="$t('user.popconfirmDeleteUser')" @confirm="removeUser(record)">
|
||||
<a-popconfirm :title="$t('user.popConfirmDeleteUser')" @confirm="removeUser(record)">
|
||||
<a-button type="link" danger size="small" v-if="hasPerm('bizUserDelete')">{{
|
||||
$t('common.removeButton')
|
||||
}}</a-button>
|
||||
@@ -123,7 +123,7 @@
|
||||
<a-menu>
|
||||
<a-menu-item v-if="hasPerm('bizUserPwdReset')">
|
||||
<a-popconfirm
|
||||
:title="$t('user.popconfirmResatUserPwd')"
|
||||
:title="$t('user.popConfirmResatUserPwd')"
|
||||
placement="topRight"
|
||||
@confirm="resetPassword(record)"
|
||||
>
|
||||
|
||||
@@ -85,7 +85,14 @@
|
||||
placeholder="请选择邮箱无对应用户时策略"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="是否允许动态口令登录:" name="SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B">
|
||||
<a-switch
|
||||
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
placeholder="请选择是否允许动态口令登录"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
|
||||
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
|
||||
@@ -166,7 +173,8 @@
|
||||
SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_B: [required('请选择是否允许手机号登录')],
|
||||
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_B: [required('请选择手机号无对应用户时策略')],
|
||||
SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_B: [required('请选择是否允许邮箱登录')],
|
||||
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B: [required('请选择邮箱无对应用户时策略')]
|
||||
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_B: [required('请选择邮箱无对应用户时策略')],
|
||||
SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_B: [required('请选择是否允许动态口令登录')]
|
||||
}
|
||||
// 验证并提交数据
|
||||
const onSubmit = () => {
|
||||
|
||||
@@ -85,6 +85,14 @@
|
||||
placeholder="请选择邮箱无对应用户时策略"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否允许动态口令登录:" name="SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C">
|
||||
<a-switch
|
||||
v-model:checked="formData.SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
placeholder="请选择是否允许动态口令登录"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</a-button>
|
||||
<a-button class="xn-ml10" @click="() => formRef.resetFields()">重置</a-button>
|
||||
@@ -165,7 +173,8 @@
|
||||
SNOWY_SYS_DEFAULT_ALLOW_PHONE_LOGIN_FLAG_FOR_C: [required('请选择是否允许手机号登录')],
|
||||
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_PHONE_FOR_C: [required('请选择手机号无对应用户时策略')],
|
||||
SNOWY_SYS_DEFAULT_ALLOW_EMAIL_LOGIN_FLAG_FOR_C: [required('请选择是否允许邮箱登录')],
|
||||
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C: [required('请选择邮箱无对应用户时策略')]
|
||||
SNOWY_SYS_DEFAULT_STRATEGY_WHEN_NO_USER_WITH_EMAIL_FOR_C: [required('请选择邮箱无对应用户时策略')],
|
||||
SNOWY_SYS_DEFAULT_ALLOW_OTP_LOGIN_FLAG_FOR_C: [required('请选择是否允许动态口令登录')]
|
||||
}
|
||||
// 验证并提交数据
|
||||
const onSubmit = () => {
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a @click="formRef.onOpen(record)">{{ $t('common.editButton') }}</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm :title="$t('user.popconfirmDeleteUser')" placement="topRight" @confirm="removeUser(record)">
|
||||
<a-popconfirm :title="$t('user.popConfirmDeleteUser')" placement="topRight" @confirm="removeUser(record)">
|
||||
<a-button type="link" danger size="small">
|
||||
{{ $t('common.removeButton') }}
|
||||
</a-button>
|
||||
@@ -113,7 +113,7 @@
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a-popconfirm
|
||||
:title="$t('user.popconfirmResatUserPwd')"
|
||||
:title="$t('user.popConfirmResatUserPwd')"
|
||||
placement="topRight"
|
||||
@confirm="resetPassword(record)"
|
||||
>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<CropUpload ref="cropUploadRef" :img-src="userInfo ? userInfo.avatar : undefined" @successful="cropUploadSuccess" />
|
||||
<CropUpload ref="cropUploadRef" :img-src="userInfo ? userInfo.avatar : undefined" @successful="cropUploadSuccess" :z-index="2000" />
|
||||
</template>
|
||||
|
||||
<script setup name="userCenter">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<a-form-item label="昵称:" name="nickname">
|
||||
<a-input v-model:value="formData.nickname" placeholder="请输入昵称" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="性别:" name="sex">
|
||||
<a-form-item label="性别:" name="gender">
|
||||
<a-radio-group v-model:value="formData.gender" :options="genderOptions" />
|
||||
</a-form-item>
|
||||
<a-form-item label="生日:" name="birthday">
|
||||
@@ -43,8 +43,8 @@
|
||||
const submitLoading = ref(false)
|
||||
// 默认要校验的
|
||||
const formRules = {
|
||||
name: [required('请输入姓名')],
|
||||
gender: [required('请选择性别')]
|
||||
account: [required('请输入账号')],
|
||||
name: [required('请输入姓名')]
|
||||
}
|
||||
const genderOptions = tool.dictList('GENDER')
|
||||
// 验证并提交数据
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-list item-layout="horizontal" :data-source="data">
|
||||
<a-list item-layout="horizontal" :data-source="bindInfoList">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta class="list-item-meta">
|
||||
@@ -10,17 +10,16 @@
|
||||
<span class="security-list-value">{{ item.value }}</span>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<qq-outlined v-if="item.type === 'qq'" class="bind-icon" :style="{ color: '#1677FF' }" />
|
||||
<wechat-outlined v-if="item.type === 'weChat'" class="bind-icon" :style="{ color: '#1AAD19' }" />
|
||||
<alipay-circle-outlined v-if="item.type === 'AliPay'" class="bind-icon" :style="{ color: '#178bf5' }" />
|
||||
<mail-outlined v-if="item.type === 'email'" class="bind-icon" :style="{ color: '#fcab43' }" />
|
||||
<mobile-outlined v-if="item.type === 'phone'" class="bind-icon" :style="{ color: '#43a0fc' }" />
|
||||
<verified-outlined v-if="item.type === 'password'" class="bind-icon" :style="{ color: '#a059e8' }" />
|
||||
<usb-outlined v-if="item.type === 'otp'" class="bind-icon" :style="{ color: '#1AAD19' }" />
|
||||
<GiteeIcon v-if="item.type === 'Gitee'" class="bind-icon xn-wd40" />
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<a @click="bindCommon(item.type)">{{ item.value ? '修改' : '去绑定' }}</a>
|
||||
<a @click="bindCommon(item)">{{ item.value ? (item.type === 'otp'?'解绑' : '修改') : '去绑定' }}</a>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
@@ -28,6 +27,7 @@
|
||||
<updatePassword ref="updatePasswordRef" />
|
||||
<bind-phone ref="bindPhoneRef" />
|
||||
<bind-email ref="bindEmailRef" />
|
||||
<bind-otp ref="bindOtpRef" @successful="getOtpInfoBindStatus()"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -35,18 +35,20 @@
|
||||
import UpdatePassword from './bindForm/updatePassword.vue'
|
||||
import BindPhone from '@/views/sys/user/userTab/bindForm/bindPhone.vue'
|
||||
import BindEmail from '@/views/sys/user/userTab/bindForm/bindEmail.vue'
|
||||
import BindOtp from '@/views/sys/user/userTab/bindForm/bindOtp.vue'
|
||||
// 按需导入图标组件
|
||||
import QqOutlined from '@ant-design/icons-vue/QqOutlined'
|
||||
import WechatOutlined from '@ant-design/icons-vue/WechatOutlined'
|
||||
import AlipayCircleOutlined from '@ant-design/icons-vue/AlipayCircleOutlined'
|
||||
import MailOutlined from '@ant-design/icons-vue/MailOutlined'
|
||||
import MobileOutlined from '@ant-design/icons-vue/MobileOutlined'
|
||||
import VerifiedOutlined from '@ant-design/icons-vue/VerifiedOutlined'
|
||||
import UsbOutlined from '@ant-design/icons-vue/UsbOutlined'
|
||||
import { globalStore } from '@/store'
|
||||
import userCenterApi from "@/api/sys/userCenterApi";
|
||||
|
||||
const updatePasswordRef = ref()
|
||||
const bindPhoneRef = ref()
|
||||
const bindEmailRef = ref()
|
||||
const bindOtpRef = ref()
|
||||
const store = globalStore()
|
||||
const userInfo = computed(() => {
|
||||
if (store.userInfo) {
|
||||
@@ -60,40 +62,58 @@
|
||||
}
|
||||
})
|
||||
// 获取绑定的情况
|
||||
const data = [
|
||||
const bindInfoList = ref([
|
||||
{ title: '密码强度', description: '当前密码强度', value: '弱', type: 'password', bindStatus: 0 },
|
||||
{
|
||||
title: '邮箱',
|
||||
description: userInfo && userInfo.value.email ? '已绑定邮箱' : '未绑定邮箱',
|
||||
value: userInfo && userInfo.value.email ? userInfo.value.email : '',
|
||||
type: 'email',
|
||||
bindStatus: 0
|
||||
bindStatus: userInfo && userInfo.value.email
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
description: userInfo && userInfo.value.phone ? '已绑定手机' : '未绑定手机',
|
||||
value: userInfo && userInfo.value.phone ? userInfo.value.phone : '',
|
||||
type: 'phone',
|
||||
bindStatus: 1
|
||||
bindStatus: userInfo && userInfo.value.phone
|
||||
},
|
||||
{ title: '绑定QQ', description: '未绑定', value: '', type: 'qq', bindStatus: 0 },
|
||||
{ title: '绑定微信', description: '未绑定', value: '', type: 'weChat', bindStatus: 0 },
|
||||
{ title: '绑定支付宝', description: '未绑定', value: '', type: 'AliPay', bindStatus: 0 },
|
||||
{ title: '绑定Gitee', description: '未绑定', value: '', type: 'Gitee', bindStatus: 0 }
|
||||
]
|
||||
const bindCommon = (key) => {
|
||||
])
|
||||
const bindCommon = (item) => {
|
||||
let key = item.type
|
||||
if (key === 'password') {
|
||||
updatePasswordRef.value.onOpen()
|
||||
} else if (key === 'phone') {
|
||||
bindPhoneRef.value.open(userInfo.value.phone)
|
||||
bindPhoneRef.value.open()
|
||||
} else if (key === 'email') {
|
||||
bindEmailRef.value.open(userInfo.value.email)
|
||||
bindEmailRef.value.open()
|
||||
} else if (key === 'otp') {
|
||||
if(item.value) {
|
||||
bindOtpRef.value.onOpen('unbind')
|
||||
} else {
|
||||
bindOtpRef.value.onOpen('bind')
|
||||
}
|
||||
} else {
|
||||
message.info('开发中')
|
||||
message.info('请在登录页使用三方登录后输入账号信息' + item.title)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
// 获取绑定情况
|
||||
// 获取动态口令绑定状态
|
||||
const getOtpInfoBindStatus = async () => {
|
||||
userCenterApi.userCenterGetOtpInfoBindStatus().then((data) => {
|
||||
bindInfoList.value[3] = {
|
||||
title: '动态口令',
|
||||
description: data ? '已绑定动态口令' : '未绑定动态口令',
|
||||
value: data ? '******' : '',
|
||||
type: 'otp',
|
||||
bindStatus: data
|
||||
}
|
||||
})
|
||||
}
|
||||
onMounted(async () => {
|
||||
// 获取动态口令绑定状态
|
||||
await getOtpInfoBindStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
120
snowy-admin-web/src/views/sys/user/userTab/bindForm/bindOtp.vue
Normal file
120
snowy-admin-web/src/views/sys/user/userTab/bindForm/bindOtp.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-modal title="绑定动态口令" :width="800" :open="visible" :destroy-on-close="true" @cancel="onClose">
|
||||
<a-skeleton active v-if="!otpInfo" />
|
||||
<div v-else>
|
||||
<a-alert type="info" banner class="mb-3">
|
||||
<template #description>
|
||||
<p>1.打开Google Authenticator或者Okta Verify等动态口令应用。</p>
|
||||
<p>2.点击“扫一扫”或者“手动输入”,将动态口令应用中的二维码扫描到应用中。</p>
|
||||
<p>3.在下方输入框中输入动态口令,即可完成绑定/解绑。</p>
|
||||
</template>
|
||||
</a-alert>
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="8">
|
||||
<img style="width: 100%;vertical-align: middle" :src="otpInfo.otpInfoBase64" />
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-descriptions :column="1" size="middle" bordered class="mb-2">
|
||||
<a-descriptions-item label="发行者">{{otpInfo.otpInfo.issuer}}</a-descriptions-item>
|
||||
<a-descriptions-item label="账号">{{otpInfo.otpInfo.account}}</a-descriptions-item>
|
||||
<a-descriptions-item label="密钥">{{otpInfo.otpInfo.secretKey}}</a-descriptions-item>
|
||||
<a-descriptions-item label="算法">{{otpInfo.otpInfo.algorithm}}</a-descriptions-item>
|
||||
<a-descriptions-item label="位数">{{otpInfo.otpInfo.digits}}</a-descriptions-item>
|
||||
<a-descriptions-item label="周期">{{otpInfo.otpInfo.period}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form ref="formRef" :model="formState" :rules="formRules" layout="vertical">
|
||||
<a-form-item
|
||||
label="动态口令"
|
||||
name="otpCode"
|
||||
has-feedback
|
||||
>
|
||||
<a-input v-model:value="formState.otpCode" placeholder="请输入动态口令" allow-clear autocomplete="off"/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
|
||||
<a-button type="primary" :loading="submitLoading" @click="onSubmit">保存</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="bindOtp">
|
||||
import { required } from '@/utils/formRules'
|
||||
import userCenterApi from '@/api/sys/userCenterApi'
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits({ successful: null })
|
||||
// 默认是关闭状态
|
||||
const visible = ref(false)
|
||||
const formRef = ref()
|
||||
// 表单数据
|
||||
const formState = ref({})
|
||||
const submitLoading = ref(false)
|
||||
const otpInfo = ref()
|
||||
const bindType = ref()
|
||||
// 打开抽屉
|
||||
const onOpen = (type) => {
|
||||
visible.value = true
|
||||
bindType.value = type
|
||||
// 获得动态口令信息
|
||||
userCenterApi.userCenterGetOtpInfo().then((data) => {
|
||||
otpInfo.value = data
|
||||
})
|
||||
}
|
||||
// 关闭抽屉
|
||||
const onClose = () => {
|
||||
visible.value = false
|
||||
otpInfo.value = {}
|
||||
formState.value = {}
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
// 默认要校验的
|
||||
const formRules = {
|
||||
otpCode: [required('请输入动态口令')]
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
const onSubmit = async () => {
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
submitLoading.value = true
|
||||
if(bindType.value === 'bind') {
|
||||
userCenterApi
|
||||
.userCenterBindOtp(formState.value)
|
||||
.then(() => {
|
||||
message.success('绑定成功')
|
||||
visible.value = false
|
||||
emit('successful')
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
} else {
|
||||
userCenterApi
|
||||
.userCenterUnBindOtp(formState.value)
|
||||
.then(() => {
|
||||
message.success('解绑成功')
|
||||
visible.value = false
|
||||
formRef.value.resetFields()
|
||||
emit('successful')
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 调用这个函数将子组件的一些数据和方法暴露出去
|
||||
defineExpose({
|
||||
onOpen
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user