diff --git a/packages/@core/base/shared/src/utils/__tests__/resources.test.ts b/packages/@core/base/shared/src/utils/__tests__/resources.test.ts new file mode 100644 index 000000000..f14ff896e --- /dev/null +++ b/packages/@core/base/shared/src/utils/__tests__/resources.test.ts @@ -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); + }); +}); diff --git a/packages/@core/base/shared/src/utils/index.ts b/packages/@core/base/shared/src/utils/index.ts index 925af1c12..c067c7316 100644 --- a/packages/@core/base/shared/src/utils/index.ts +++ b/packages/@core/base/shared/src/utils/index.ts @@ -7,6 +7,7 @@ export * from './inference'; export * from './letter'; export * from './merge'; export * from './nprogress'; +export * from './resources'; export * from './state-handler'; export * from './to'; export * from './tree'; diff --git a/packages/@core/base/shared/src/utils/resources.ts b/packages/@core/base/shared/src/utils/resources.ts new file mode 100644 index 000000000..c5afa7f10 --- /dev/null +++ b/packages/@core/base/shared/src/utils/resources.ts @@ -0,0 +1,21 @@ +/** + * 加载js文件 + * @param src js文件地址 + */ +function loadScript(src: string) { + return new Promise((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 }; diff --git a/packages/effects/common-ui/src/ui/authentication/dingding-login.vue b/packages/effects/common-ui/src/ui/authentication/dingding-login.vue new file mode 100644 index 000000000..4c63301ee --- /dev/null +++ b/packages/effects/common-ui/src/ui/authentication/dingding-login.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/packages/effects/common-ui/src/ui/authentication/third-party-login.vue b/packages/effects/common-ui/src/ui/authentication/third-party-login.vue index 16533f490..930638ecb 100644 --- a/packages/effects/common-ui/src/ui/authentication/third-party-login.vue +++ b/packages/effects/common-ui/src/ui/authentication/third-party-login.vue @@ -1,12 +1,19 @@ diff --git a/packages/effects/hooks/src/use-app-config.ts b/packages/effects/hooks/src/use-app-config.ts index 857ac7cb7..41f383cd2 100644 --- a/packages/effects/hooks/src/use-app-config.ts +++ b/packages/effects/hooks/src/use-app-config.ts @@ -15,9 +15,22 @@ export function useAppConfig( ? window._VBEN_ADMIN_PRO_APP_CONF_ : (env as VbenAdminProAppConfigRaw); - const { VITE_GLOB_API_URL } = config; + const { + VITE_GLOB_API_URL, + VITE_GLOB_AUTH_DINGDING_CORP_ID, + VITE_GLOB_AUTH_DINGDING_CLIENT_ID, + } = config; - return { + const applicationConfig: ApplicationConfig = { apiURL: VITE_GLOB_API_URL, + auth: {}, }; + if (VITE_GLOB_AUTH_DINGDING_CORP_ID && VITE_GLOB_AUTH_DINGDING_CLIENT_ID) { + applicationConfig.auth.dingding = { + clientId: VITE_GLOB_AUTH_DINGDING_CLIENT_ID, + corpId: VITE_GLOB_AUTH_DINGDING_CORP_ID, + }; + } + + return applicationConfig; } diff --git a/packages/icons/src/iconify/index.ts b/packages/icons/src/iconify/index.ts index a0985ac15..bec4d5a50 100644 --- a/packages/icons/src/iconify/index.ts +++ b/packages/icons/src/iconify/index.ts @@ -11,3 +11,5 @@ export const MdiGithub = createIconifyIcon('mdi:github'); export const MdiGoogle = createIconifyIcon('mdi:google'); export const MdiQqchat = createIconifyIcon('mdi:qqchat'); + +export const RiDingding = createIconifyIcon('ri:dingding-fill'); diff --git a/packages/locales/src/langs/en-US/authentication.json b/packages/locales/src/langs/en-US/authentication.json index f294cdd82..ec9b8ca7f 100644 --- a/packages/locales/src/langs/en-US/authentication.json +++ b/packages/locales/src/langs/en-US/authentication.json @@ -36,6 +36,11 @@ "qrcodeSubtitle": "Scan the QR code with your phone to login", "qrcodePrompt": "Click 'Confirm' after scanning to complete login", "qrcodeLogin": "QR Code Login", + "wechatLogin": "Wechat Login", + "qqLogin": "QQ Login", + "githubLogin": "Github Login", + "googleLogin": "Google Login", + "dingdingLogin": "Dingding Login", "codeSubtitle": "Enter your phone number to start managing your project", "code": "Security code", "codeTip": "Security code required {0} characters", diff --git a/packages/locales/src/langs/zh-CN/authentication.json b/packages/locales/src/langs/zh-CN/authentication.json index 147da6322..ee4a2ec85 100644 --- a/packages/locales/src/langs/zh-CN/authentication.json +++ b/packages/locales/src/langs/zh-CN/authentication.json @@ -36,6 +36,11 @@ "qrcodeSubtitle": "请用手机扫描二维码登录", "qrcodePrompt": "扫码后点击 '确认',即可完成登录", "qrcodeLogin": "扫码登录", + "wechatLogin": "微信登录", + "qqLogin": "QQ登录", + "githubLogin": "Github登录", + "googleLogin": "Google登录", + "dingdingLogin": "钉钉登录", "codeSubtitle": "请输入您的手机号码以开始管理您的项目", "code": "验证码", "codeTip": "请输入{0}位验证码", diff --git a/packages/types/global.d.ts b/packages/types/global.d.ts index 0c8f01983..5b3b3e91a 100644 --- a/packages/types/global.d.ts +++ b/packages/types/global.d.ts @@ -9,10 +9,20 @@ declare module 'vue-router' { export interface VbenAdminProAppConfigRaw { VITE_GLOB_API_URL: string; + VITE_GLOB_AUTH_DINGDING_CLIENT_ID: string; + VITE_GLOB_AUTH_DINGDING_CORP_ID: string; +} + +interface AuthConfig { + dingding?: { + clientId: string; + corpId: string; + }; } export interface ApplicationConfig { apiURL: string; + auth: AuthConfig; } declare global { diff --git a/playground/.env.development b/playground/.env.development index dcf361e73..0b1dc0571 100644 --- a/playground/.env.development +++ b/playground/.env.development @@ -14,3 +14,7 @@ VITE_DEVTOOLS=false # 是否注入全局loading VITE_INJECT_APP_LOADING=true + +# 钉钉登录配置 +VITE_GLOB_AUTH_DINGDING_CLIENT_ID=应用的clientId +VITE_GLOB_AUTH_DINGDING_CORP_ID=应用的corpId