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