【更新】底座增加动态口令登录,完善单点登录客户端用于未来无缝接入统一认证平台,优化诸多代码,更新sql

This commit is contained in:
xuyuxiang
2025-09-14 00:20:56 +08:00
parent 8111719330
commit f4d875ae3c
71 changed files with 2613 additions and 295 deletions

View File

@@ -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')
}
}

View File

@@ -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)
}
}

View File

@@ -29,5 +29,9 @@ export default {
// 第三方登录授权回调
thirdCallback(data) {
return request('callback', data, 'get')
},
// 第三方登录绑定账号
thirdBindAccount(data) {
return request('bindAccount', data)
}
}

View File

@@ -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)
},
}

View File

@@ -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'
}
}

View File

@@ -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: '确定要重置吗?'
}
}

View File

@@ -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

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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);
}
}

View 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>

View File

@@ -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)"
>

View File

@@ -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 = () => {

View File

@@ -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 = () => {

View File

@@ -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)"
>

View File

@@ -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">

View File

@@ -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')
// 验证并提交数据

View File

@@ -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>

View 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>