11 changed files with 291 additions and 6 deletions
@ -0,0 +1,82 @@ |
|||
import { beforeEach, describe, expect, it } from 'vitest'; |
|||
|
|||
import { loadScript } from '../resources'; |
|||
|
|||
const testJsPath = |
|||
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js'; |
|||
|
|||
describe('loadScript', () => { |
|||
beforeEach(() => { |
|||
// 每个测试前清空 head,保证环境干净
|
|||
document.head.innerHTML = ''; |
|||
}); |
|||
|
|||
it('should resolve when the script loads successfully', async () => { |
|||
const promise = loadScript(testJsPath); |
|||
|
|||
// 此时脚本元素已被创建并插入
|
|||
const script = document.querySelector( |
|||
`script[src="${testJsPath}"]`, |
|||
) as HTMLScriptElement; |
|||
expect(script).toBeTruthy(); |
|||
|
|||
// 模拟加载成功
|
|||
script.dispatchEvent(new Event('load')); |
|||
|
|||
// 等待 promise resolve
|
|||
await expect(promise).resolves.toBeUndefined(); |
|||
}); |
|||
|
|||
it('should not insert duplicate script and resolve immediately if already loaded', async () => { |
|||
// 先手动插入一个相同 src 的 script
|
|||
const existing = document.createElement('script'); |
|||
existing.src = 'bar.js'; |
|||
document.head.append(existing); |
|||
|
|||
// 再次调用
|
|||
const promise = loadScript('bar.js'); |
|||
|
|||
// 立即 resolve
|
|||
await expect(promise).resolves.toBeUndefined(); |
|||
|
|||
// head 中只保留一个
|
|||
const scripts = document.head.querySelectorAll('script[src="bar.js"]'); |
|||
expect(scripts).toHaveLength(1); |
|||
}); |
|||
|
|||
it('should reject when the script fails to load', async () => { |
|||
const promise = loadScript('error.js'); |
|||
|
|||
const script = document.querySelector( |
|||
'script[src="error.js"]', |
|||
) as HTMLScriptElement; |
|||
expect(script).toBeTruthy(); |
|||
|
|||
// 模拟加载失败
|
|||
script.dispatchEvent(new Event('error')); |
|||
|
|||
await expect(promise).rejects.toThrow('Failed to load script: error.js'); |
|||
}); |
|||
|
|||
it('should handle multiple concurrent calls and only insert one script tag', async () => { |
|||
const p1 = loadScript(testJsPath); |
|||
const p2 = loadScript(testJsPath); |
|||
|
|||
const script = document.querySelector( |
|||
`script[src="${testJsPath}"]`, |
|||
) as HTMLScriptElement; |
|||
expect(script).toBeTruthy(); |
|||
|
|||
// 触发一次 load,两个 promise 都应该 resolve
|
|||
script.dispatchEvent(new Event('load')); |
|||
|
|||
await expect(p1).resolves.toBeUndefined(); |
|||
await expect(p2).resolves.toBeUndefined(); |
|||
|
|||
// 只插入一次
|
|||
const scripts = document.head.querySelectorAll( |
|||
`script[src="${testJsPath}"]`, |
|||
); |
|||
expect(scripts).toHaveLength(1); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* 加载js文件 |
|||
* @param src js文件地址 |
|||
*/ |
|||
function loadScript(src: string) { |
|||
return new Promise<void>((resolve, reject) => { |
|||
if (document.querySelector(`script[src="${src}"]`)) { |
|||
// 如果已经加载过,直接 resolve
|
|||
return resolve(); |
|||
} |
|||
const script = document.createElement('script'); |
|||
script.src = src; |
|||
script.addEventListener('load', () => resolve()); |
|||
script.addEventListener('error', () => |
|||
reject(new Error(`Failed to load script: ${src}`)), |
|||
); |
|||
document.head.append(script); |
|||
}); |
|||
} |
|||
|
|||
export { loadScript }; |
|||
@ -0,0 +1,113 @@ |
|||
<script setup lang="ts"> |
|||
import { useRoute } from 'vue-router'; |
|||
|
|||
import { RiDingding } from '@vben/icons'; |
|||
import { $t } from '@vben/locales'; |
|||
|
|||
import { alert, useVbenModal } from '@vben-core/popup-ui'; |
|||
import { VbenIconButton } from '@vben-core/shadcn-ui'; |
|||
import { loadScript } from '@vben-core/shared/utils'; |
|||
|
|||
interface Props { |
|||
clientId: string; |
|||
corpId: string; |
|||
// 登录回调地址 |
|||
redirectUri?: string; |
|||
// 是否内嵌二维码登录 |
|||
isQrCode?: boolean; |
|||
} |
|||
|
|||
const props = defineProps<Props>(); |
|||
|
|||
const route = useRoute(); |
|||
|
|||
const [Modal, modalApi] = useVbenModal({ |
|||
header: false, |
|||
footer: false, |
|||
fullscreenButton: false, |
|||
class: 'w-[302px] h-[302px] dingding-qrcode-login-modal', |
|||
onOpened() { |
|||
handleQrCodeLogin(); |
|||
}, |
|||
}); |
|||
|
|||
const getRedirectUri = () => { |
|||
const { redirectUri } = props; |
|||
if (redirectUri) { |
|||
return redirectUri; |
|||
} |
|||
return window.location.origin + route.fullPath; |
|||
}; |
|||
|
|||
/** |
|||
* 内嵌二维码登录 |
|||
*/ |
|||
const handleQrCodeLogin = async () => { |
|||
const { clientId, corpId } = props; |
|||
if (!(window as any).DTFrameLogin) { |
|||
// 二维码登录 加载资源 |
|||
await loadScript( |
|||
'https://g.alicdn.com/dingding/h5-dingtalk-login/0.21.0/ddlogin.js', |
|||
); |
|||
} |
|||
(window as any).DTFrameLogin( |
|||
{ |
|||
id: 'dingding_qrcode_login_element', |
|||
width: 300, |
|||
height: 300, |
|||
}, |
|||
{ |
|||
// 注意:redirect_uri 需为完整URL,扫码后钉钉会带code跳转到这里 |
|||
redirect_uri: encodeURIComponent(getRedirectUri()), |
|||
client_id: clientId, |
|||
scope: 'openid corpid', |
|||
response_type: 'code', |
|||
state: '1', |
|||
prompt: 'consent', |
|||
corpId, |
|||
}, |
|||
(loginResult: any) => { |
|||
const { redirectUrl } = loginResult; |
|||
// 这里可以直接进行重定向 |
|||
window.location.href = redirectUrl; |
|||
}, |
|||
(errorMsg: string) => { |
|||
// 这里一般需要展示登录失败的具体原因 |
|||
alert(`Login Error: ${errorMsg}`); |
|||
}, |
|||
); |
|||
}; |
|||
|
|||
const handleLogin = () => { |
|||
const { clientId, corpId, isQrCode } = props; |
|||
if (isQrCode) { |
|||
// 内嵌二维码登录 |
|||
modalApi.open(); |
|||
} else { |
|||
window.location.href = `https://login.dingtalk.com/oauth2/auth?redirect_uri=${encodeURIComponent(getRedirectUri())}&response_type=code&client_id=${clientId}&scope=openid&corpid=${corpId}&prompt=consent`; |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<div> |
|||
<VbenIconButton |
|||
@click="handleLogin" |
|||
:tooltip="$t('authentication.dingdingLogin')" |
|||
tooltip-side="top" |
|||
> |
|||
<RiDingding /> |
|||
</VbenIconButton> |
|||
<Modal> |
|||
<div id="dingding_qrcode_login_element"></div> |
|||
</Modal> |
|||
</div> |
|||
</template> |
|||
|
|||
<style> |
|||
.dingding-qrcode-login-modal { |
|||
.relative { |
|||
padding: 0 !important; |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue