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'); |
export default defineEventHandler(() => 'Test get handler'); |
||||
|
|||||
@ -1 +1,3 @@ |
|||||
|
import { defineEventHandler } from 'h3'; |
||||
|
|
||||
export default defineEventHandler(() => 'Test post handler'); |
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