perf: Third-Party Authentication Code Optimization

This commit is contained in:
fit2cloud-chenyw
2026-01-23 18:50:31 +08:00
committed by fit2cloud-chenyw
parent 0d78646722
commit 959529441a
9 changed files with 156 additions and 463 deletions

View File

@@ -823,7 +823,8 @@
"input_account": "Please enter account",
"redirect_2_auth": "Redirecting to {0} authentication, {1} seconds...",
"redirect_immediately": "Redirecting immediately",
"permission_invalid": "Authentication invalid [Current account has insufficient permissions]"
"permission_invalid": "Authentication invalid [Current account has insufficient permissions]",
"scan_qr_login": " Scan QR Code"
},
"supplier": {
"alibaba_cloud_bailian": "Alibaba Cloud Bailian",

View File

@@ -823,7 +823,8 @@
"input_account": "계정을 입력해 주세요",
"redirect_2_auth": "{0} 인증으로 리디렉션 중입니다, {1}초...",
"redirect_immediately": "지금 이동",
"permission_invalid": "인증 무효 [현재 계정의 권한이 부족합니다]"
"permission_invalid": "인증 무효 [현재 계정의 권한이 부족합니다]",
"scan_qr_login": " QR 코드 스캔"
},
"supplier": {
"alibaba_cloud_bailian": "알리바바 클라우드 바이리엔",

View File

@@ -823,7 +823,8 @@
"input_account": "请输入账号",
"redirect_2_auth": "正在跳转至 {0} 认证,{1} 秒...",
"redirect_immediately": "立即跳转",
"permission_invalid": "认证无效【当前账号权限不够】"
"permission_invalid": "认证无效【当前账号权限不够】",
"scan_qr_login": "扫码登录"
},
"supplier": {
"alibaba_cloud_bailian": "阿里云百炼",

View File

@@ -63,7 +63,6 @@
</el-form>
</div>
<Handler
ref="xpackLoginHandler"
v-model:loading="showLoading"
jsname="L2NvbXBvbmVudC9sb2dpbi9IYW5kbGVy"
@switch-tab="switchTab"
@@ -92,7 +91,6 @@ const router = useRouter()
const userStore = useUserStore()
const appearanceStore = useAppearanceStoreWithOut()
const { t } = useI18n()
const xpackLoginHandler = ref<any>(null)
const loginForm = ref({
username: '',
password: '',
@@ -202,7 +200,7 @@ const switchTab = (name: string) => {
.login-btn {
width: 100%;
height: 45px;
height: 40px;
font-size: 16px;
border-radius: 4px;
}

View File

@@ -1,5 +1,7 @@
<template>
<div id="de2-dingtalk-qr" :class="{ 'de2-dingtalk-qr': !isBind }" />
<div class="dingtalk-qr-div">
<div id="de2-dingtalk-qr" :class="{ 'de2-dingtalk-qr': !isBind }" />
</div>
</template>
<script lang="ts" setup>
@@ -46,8 +48,6 @@ const loadQr = (client_id: string, STATE: string, REDIRECT_URI: string) => {
window.DTFrameLogin(
{
id: 'de2-dingtalk-qr',
width: 280,
height: 300,
},
{
redirect_uri: encodeURIComponent(REDIRECT_URI),
@@ -73,7 +73,16 @@ const loadQr = (client_id: string, STATE: string, REDIRECT_URI: string) => {
init()
</script>
<style lang="less" scoped>
.de2-dingtalk-qr {
margin-top: -36px;
.dingtalk-qr-div {
// margin-top: -36px;
width: 234px;
height: 234px;
.de2-dingtalk-qr {
transform: scale(1.3);
transform-origin: top left;
width: 234px;
height: 234px;
margin: -80px;
}
}
</style>

View File

@@ -9,27 +9,26 @@
/>
</div>
<LdapLoginForm v-if="isLdap" />
<el-divider v-if="anyEnable" class="de-other-login-divider">{{
t('login.other_login')
}}</el-divider>
<el-form-item v-if="anyEnable" class="other-login-item">
<div class="login-list">
<QrcodeLdap
v-if="loginCategory.qrcode || loginCategory.ldap"
ref="qrcodeLdapHandler"
:qrcode="loginCategory.qrcode"
:ldap="loginCategory.ldap"
@status-change="qrStatusChange"
/>
<Oidc v-if="loginCategory.oidc" @switch-category="switcherCategory" />
<Oauth2 v-if="loginCategory.oauth2" ref="oauth2Handler" @switch-category="switcherCategory" />
<Cas v-if="loginCategory.cas" @switch-category="switcherCategory" />
<!-- <Saml2 v-if="loginCategory.saml2" ref="saml2Handler" @switch-category="switcherCategory" /> -->
</div>
</el-form-item>
<div class="sqlbot-other-login">
<el-divider v-if="anyEnable" class="de-other-login-divider">{{
t('login.other_login')
}}</el-divider>
<el-form-item v-if="anyEnable" class="other-login-item">
<div class="login-list">
<QrcodeLdap
v-if="loginCategory.qrcode || loginCategory.ldap"
ref="qrcodeLdapHandler"
:qrcode="loginCategory.qrcode"
:ldap="loginCategory.ldap"
@status-change="qrStatusChange"
/>
<Oidc v-if="loginCategory.oidc" @switch-category="switcherCategory" />
<Oauth2 v-if="loginCategory.oauth2" @switch-category="switcherCategory" />
<Cas v-if="loginCategory.cas" @switch-category="switcherCategory" />
</div>
</el-form-item>
</div>
<!-- <mfa-step v-if="showMfa" :mfa-data="state.mfaData" @close="showMfa = false" />
<platform-error v-if="platformLoginMsg" :msg="platformLoginMsg" /> -->
<el-dialog
v-model="loginDialogVisible"
:title="dialogTitle"
@@ -65,11 +64,9 @@ import { useCache } from '@/utils/useCache'
import router from '@/router'
import { useUserStore } from '@/stores/user.ts'
import { getQueryString, getSQLBotAddr, getUrlParams, isPlatformClient } from '@/utils/utils'
import { loadClient, type LoginCategory } from './PlatformClient'
// import MfaStep from './MfaStep.vue'
// import { logoutHandler } from '@/utils/logout'
import { loadClient, origin_mapping, type LoginCategory } from './PlatformClient'
import { useI18n } from 'vue-i18n'
// import PlatformError from './PlatformError.vue'
const isLdap = ref(false)
defineProps<{
loading: boolean
@@ -105,14 +102,7 @@ const qrStatus = ref(false)
const loginCategory = ref({} as LoginCategory)
const anyEnable = ref(false)
const qrcodeLdapHandler = ref()
const oauth2Handler = ref()
const saml2Handler = ref()
/* const state = reactive({
mfaData: {
enabled: false,
ready: false,
},
}) */
const openDialog = (origin_text: string): RedirectDialogResult => {
const platforms: { [key: string]: string } = {
@@ -180,10 +170,6 @@ const closeHandler = () => {
}
const closeDialog = () => {
loginDialogVisible.value = false
/* if (loginDialogVisible.value) {
clearInterval(dialogInterval.value)
dialogInterval.value = null
} */
}
const redirectImmediately = () => {
if (currentSureHandler.value) {
@@ -195,8 +181,6 @@ const init = (cb?: () => void) => {
.then((res) => {
if (res) {
const list: any[] = res as any[]
/* list.push({ name: 'qrcode', enable: true })
list.push({ name: 'wecom', enable: true }) */
list.forEach((item: { name: keyof LoginCategory; enable: boolean }) => {
loginCategory.value[item.name] = item.enable
if (item.enable) {
@@ -228,14 +212,7 @@ const qrStatusChange = (activeComponent: string) => {
switcherCategory({ category: 'ldap', proxy: '' })
}
}
/* const showMfa = ref(false)
const toMfa = (mfa) => {
state.mfaData = mfa
showMfa.value = true
if (document.getElementsByClassName('preheat-container')?.length) {
document.getElementsByClassName('preheat-container')[0].setAttribute('style', 'display: none;')
}
} */
const ssoLogin = (category: any) => {
const array = [
{ category: 'cas', proxy: '/casbi/#' },
@@ -316,357 +293,69 @@ const getCurLocation = () => {
}
return queryRedirectPath
}
const third_party_authentication = (state?: string) => {
if (!state) {
return null
}
const findKey = Object.keys(origin_mapping)
.reverse()
.find((key: any) => state.includes(origin_mapping[key])) as unknown as number | null
if (!findKey) {
return null
}
const originName = origin_mapping[findKey]
if (originName === 'saml2' || originName === 'ldap') {
return null
}
const urlParams = getUrlParams()
const urlFlag = findKey && findKey > 6 ? 'platform' : 'authentication'
const ssoUrl = `/system/${urlFlag}/sso/${findKey}`
if (!urlParams?.redirect_uri) {
urlParams['redirect_uri'] = encodeURIComponent(getSQLBotAddr())
}
request
.post(ssoUrl, urlParams)
.then((res: any) => {
const token = res.access_token
const platform_info = res.platform_info
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
const platform_info_param = {
flag: originName,
origin: findKey,
} as any
if (platform_info) {
platform_info_param['data'] = JSON.stringify(platform_info)
}
if (originName === 'cas') {
const ticket = getQueryString('ticket')
platform_info_param['data'] = ticket
}
userStore.setPlatformInfo(platform_info_param)
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
return findKey
}
const casLogin = () => {
const urlParams = getUrlParams()
const ticket = getQueryString('ticket')
request
.post('/system/authentication/sso/1', urlParams)
.then((res: any) => {
const token = res.access_token
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
userStore.setPlatformInfo({
flag: 'cas',
data: ticket,
origin: 1,
})
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
}
const oauth2Login = () => {
const urlParams = getUrlParams()
request
.post('/system/authentication/sso/4', urlParams)
.then((res: any) => {
const token = res.access_token
const platform_info = res.platform_info
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
userStore.setPlatformInfo({
flag: 'oauth2',
data: platform_info ? JSON.stringify(platform_info) : '',
origin: 4,
})
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
}
const oidcLogin = () => {
const urlParams = getUrlParams()
request
.post('/system/authentication/sso/2', urlParams)
.then((res: any) => {
const token = res.access_token
const platform_info = res.platform_info
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
userStore.setPlatformInfo({
flag: 'oidc',
data: platform_info ? JSON.stringify(platform_info) : '',
origin: 2,
})
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
}
const wecomLogin = () => {
const urlParams = getUrlParams()
request
.post('/system/platform/sso/6', urlParams)
.then((res: any) => {
const token = res.access_token
// const platform_info = res.platform_info
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
userStore.setPlatformInfo({
flag: 'wecom',
// data: platform_info ? JSON.stringify(platform_info) : '',
origin: 6,
})
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
}
const larksuiteLogin = () => {
const urlParams = getUrlParams()
urlParams['redirect_uri'] = encodeURIComponent(getSQLBotAddr())
request
.post('/system/platform/sso/9', urlParams)
.then((res: any) => {
const token = res.access_token
// const platform_info = res.platform_info
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
userStore.setPlatformInfo({
flag: 'larksuite',
// data: platform_info ? JSON.stringify(platform_info) : '',
origin: 9,
})
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
}
const larkLogin = () => {
const urlParams = getUrlParams()
urlParams['redirect_uri'] = encodeURIComponent(getSQLBotAddr())
request
.post('/system/platform/sso/8', urlParams)
.then((res: any) => {
const token = res.access_token
// const platform_info = res.platform_info
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
userStore.setPlatformInfo({
flag: 'lark',
// data: platform_info ? JSON.stringify(platform_info) : '',
origin: 8,
})
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
}
const dingtalkLogin = () => {
const urlParams = getUrlParams()
request
.post('/system/platform/sso/7', urlParams)
.then((res: any) => {
const token = res.access_token
// const platform_info = res.platform_info
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
userStore.setPlatformInfo({
flag: 'dingtalk',
// data: platform_info ? JSON.stringify(platform_info) : '',
origin: 7,
})
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
})
.catch((e: any) => {
userStore.setToken('')
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
setTimeout(() => {
window.location.href = getSQLBotAddr() + window.location.hash
}, 2000)
}, 1500)
})
}
/* const platformLogin = (origin: number) => {
const url = '/system/authentication/sso/cas'
request
.get(url)
.then((res: any) => {
const mfa = res?.mfa
if (mfa?.enabled) {
mfa['origin'] = origin
// toMfa(mfa)
return
}
const token = res.token
if (token && isPlatformClient()) {
wsCache.set('sqlbot-platform-client', true)
}
userStore.setToken(token)
userStore.setExp(res.exp)
userStore.setTime(Date.now())
if (origin === 10 || isLarkPlatform()) {
window.location.href =
getSQLBotAddr() + window.location.hash
} else {
const queryRedirectPath = getCurLocation()
router.push({ path: queryRedirectPath })
}
})
.catch((e: any) => {
userStore.setToken('')
if (isLarkPlatform()) {
setTimeout(() => {
window.location.href =
getSQLBotAddr() + window.location.hash
}, 2000)
} else {
setTimeout(() => {
// logoutHandler(true, true)
platformLoginMsg.value = e?.message || e
}, 1500)
}
})
}
*/
const queryCategoryStatus = () => {
const url = `/system/authentication/platform/status`
return request.get(url)
}
/* const wecomToken = async () => {
const code = getQueryString('code')
const state = getQueryString('state')
if (!code || !state) {
return null
}
const res = await request.post({ url: '/wecom/token', data: { code, state } })
userStore.setToken(res.data)
return res.data
}
const larkToken = async () => {
const code = getQueryString('code')
const state = getQueryString('state')
if (!code || !state) {
return null
}
const res = await request.post({ url: '/lark/token', data: { code, state } })
userStore.setToken(res.data)
return res.data
}
const saml2Token = (cb) => {
const token = getQueryString('saml2Token')
if (!token) {
return
}
userStore.setToken(token)
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
cb && cb()
}
const oauth2Token = (cb) => {
const localCodeKey = localStorage.getItem('DE_OAUTH2_CODE_KEY') || 'code'
const code = getQueryString(localCodeKey)
const state = getQueryString('state')
if (!code || !state) {
throw Error('no code or state')
return null
}
request
.post({ url: '/oauth2/token', data: { code, state } })
.then((res) => {
userStore.setToken(res.data.token)
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
cb && cb()
})
.catch(() => {
setTimeout(() => {
window.location.href =
getSQLBotAddr() + window.location.hash
}, 2000)
})
}
const larksuiteToken = async () => {
const code = getQueryString('code')
const state = getQueryString('state')
if (!code || !state) {
return null
}
const res = await request.post({ url: '/larksuite/token', data: { code, state } })
userStore.setToken(res.data)
return res.data
}
const dingtalkToken = async () => {
const code = getQueryString('code')
const state = getQueryString('state')
if (!code || !state) {
return null
}
const res = await request.post({ url: '/dingtalk/token', data: { code, state } })
userStore.setToken(res.data)
return res.data
} */
const callBackType = () => {
return getQueryString('state')
}
@@ -711,61 +400,17 @@ onMounted(() => {
wsCache.delete('sqlbot-platform-client')
init(async () => {
const state = callBackType()
if (state?.includes('cas') && getQueryString('ticket')) {
// platformLogin(1)
casLogin()
} else if (state?.includes('oauth2')) {
oauth2Login()
} else if (state?.includes('oidc')) {
oidcLogin()
} else if (state?.includes('wecom')) {
wecomLogin()
} else if (state?.includes('dingtalk')) {
dingtalkLogin()
} else if (state?.includes('larksuite')) {
larksuiteLogin()
} else if (state?.includes('lark')) {
larkLogin()
} else {
const originName = third_party_authentication(state as string)
if (!originName) {
auto2Platform()
return
}
/* else if (window.location.pathname.includes('/oidcbi/')) {
platformLogin(2)
} else if (state?.includes('dingtalk')) {
await dingtalkToken()
platformLogin(5)
} else if (state?.includes('larksuite')) {
await larksuiteToken()
platformLogin(7)
} else if (state?.includes('wecom')) {
await wecomToken()
platformLogin(6)
} else if (state?.includes('lark')) {
await larkToken()
platformLogin(4)
} else if (state?.includes('oauth2')) {
oauth2Token(() => {
platformLogin(9)
})
} else if (state?.includes('saml2')) {
saml2Token(() => {
platformLogin(10)
})
} else {
auto2Platform()
} */
})
})
/* defineExpose({
ssoLogin,
toMfa,
}) */
</script>
<style lang="less" scoped>
.login-list {
margin-top: 2px;
width: 100%;
display: flex;
justify-content: center;
@@ -774,10 +419,24 @@ onMounted(() => {
.de-qr-hidden {
display: none;
}
.de-other-login-divider {
border-top: 1px solid #1f232926;
}
.other-login-item {
margin-bottom: 0;
}
.sqlbot-other-login {
height: 68px;
display: flex;
flex-direction: column;
row-gap: 16px;
overflow-y: hidden;
.de-other-login-divider {
border-top: 1px solid #1f232926;
margin: 9px 0 10px 0;
::v-deep(.ed-divider__text) {
color: #8f959e;
font-size: 12px;
font-weight: 400;
}
}
}
</style>

View File

@@ -91,7 +91,7 @@ const submitForm = () => {
.login-btn {
width: 100%;
height: 45px;
height: 40px;
font-size: 16px;
border-radius: 4px;
}

View File

@@ -177,3 +177,15 @@ const toUrl = (url: string) => {
window.location.href =
origin + pathname + url + (redirect?.includes('chatPreview') ? `#${redirect}` : '')
}
export const origin_mapping: { [key: number]: string } = {
1: 'cas',
2: 'oidc',
3: 'ldap',
4: 'oauth2',
5: 'saml2',
6: 'wecom',
7: 'dingtalk',
8: 'lark',
9: 'larksuite',
}

View File

@@ -1,5 +1,5 @@
<template>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tabs v-model="activeName" class="qr-tab" @tab-click="handleClick">
<el-tab-pane v-if="props.wecom" :label="t('user.wecom')" name="wecom"></el-tab-pane>
<el-tab-pane v-if="props.dingtalk" :label="t('user.dingtalk')" name="dingtalk"></el-tab-pane>
<el-tab-pane v-if="props.lark" :label="t('user.lark')" name="lark"></el-tab-pane>
@@ -10,7 +10,7 @@
<el-icon>
<Icon name="logo_wechat-work"><logo_wechatWork class="svg-icon" /></Icon>
</el-icon>
{{ t('user.wecom') }}
{{ t('user.wecom') + t('login.scan_qr_login') }}
</div>
<div class="qrcode">
<wecom-qr v-if="activeName === 'wecom'" />
@@ -21,7 +21,7 @@
<el-icon>
<Icon name="logo_dingtalk"><logo_dingtalk class="svg-icon" /></Icon>
</el-icon>
{{ t('user.dingtalk') }}
{{ t('user.dingtalk') + t('login.scan_qr_login') }}
</div>
<div class="qrcode">
<dingtalk-qr v-if="activeName === 'dingtalk'" />
@@ -32,7 +32,7 @@
<el-icon>
<Icon name="logo_lark"><logo_lark class="svg-icon" /></Icon>
</el-icon>
{{ t('user.lark') }}
{{ t('user.lark') + t('login.scan_qr_login') }}
</div>
<div class="qrcode">
<lark-qr v-if="activeName === 'lark'" />
@@ -43,7 +43,7 @@
<el-icon>
<Icon name="logo_lark"><logo_lark class="svg-icon" /></Icon>
</el-icon>
{{ t('user.larksuite') }}
{{ t('user.larksuite') + t('login.scan_qr_login') }}
</div>
<div class="qrcode">
<larksuite-qr v-if="activeName === 'larksuite'" />
@@ -87,9 +87,11 @@ initActive()
<style lang="less" scoped>
.login-qrcode {
height: 340px;
margin: 24px 0;
height: 274px;
display: flex;
align-items: center;
row-gap: 12px;
flex-direction: column;
.qrcode {
max-width: 286px;
@@ -105,17 +107,27 @@ initActive()
display: flex;
align-items: center;
justify-content: center;
margin: 24px 0 12px 0;
// margin: 24px 0 12px 0;
font-family: var(--de-custom_font, 'PingFang');
font-size: 18px;
font-size: 20px;
font-style: normal;
font-weight: 500;
line-height: 26px;
height: 26px;
line-height: 28px;
column-gap: 8px;
color: #1f2329;
height: 28px;
.ed-icon {
margin-right: 8px;
font-size: 24px;
}
}
}
.qr-tab {
width: auto;
--ed-tabs-header-height: 34px;
::v-deep(.ed-tabs__item) {
font-size: 14px;
font-weight: 400;
align-items: start;
}
}
</style>