feat: support three-party password-free login

--story=1018017 --user=王孝刚 【登录认证】-X-Pack 支持三方应用(企业微信、钉钉、飞书)免密登录 https://www.tapd.cn/57709429/s/1669142
This commit is contained in:
wxg0103 2025-03-13 11:49:05 +08:00
parent 7bd1dfbdaa
commit f6e089daee
8 changed files with 185 additions and 10 deletions

View File

@ -25,6 +25,7 @@
"axios": "^0.28.0",
"codemirror": "^6.0.1",
"cropperjs": "^1.6.2",
"dingtalk-jsapi": "^2.15.6",
"echarts": "^5.5.0",
"element-plus": "^2.9.1",
"file-saver": "^2.0.5",

View File

@ -162,6 +162,10 @@ const getQrType: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) =>
return get('qr_type', undefined, loading)
}
const getQrSource: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) => {
return get('qr_type/source', undefined, loading)
}
const getDingCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
code,
loading
@ -169,12 +173,25 @@ const getDingCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<
return get('dingtalk', { code }, loading)
}
const getDingOauth2Callback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
code,
loading
) => {
return get('dingtalk/oauth2', { code }, loading)
}
const getWecomCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
code,
loading
) => {
return get('wecom', { code }, loading)
}
const getlarkCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
code,
loading
) => {
return get('feishu/oauth2', { code }, loading)
}
/**
*
@ -206,5 +223,8 @@ export default {
getDingCallback,
getQrType,
getWecomCallback,
postLanguage
postLanguage,
getDingOauth2Callback,
getlarkCallback,
getQrSource
}

View File

@ -143,6 +143,13 @@ const useUserStore = defineStore({
return this.profile()
})
},
async dingOauth2Callback(code: string) {
return UserApi.getDingOauth2Callback(code).then((ok) => {
this.token = ok.data
localStorage.setItem('token', ok.data)
return this.profile()
})
},
async wecomCallback(code: string) {
return UserApi.getWecomCallback(code).then((ok) => {
this.token = ok.data
@ -150,6 +157,13 @@ const useUserStore = defineStore({
return this.profile()
})
},
async larkCallback(code: string) {
return UserApi.getlarkCallback(code).then((ok) => {
this.token = ok.data
localStorage.setItem('token', ok.data)
return this.profile()
})
},
async logout() {
return UserApi.logout().then(() => {
@ -167,6 +181,11 @@ const useUserStore = defineStore({
return ok.data
})
},
async getQrSource() {
return UserApi.getQrSource().then((ok) => {
return ok.data
})
},
async postUserLanguage(lang: string, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
UserApi.postLanguage({ language: lang }, loading)

View File

@ -1,3 +1,5 @@
import { MsgError } from '@/utils/message'
export function toThousands(num: any) {
return num?.toString().replace(/\d+/, function (n: any) {
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
@ -113,3 +115,53 @@ export function getNormalizedUrl(url: string) {
}
return url
}
interface LoadScriptOptions {
jsId?: string // 自定义脚本 ID
forceReload?: boolean // 是否强制重新加载(默认 false
}
export const loadScript = (url: string, options: LoadScriptOptions = {}): Promise<void> => {
const { jsId, forceReload = false } = options
const scriptId = jsId || `script-${btoa(url).slice(0, 12)}` // 生成唯一 ID
return new Promise((resolve, reject) => {
// 检查是否已存在且无需强制加载
const existingScript = document.getElementById(scriptId) as HTMLScriptElement | null
if (existingScript && !forceReload) {
if (existingScript.src === url) {
existingScript.onload = () => resolve() // 复用现有脚本
return
}
// URL 不同则移除旧脚本
existingScript.parentElement?.removeChild(existingScript)
}
// 创建新脚本
const script = document.createElement('script')
script.id = scriptId
script.src = url
script.async = true // 明确启用异步加载
// 成功回调
script.onload = () => {
resolve()
}
// 错误处理(兼容性增强)
script.onerror = () => {
reject(new Error(`Failed to load script: ${url}`))
cleanupScript(script)
}
// 插入到 <head> 确保加载顺序
document.head.appendChild(script)
})
}
// 清理脚本(可选)
const cleanupScript = (script: HTMLScriptElement) => {
script.onload = null
script.onerror = null
script.parentElement?.removeChild(script)
}

View File

@ -174,6 +174,7 @@ const open = async (platform: Platform) => {
app_secret: currentPlatform.config.app_secret,
callback_url: defaultCallbackUrl
}
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/dingtalk`
break
case 'lark':
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/feishu`

View File

@ -17,6 +17,7 @@
import { onMounted, ref, defineAsyncComponent } from 'vue'
import platformApi from '@/api/platform-source'
import useStore from '@/stores'
interface Tab {
key: string
@ -42,11 +43,10 @@ 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 {
const res = await platformApi.getPlatformInfo()
return res.data
return await user.getQrSource()
} catch (error) {
return []
}

View File

@ -123,7 +123,7 @@ const initActive = async () => {
watch(
() => props.config,
(newConfig) => {
if (newConfig.app_secret && newConfig.app_key && newConfig.corp_id) {
if (newConfig.app_key && newConfig.corp_id) {
isConfigReady.value = true
initActive()
}

View File

@ -36,9 +36,9 @@
</div>
</el-form>
<el-button size="large" type="primary" class="w-full" @click="login">{{
$t('views.login.buttons.login')
}}</el-button>
<el-button size="large" type="primary" class="w-full" @click="login"
>{{ $t('views.login.buttons.login') }}
</el-button>
<div class="operate-container flex-between mt-12">
<!-- <el-button class="register" @click="router.push('/register')" link type="primary">
注册
@ -103,15 +103,17 @@
<script setup lang="ts">
import { onMounted, ref, onBeforeMount } from 'vue'
import type { LoginRequest } from '@/api/type/user'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import useStore from '@/stores'
import authApi from '@/api/auth-setting'
import { MsgConfirm, MsgSuccess } from '@/utils/message'
import { MsgConfirm, MsgError, MsgSuccess } from '@/utils/message'
import { t, getBrowserLang } from '@/locales'
import QrCodeTab from '@/views/login/components/QrCodeTab.vue'
import { useI18n } from 'vue-i18n'
import * as dd from 'dingtalk-jsapi'
import { loadScript } from '@/utils/utils'
const { locale } = useI18n({ useScope: 'global' })
const loading = ref<boolean>(false)
const { user } = useStore()
@ -143,11 +145,14 @@ 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 redirectAuth(authType: string) {
if (authType === 'LDAP' || authType === '') {
return
@ -266,6 +271,83 @@ onBeforeMount(() => {
}
})
})
declare const window: any
onMounted(() => {
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" scope>
.login-gradient-divider {