feat: user-login

This commit is contained in:
wangdan-fit2cloud 2025-06-12 12:03:43 +08:00
parent b00ad14214
commit e4e58bc3bd
19 changed files with 1301 additions and 8 deletions

View File

@ -79,7 +79,7 @@ const loginImage = computed(() => {
return `${fileURL.value}`
} else {
const imgName = getThemeImg(theme.themeInfo?.theme)
const imgPath = `../../../assets/theme/${imgName}.jpg`
const imgPath = `../../assets/theme/${imgName}.jpg`
const imageUrl = new URL(imgPath, import.meta.url).href
return imageUrl
}

View File

@ -0,0 +1,60 @@
<template>
<div class="login-warp flex-center">
<div class="login-container w-full h-full">
<div class="flex-center w-full h-full">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getThemeImg } from '@/utils/theme'
import useStore from '@/stores'
import { useLocalStorage } from '@vueuse/core'
import { langList, localeConfigKey, getBrowserLang } from '@/locales/index'
defineProps({
lang: {
type: Boolean,
default: true,
},
})
const { user, theme } = useStore()
const changeLang = (lang: string) => {
useLocalStorage(localeConfigKey, getBrowserLang()).value = lang
window.location.reload()
}
const currentLanguage = computed(() => {
return langList.value?.filter((v: any) => v.value === user.getLanguage())?.[0]?.label
})
const fileURL = computed(() => {
if (theme.themeInfo?.loginImage) {
if (typeof theme.themeInfo?.loginImage === 'string') {
return theme.themeInfo?.loginImage
} else {
return URL.createObjectURL(theme.themeInfo?.loginImage)
}
} else {
return ''
}
})
const loginImage = computed(() => {
if (theme.themeInfo?.loginImage) {
return `${fileURL.value}`
} else {
const imgName = getThemeImg(theme.themeInfo?.theme)
const imgPath = `../../assets/theme/${imgName}.jpg`
const imageUrl = new URL(imgPath, import.meta.url).href
return imageUrl
}
})
</script>
<style lang="scss" scoped>
.login-warp {
height: 100vh;
}
</style>

View File

@ -47,7 +47,7 @@ instance.interceptors.response.use(
}
if (
!response.config.url.includes('/valid') &&
!response.config.url.includes('/function_lib/debug')
!response.config.url.includes('/tool/debug')
) {
MsgError(response.data.message)
return Promise.reject(response.data)

View File

@ -25,8 +25,8 @@ router.beforeEach(
return
}
const { user, login } = useStore()
const notAuthRouteNameList = ['register', 'login', 'forgot_password', 'reset_password', 'Chat']
const notAuthRouteNameList = ['login', 'ForgotPassword', 'ResetPassword', 'Chat', 'UserLogin']
if (!notAuthRouteNameList.includes(to.name ? to.name.toString() : '')) {
if (to.query && to.query.token) {
localStorage.setItem('token', to.query.token.toString())

View File

@ -24,6 +24,13 @@ export const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/chat/index.vue'),
},
// 对话用户登录
{
path: '/user-login/:accessToken',
name: 'UserLogin',
component: () => import('@/views/chat/user-login/index.vue'),
},
{
path: '/login',
name: 'login',

View File

@ -0,0 +1,137 @@
<template>
<UserLoginLayout>
<LoginContainer :subTitle="$t('theme.defaultSlogan')">
<h2 class="mb-24">{{ $t('views.login.resetPassword') }}</h2>
<el-form
class="reset-password-form"
ref="resetPasswordFormRef"
:model="resetPasswordForm"
:rules="rules"
>
<div class="mb-24">
<el-form-item prop="password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.password"
:placeholder="$t('views.login.loginForm.password.placeholder')"
show-password
>
</el-input>
</el-form-item>
</div>
<div class="mb-24">
<el-form-item prop="re_password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.re_password"
:placeholder="$t('views.login.loginForm.re_password.placeholder')"
show-password
>
</el-input>
</el-form-item>
</div>
</el-form>
<el-button size="large" type="primary" class="w-full" @click="resetPassword">{{
$t('common.confirm')
}}</el-button>
<div class="operate-container mt-12">
<el-button
size="large"
class="register"
@click="router.push('/login')"
link
type="primary"
icon="ArrowLeft"
>
{{ $t('views.login.buttons.backLogin') }}
</el-button>
</div>
</LoginContainer>
</UserLoginLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { ResetPasswordRequest } from '@/api/type/user'
import LoginContainer from '@/layout/login-layout/LoginContainer.vue'
import UserLoginLayout from '@/layout/login-layout/UserLoginLayout.vue'
import { useRouter, useRoute } from 'vue-router'
import { MsgSuccess } from '@/utils/message'
import type { FormInstance, FormRules } from 'element-plus'
import UserApi from '@/api/user/user'
import { t } from '@/locales'
const router = useRouter()
const route = useRoute()
const {
params: { code, email },
} = route
const resetPasswordForm = ref<ResetPasswordRequest>({
password: '',
re_password: '',
email: '',
code: '',
})
onMounted(() => {
if (code && email) {
resetPasswordForm.value.code = code as string
resetPasswordForm.value.email = email as string
} else {
router.push('forgot_password')
}
})
const rules = ref<FormRules<ResetPasswordRequest>>({
password: [
{
required: true,
message: t('views.login.loginForm.re_password.requiredMessage'),
trigger: 'blur',
},
{
min: 6,
max: 20,
message: t('views.login.loginForm.password.lengthMessage'),
trigger: 'blur',
},
],
re_password: [
{
required: true,
message: t('views.login.loginForm.re_password.requiredMessage'),
trigger: 'blur',
},
{
min: 6,
max: 20,
message: t('views.login.loginForm.password.lengthMessage'),
trigger: 'blur',
},
{
validator: (rule, value, callback) => {
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
callback(new Error(t('views.login.loginForm.re_password.validatorMessage')))
} else {
callback()
}
},
trigger: 'blur',
},
],
})
const resetPasswordFormRef = ref<FormInstance>()
const loading = ref<boolean>(false)
const resetPassword = () => {
resetPasswordFormRef.value
?.validate()
.then(() => UserApi.resetPassword(resetPasswordForm.value, loading))
.then(() => {
MsgSuccess(t('common.modifySuccess'))
router.push({ name: 'login' })
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,137 @@
<template>
<UserLoginLayout>
<LoginContainer :subTitle="$t('theme.defaultSlogan')">
<h2 class="mb-24">{{ $t('views.login.resetPassword') }}</h2>
<el-form
class="reset-password-form"
ref="resetPasswordFormRef"
:model="resetPasswordForm"
:rules="rules"
>
<div class="mb-24">
<el-form-item prop="password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.password"
:placeholder="$t('views.login.loginForm.password.placeholder')"
show-password
>
</el-input>
</el-form-item>
</div>
<div class="mb-24">
<el-form-item prop="re_password">
<el-input
type="password"
size="large"
class="input-item"
v-model="resetPasswordForm.re_password"
:placeholder="$t('views.login.loginForm.re_password.placeholder')"
show-password
>
</el-input>
</el-form-item>
</div>
</el-form>
<el-button size="large" type="primary" class="w-full" @click="resetPassword">{{
$t('common.confirm')
}}</el-button>
<div class="operate-container mt-12">
<el-button
size="large"
class="register"
@click="router.push('/login')"
link
type="primary"
icon="ArrowLeft"
>
{{ $t('views.login.buttons.backLogin') }}
</el-button>
</div>
</LoginContainer>
</UserLoginLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import LoginContainer from '@/layout/login-layout/LoginContainer.vue'
import UserLoginLayout from '@/layout/login-layout/UserLoginLayout.vue'
import type { ResetPasswordRequest } from '@/api/type/user'
import { useRouter, useRoute } from 'vue-router'
import { MsgSuccess } from '@/utils/message'
import type { FormInstance, FormRules } from 'element-plus'
import UserApi from '@/api/user/user-manage'
import { t } from '@/locales'
const router = useRouter()
const route = useRoute()
const {
params: { code, email },
} = route
const resetPasswordForm = ref<ResetPasswordRequest>({
password: '',
re_password: '',
email: '',
code: '',
})
onMounted(() => {
if (code && email) {
resetPasswordForm.value.code = code as string
resetPasswordForm.value.email = email as string
} else {
router.push('forgot_password')
}
})
const rules = ref<FormRules<ResetPasswordRequest>>({
password: [
{
required: true,
message: t('views.login.loginForm.re_password.requiredMessage'),
trigger: 'blur',
},
{
min: 6,
max: 20,
message: t('views.login.loginForm.password.lengthMessage'),
trigger: 'blur',
},
],
re_password: [
{
required: true,
message: t('views.login.loginForm.re_password.requiredMessage'),
trigger: 'blur',
},
{
min: 6,
max: 20,
message: t('views.login.loginForm.password.lengthMessage'),
trigger: 'blur',
},
{
validator: (rule, value, callback) => {
if (resetPasswordForm.value.password != resetPasswordForm.value.re_password) {
callback(new Error(t('views.login.loginForm.re_password.validatorMessage')))
} else {
callback()
}
},
trigger: 'blur',
},
],
})
const resetPasswordFormRef = ref<FormInstance>()
const loading = ref<boolean>(false)
const resetPassword = () => {
resetPasswordFormRef.value
?.validate()
.then(() => UserApi.resetPassword(resetPasswordForm.value, loading))
.then(() => {
MsgSuccess(t('common.modifySuccess'))
router.push({ name: 'login' })
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,37 @@
<template>
<div class="login-form-container">
<div class="login-title">
<div class="logo text-center">
<LogoFull height="45px" />
</div>
<div class="sub-title text-center" v-if="subTitle">
<el-text type="info">{{ subTitle }}</el-text>
</div>
</div>
<el-card class="login-card">
<slot></slot>
</el-card>
</div>
</template>
<script setup lang="ts">
defineProps({
title: String,
subTitle: String
})
</script>
<style lang="scss" scoped>
.login-form-container {
width: 480px;
.login-title {
margin-bottom: 32px;
.sub-title {
font-size: 16px;
}
}
.login-card {
border-radius: 8px;
padding: 18px;
}
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div class="login-warp flex-center">
<div class="login-container w-full h-full">
<el-row class="container w-full h-full">
<el-col :xs="0" :sm="0" :md="10" :lg="10" :xl="10" class="left-container">
<div class="login-image" :style="{ backgroundImage: `url(${loginImage})` }"></div>
</el-col>
<el-col :xs="24" :sm="24" :md="14" :lg="14" :xl="14" class="right-container flex-center">
<el-dropdown trigger="click" type="primary" class="lang" v-if="lang">
<template #dropdown>
<el-dropdown-menu style="width: 180px">
<el-dropdown-item
v-for="(lang, index) in langList"
:key="index"
:value="lang.value"
@click="changeLang(lang.value)"
class="flex-between"
>
<span :class="lang.value === user.getLanguage() ? 'primary' : ''">{{
lang.label
}}</span>
<el-icon
:class="lang.value === user.getLanguage() ? 'primary' : ''"
v-if="lang.value === user.getLanguage()"
>
<Check />
</el-icon>
</el-dropdown-item>
</el-dropdown-menu>
</template>
<el-button>
{{ currentLanguage }}<el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
</el-dropdown>
<slot></slot>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { getThemeImg } from '@/utils/theme'
import useStore from '@/stores'
import { useLocalStorage } from '@vueuse/core'
import { langList, localeConfigKey, getBrowserLang } from '@/locales/index'
defineProps({
lang: {
type: Boolean,
default: true,
},
})
const { user, theme } = useStore()
const changeLang = (lang: string) => {
useLocalStorage(localeConfigKey, getBrowserLang()).value = lang
window.location.reload()
}
const currentLanguage = computed(() => {
return langList.value?.filter((v: any) => v.value === user.getLanguage())?.[0]?.label
})
const fileURL = computed(() => {
if (theme.themeInfo?.loginImage) {
if (typeof theme.themeInfo?.loginImage === 'string') {
return theme.themeInfo?.loginImage
} else {
return URL.createObjectURL(theme.themeInfo?.loginImage)
}
} else {
return ''
}
})
const loginImage = computed(() => {
if (theme.themeInfo?.loginImage) {
return `${fileURL.value}`
} else {
const imgName = getThemeImg(theme.themeInfo?.theme)
const imgPath = `../../assets/theme/${imgName}.jpg`
const imageUrl = new URL(imgPath, import.meta.url).href
return imageUrl
}
})
</script>
<style lang="scss" scoped>
.login-warp {
height: 100vh;
.login-image {
background-repeat: no-repeat;
background-position: center;
background-size: cover;
width: 100%;
height: 100%;
}
.right-container {
position: relative;
.lang {
position: absolute;
right: 20px;
top: 20px;
}
}
}
</style>

View File

@ -0,0 +1,450 @@
<template>
<UserLoginLayout v-if="!loading" v-loading="loading">
<LoginContainer :subTitle="theme.themeInfo?.slogan || $t('theme.defaultSlogan')">
<h2 class="mb-24" v-if="!showQrCodeTab">{{ loginMode || $t('views.login.title') }}</h2>
<div v-if="!showQrCodeTab">
<el-form
class="login-form"
:rules="rules"
:model="loginForm"
ref="loginFormRef"
@keyup.enter="login"
>
<div class="mb-24">
<el-form-item prop="username">
<el-input
size="large"
class="input-item"
v-model="loginForm.username"
:placeholder="$t('views.login.loginForm.username.placeholder')"
>
</el-input>
</el-form-item>
</div>
<div class="mb-24">
<el-form-item prop="password">
<el-input
type="password"
size="large"
class="input-item"
v-model="loginForm.password"
:placeholder="$t('views.login.loginForm.password.placeholder')"
show-password
>
</el-input>
</el-form-item>
</div>
<div class="mb-24" v-if="loginMode !== 'LDAP'">
<el-form-item prop="captcha">
<div class="flex-between w-full">
<el-input
size="large"
class="input-item"
v-model="loginForm.captcha"
:placeholder="$t('views.login.loginForm.captcha.placeholder')"
>
</el-input>
<img
:src="identifyCode"
alt=""
height="38"
class="ml-8 cursor border border-r-4"
@click="makeCode"
/>
</div>
</el-form-item>
</div>
</el-form>
<el-button
size="large"
type="primary"
class="w-full"
@click="loginHandle"
:loading="loading"
>
{{ $t('views.login.buttons.login') }}
</el-button>
<div class="operate-container flex-between mt-12">
<el-button
:loading="loading"
class="forgot-password"
@click="router.push('/forgot_password')"
link
type="primary"
>
{{ $t('views.login.forgotPassword') }}?
</el-button>
</div>
</div>
<div v-if="showQrCodeTab">
<QrCodeTab :tabs="orgOptions" />
</div>
<div class="login-gradient-divider lighter mt-24" v-if="modeList.length > 1">
<span>{{ $t('views.login.moreMethod') }}</span>
</div>
<div class="text-center mt-16">
<template v-for="item in modeList">
<el-button
v-if="item !== '' && loginMode !== item && item !== 'QR_CODE'"
circle
:key="item"
class="login-button-circle color-secondary"
@click="changeMode(item)"
>
<span
:style="{
'font-size': item === 'OAUTH2' ? '8px' : '10px',
color: user.themeInfo?.theme,
}"
>{{ item }}</span
>
</el-button>
<el-button
v-if="item === 'QR_CODE' && loginMode !== item"
circle
:key="item"
class="login-button-circle color-secondary"
@click="changeMode('QR_CODE')"
>
<img src="@/assets/scan/icon_qr_outlined.svg" width="25px" />
</el-button>
<el-button
v-if="item === '' && loginMode !== ''"
circle
:key="item"
class="login-button-circle color-secondary"
style="font-size: 24px"
icon="UserFilled"
@click="changeMode('')"
/>
</template>
</div>
</LoginContainer>
</UserLoginLayout>
</template>
<script setup lang="ts">
import { onMounted, ref, onBeforeMount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import type { LoginRequest } from '@/api/type/login'
import LoginContainer from '@/layout/login-layout/LoginContainer.vue'
import UserLoginLayout from '@/layout/login-layout/UserLoginLayout.vue'
import loginApi from '@/api/user/login'
import authApi from '@/api/system-settings/auth-setting'
import { t, getBrowserLang } from '@/locales'
import useStore from '@/stores'
import { useI18n } from 'vue-i18n'
import QrCodeTab from '@/views/login/scanCompinents/QrCodeTab.vue'
import { MsgConfirm, MsgError } from '@/utils/message.ts'
import * as dd from 'dingtalk-jsapi'
import { loadScript } from '@/utils/utils'
const router = useRouter()
const { login, user, theme } = useStore()
const { locale } = useI18n({ useScope: 'global' })
const loading = ref<boolean>(false)
const identifyCode = ref<string>('')
const loginFormRef = ref<FormInstance>()
const loginForm = ref<LoginRequest>({
username: '',
password: '',
captcha: '',
})
const rules = ref<FormRules<LoginRequest>>({
username: [
{
required: true,
message: t('views.login.loginForm.username.requiredMessage'),
trigger: 'blur',
},
],
password: [
{
required: true,
message: t('views.login.loginForm.password.requiredMessage'),
trigger: 'blur',
},
],
captcha: [
{
required: true,
message: t('views.login.loginForm.captcha.requiredMessage'),
trigger: 'blur',
},
],
})
const loginHandle = () => {
loginFormRef.value?.validate().then(() => {
if (loginMode.value === 'LDAP') {
login.asyncLdapLogin(loginForm.value, loading).then(() => {
locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US'
router.push({ name: 'home' })
})
} else {
login.asyncLogin(loginForm.value, loading).then(() => {
locale.value = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US'
localStorage.setItem('workspace_id', 'default')
router.push({ name: 'home' })
})
}
})
}
function makeCode() {
loginApi.getCaptcha().then((res: any) => {
identifyCode.value = res.data.captcha
})
}
onBeforeMount(() => {
makeCode()
})
const modeList = ref<string[]>([''])
const QrList = ref<any[]>([''])
const loginMode = ref('')
const showQrCodeTab = ref(false)
interface qrOption {
key: string
value: string
}
const orgOptions = ref<qrOption[]>([])
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
function redirectAuth(authType: string) {
if (authType === 'LDAP' || authType === '') {
return
}
authApi.getAuthSetting(authType, loading).then((res: any) => {
if (!res.data) {
return
}
MsgConfirm(t('views.login.jump_tip'), '', {
confirmButtonText: t('views.login.jump'),
cancelButtonText: t('common.cancel'),
confirmButtonClass: '',
})
.then(() => {
if (!res.data.config_data) {
return
}
const config = res.data.config_data
const redirectUrl = eval(`\`${config.redirectUrl}\``)
let url
if (authType === 'CAS') {
url = config.ldpUri
if (url.indexOf('?') !== -1) {
url = `${config.ldpUri}&service=${encodeURIComponent(redirectUrl)}`
} else {
url = `${config.ldpUri}?service=${encodeURIComponent(redirectUrl)}`
}
}
if (authType === 'OIDC') {
const scope = config.scope || 'openid+profile+email'
url = `${config.authEndpoint}?client_id=${config.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}`
if (config.state) {
url += `&state=${config.state}`
}
}
if (authType === 'OAuth2') {
url =
`${config.authEndpoint}?client_id=${config.clientId}&response_type=code` +
`&redirect_uri=${redirectUrl}&state=${uuidv4()}`
if (config.scope) {
url += `&scope=${config.scope}`
}
}
if (url) {
window.location.href = url
}
})
.catch(() => {})
})
}
function changeMode(val: string) {
loginMode.value = val === 'LDAP' ? val : ''
if (val === 'QR_CODE') {
loginMode.value = val
showQrCodeTab.value = true
return
}
showQrCodeTab.value = false
loginForm.value = {
username: '',
password: '',
captcha: '',
}
redirectAuth(val)
loginFormRef.value?.clearValidate()
}
onBeforeMount(() => {
loading.value = true
user.asyncGetProfile().then((res) => {
if (user.isEnterprise()) {
user
.getAuthType()
.then((res) => {
//LDAPLDAP
const ldapIndex = res.indexOf('LDAP')
if (ldapIndex !== -1) {
const [ldap] = res.splice(ldapIndex, 1)
res.unshift(ldap)
}
modeList.value = [...modeList.value, ...res]
})
.finally(() => (loading.value = false))
user
.getQrType()
.then((res) => {
if (res.length > 0) {
modeList.value = ['QR_CODE', ...modeList.value]
QrList.value = res
QrList.value.forEach((item) => {
orgOptions.value.push({
key: item,
value:
item === 'wecom'
? t('views.system.authentication.scanTheQRCode.wecom')
: item === 'dingtalk'
? t('views.system.authentication.scanTheQRCode.dingtalk')
: t('views.system.authentication.scanTheQRCode.lark'),
})
})
}
})
.finally(() => (loading.value = false))
} else {
loading.value = false
}
})
})
declare const window: any
onMounted(() => {
makeCode()
const route = useRoute()
const currentUrl = ref(route.fullPath)
const params = new URLSearchParams(currentUrl.value.split('?')[1])
const client = params.get('client')
const handleDingTalk = () => {
const code = params.get('corpId')
if (code) {
dd.runtime.permission.requestAuthCode({ corpId: code }).then((res) => {
console.log('DingTalk client request success:', res)
user.dingOauth2Callback(res.code).then(() => {
router.push({ name: 'home' })
})
})
}
}
const handleLark = () => {
const appId = params.get('appId')
const callRequestAuthCode = () => {
window.tt?.requestAuthCode({
appId: appId,
success: (res: any) => {
user.larkCallback(res.code).then(() => {
router.push({ name: 'home' })
})
},
fail: (error: any) => {
MsgError(error)
},
})
}
loadScript('https://lf-scm-cn.feishucdn.com/lark/op/h5-js-sdk-1.5.35.js', {
jsId: 'lark-sdk',
forceReload: true,
})
.then(() => {
if (window.tt) {
window.tt.requestAccess({
appID: appId,
scopeList: [],
success: (res: any) => {
user.larkCallback(res.code).then(() => {
router.push({ name: 'home' })
})
},
fail: (error: any) => {
const { errno } = error
if (errno === 103) {
callRequestAuthCode()
}
},
})
} else {
callRequestAuthCode()
}
})
.catch((error) => {
console.error('SDK 加载失败:', error)
})
}
switch (client) {
case 'dingtalk':
handleDingTalk()
break
case 'lark':
handleLark()
break
default:
break
}
})
</script>
<style lang="scss" scoped>
.login-gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 25%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 25%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
.login-button-circle {
padding: 20px !important;
margin: 0 4px;
width: 32px;
height: 32px;
text-align: center;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<el-tabs v-model="activeKey" @tab-change="selectTab">
<template v-for="item in tabs" :key="item.key">
<el-tab-pane :label="item.value" :name="item.key">
<div class="text-center mt-16" v-if="item.key === activeKey">
<component
:is="defineAsyncComponent(() => import(`./${item.key}QrCode.vue`))"
:config="config"
/>
</div>
</el-tab-pane>
</template>
</el-tabs>
</template>
<script setup lang="ts">
import { onMounted, ref, defineAsyncComponent } from 'vue'
import useStore from '@/stores'
interface Tab {
key: string
value: string
}
interface PlatformConfig {
app_key: string
app_secret: string
auth_type: string
config: any
}
interface Config {
app_key: string
app_secret: string
corpId?: string
agentId?: string
}
const props = defineProps<{ tabs: Tab[] }>()
const activeKey = ref('')
const allConfigs = ref<PlatformConfig[]>([])
const config = ref<Config>({ app_key: '', app_secret: '' })
// const logoUrl = ref('')
const { user } = useStore()
async function getPlatformInfo() {
try {
return await user.getQrSource()
} catch (error) {
return []
}
}
onMounted(async () => {
if (props.tabs.length > 0) {
activeKey.value = props.tabs[0].key
}
allConfigs.value = await getPlatformInfo()
updateConfig(activeKey.value)
})
const updateConfig = (key: string) => {
const selectedConfig = allConfigs.value.find((item) => item.auth_type === key)
if (selectedConfig && selectedConfig.config) {
config.value = selectedConfig.config
}
}
const selectTab = (key: string) => {
activeKey.value = key
updateConfig(key)
}
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,141 @@
<template>
<div class="flex-center mb-16">
<img src="@/assets/scan/logo_dingtalk.svg" alt="" width="24px" class="mr-4" />
<h2>{{ $t('views.system.authentication.scanTheQRCode.dingtalkQrCode') }}</h2>
</div>
<div class="ding-talk-qrName">
<div id="ding-talk-qr"></div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { useScriptTag } from '@vueuse/core'
import { ref, watch } from 'vue'
import useStore from '@/stores'
import { MsgError } from '@/utils/message'
// DTFrameLogin QRLogin
declare global {
interface Window {
DTFrameLogin: (
frameParams: IDTLoginFrameParams,
loginParams: IDTLoginLoginParams,
successCbk: (result: IDTLoginSuccess) => void,
errorCbk?: (errorMsg: string) => void
) => void
QRLogin: (QRLogin: qrLogin) => Record<any, any>
}
}
//
interface IDTLoginFrameParams {
id: string
width?: number
height?: number
}
interface IDTLoginLoginParams {
redirect_uri: string
response_type: string
client_id: string
scope: string
prompt: string
state?: string
org_type?: string
corpId?: string
exclusiveLogin?: string
exclusiveCorpId?: string
}
interface IDTLoginSuccess {
redirectUrl: string
authCode: string
state?: string
}
interface qrLogin {
id: string
goto: string
width: string
height: string
style?: string
}
const props = defineProps<{
config: {
app_secret: string
app_key: string
corp_id: string
}
}>()
const router = useRouter()
const { user } = useStore()
const { load } = useScriptTag('https://g.alicdn.com/dingding/h5-dingtalk-login/0.21.0/ddlogin.js')
const isConfigReady = ref(false)
const initActive = async () => {
try {
await load(true)
if (!isConfigReady.value) {
return
}
const data = {
appKey: props.config.app_key,
appSecret: props.config.app_secret,
corp_id: props.config.corp_id
}
const redirectUri = encodeURIComponent(window.location.origin)
window.DTFrameLogin(
{
id: 'ding-talk-qr',
width: 280,
height: 280
},
{
redirect_uri: redirectUri,
client_id: data.appKey,
scope: 'openid corpid',
response_type: 'code',
state: 'fit2cloud-ding-qr',
prompt: 'consent',
corpId: data.corp_id
},
(loginResult) => {
const authCode = loginResult.authCode
user.dingCallback(authCode).then(() => {
router.push({ name: 'home' })
})
},
(errorMsg: string) => {
MsgError(errorMsg)
}
)
} catch (error) {
console.error(error)
}
}
watch(
() => props.config,
(newConfig) => {
if (newConfig.app_key && newConfig.corp_id) {
isConfigReady.value = true
initActive()
}
},
{ immediate: true }
)
</script>
<style lang="scss">
.ding-talk-qrName {
border: 1px solid #e8e8e8;
border-radius: 8px;
height: 280px;
width: 280px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="flex-center mb-16">
<img src="@/assets/scan/logo_lark.svg " alt="" width="24px" class="mr-4" />
<h2>{{ $t('views.system.authentication.scanTheQRCode.larkQrCode') }}</h2>
</div>
<div id="lark-qr" class="lark-qrName"></div>
</template>
<script lang="ts" setup>
import { useScriptTag } from '@vueuse/core'
import { onMounted } from 'vue'
const { load } = useScriptTag(
'https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js'
)
const props = defineProps<{
config: {
app_secret: string
app_key: string
}
}>()
const initActive = async () => {
const scriptLoaded = await load(true)
if (!scriptLoaded) {
console.error('飞书二维码 SDK 加载失败')
return
}
const data = {
agentId: props.config.app_key,
appSecret: props.config.app_secret
}
const redirectUrl = encodeURIComponent(`${window.location.origin}/api/feishu`)
const url = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=${data.agentId}&redirect_uri=${redirectUrl}&response_type=code&state=fit2cloud-lark-qr`
const QRLoginObj = window.QRLogin({
id: 'lark-qr',
goto: url,
width: '266',
height: '266',
style: 'width:280px;height:280px;border:1px solid #e8e8e8;margin:0 auto;border-radius:8px;'
})
window.addEventListener('message', async (event: any) => {
if (QRLoginObj.matchOrigin(event.origin) && QRLoginObj.matchData(event.data)) {
const loginTmpCode = event.data.tmp_code
window.location.href = `${url}&tmp_code=${loginTmpCode}`
}
})
}
onMounted(() => {
initActive()
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,79 @@
<template>
<div id="wecom-qr" class="wecom-qr" style="margin-left: 50px"></div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import * as ww from '@wecom/jssdk'
import {
WWLoginLangType,
WWLoginPanelSizeType,
WWLoginRedirectType,
WWLoginType
} from '@wecom/jssdk'
import { ref, nextTick, defineProps } from 'vue'
import { MsgError } from '@/utils/message'
import useStore from '@/stores'
import { getBrowserLang } from '@/locales'
const router = useRouter()
const wwLogin = ref({})
const obj = ref<any>({ isWeComLogin: false })
const { user } = useStore()
const props = defineProps<{
config: {
app_secret: string
app_key: string
corp_id?: string
agent_id?: string
}
}>()
const init = async () => {
await nextTick() // DOM
const data = {
corpId: props.config.corp_id,
agentId: props.config.agent_id
}
const lang = localStorage.getItem('MaxKB-locale') || getBrowserLang() || 'en-US'
const redirectUri = window.location.origin
try {
wwLogin.value = ww.createWWLoginPanel({
el: '#wecom-qr',
params: {
login_type: WWLoginType.corpApp,
appid: data.corpId || '',
agentid: data.agentId,
redirect_uri: redirectUri,
state: 'fit2cloud-wecom-qr',
lang: lang === 'zh-CN' || lang === 'zh-Hant' ? WWLoginLangType.zh : WWLoginLangType.en,
redirect_type: WWLoginRedirectType.callback,
panel_size: WWLoginPanelSizeType.small
},
onCheckWeComLogin: obj.value,
async onLoginSuccess({ code }: any) {
user.wecomCallback(code).then(() => {
setTimeout(() => {
router.push({ name: 'home' })
})
})
},
onLoginFail(err) {
MsgError(`${err.errMsg}`)
}
})
} catch (error) {
console.error('Error initializing login panel:', error)
}
}
init()
</script>
<style scoped lang="scss">
.wecom-qr {
margin-top: -20px;
height: 331px;
}
</style>

View File

@ -28,7 +28,7 @@
size="large"
class="input-item"
v-model="resetPasswordForm.re_password"
:placeholder="$t('views.login.loginForm..re_password.placeholder')"
:placeholder="$t('views.login.loginForm.re_password.placeholder')"
show-password
>
</el-input>
@ -55,6 +55,8 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import LoginContainer from '@/layout/login-layout/LoginContainer.vue'
import LoginLayout from '@/layout/login-layout/LoginLayout.vue'
import type { ResetPasswordRequest } from '@/api/type/user'
import { useRouter, useRoute } from 'vue-router'
import { MsgSuccess } from '@/utils/message'

View File

@ -55,6 +55,8 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import LoginContainer from '@/layout/login-layout/LoginContainer.vue'
import LoginLayout from '@/layout/login-layout/LoginLayout.vue'
import type { ResetPasswordRequest } from '@/api/type/user'
import { useRouter, useRoute } from 'vue-router'
import { MsgSuccess } from '@/utils/message'

View File

@ -124,8 +124,8 @@ import {onMounted, ref, onBeforeMount} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import type {FormInstance, FormRules} from 'element-plus'
import type {LoginRequest} from '@/api/type/login'
import LoginContainer from '@/views/login/components/LoginContainer.vue'
import LoginLayout from '@/views/login/components/LoginLayout.vue'
import LoginContainer from '@/layout/login-layout/LoginContainer.vue'
import LoginLayout from '@/layout/login-layout/LoginLayout.vue'
import loginApi from '@/api/user/login'
import authApi from '@/api/system-settings/auth-setting'
import {t, getBrowserLang} from '@/locales'

View File

@ -53,8 +53,8 @@
<script lang="ts" setup>
import { computed } from 'vue'
import LoginLayout from "@/views/login/components/LoginLayout.vue";
import LoginContainer from "@/views/login/components/LoginContainer.vue";
import LoginLayout from "@/layout/login-layout/LoginLayout.vue";
import LoginContainer from "@/layout/login-layout/LoginContainer.vue";
const props = defineProps({
data: {