committed by
GitHub
107 changed files with 1599 additions and 429 deletions
@ -1 +1,3 @@ |
|||
import { defineEventHandler } from 'h3'; |
|||
|
|||
export default defineEventHandler(() => 'Test get handler'); |
|||
|
|||
@ -1 +1,3 @@ |
|||
import { defineEventHandler } from 'h3'; |
|||
|
|||
export default defineEventHandler(() => 'Test post handler'); |
|||
|
|||
@ -0,0 +1,36 @@ |
|||
<script lang="ts" setup> |
|||
import { useVbenModal, VbenButton } from '@vben/common-ui'; |
|||
|
|||
const [SlideModal, slideModalApi] = useVbenModal({ |
|||
animationType: 'slide', |
|||
}); |
|||
|
|||
const [ScaleModal, scaleModalApi] = useVbenModal({ |
|||
animationType: 'scale', |
|||
}); |
|||
|
|||
function openSlideModal() { |
|||
slideModalApi.open(); |
|||
} |
|||
|
|||
function openScaleModal() { |
|||
scaleModalApi.open(); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="space-y-4"> |
|||
<div class="flex gap-4"> |
|||
<VbenButton @click="openSlideModal">滑动动画</VbenButton> |
|||
<VbenButton @click="openScaleModal">缩放动画</VbenButton> |
|||
</div> |
|||
|
|||
<SlideModal title="滑动动画示例" class="w-[500px]"> |
|||
<p>这是使用滑动动画的弹窗,从顶部向下滑动进入。</p> |
|||
</SlideModal> |
|||
|
|||
<ScaleModal title="缩放动画示例" class="w-[500px]"> |
|||
<p>这是使用缩放动画的弹窗,以缩放淡入淡出的方式显示。</p> |
|||
</ScaleModal> |
|||
</div> |
|||
</template> |
|||
@ -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> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue