This commit is contained in:
xingyu4j
2025-07-29 15:21:32 +08:00
60 changed files with 399 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -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);
});
});

View File

@@ -8,6 +8,7 @@ export * from './inference';
export * from './letter';
export * from './merge';
export * from './nprogress';
export * from './resources';
export * from './state-handler';
export * from './time';
export * from './to';

View File

@@ -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 };

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/form-ui",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -59,6 +59,7 @@ export class ModalApi {
showCancelButton: true,
showConfirmButton: true,
title: '',
animationType: 'slide',
};
this.store = new Store<ModalState>(

View File

@@ -5,6 +5,11 @@ import type { MaybePromise } from '@vben-core/typings';
import type { ModalApi } from './modal-api';
export interface ModalProps {
/**
* 动画类型
* @default 'slide'
*/
animationType?: 'scale' | 'slide';
/**
* 是否要挂载到内容区域
* @default false

View File

@@ -94,12 +94,11 @@ const {
submitting,
title,
titleTooltip,
animationType,
zIndex,
} = usePriorityValues(props, state);
const shouldFullscreen = computed(
() => (fullscreen.value && header.value) || isMobile.value,
);
const shouldFullscreen = computed(() => fullscreen.value || isMobile.value);
const shouldDraggable = computed(
() => draggable.value && !shouldFullscreen.value && header.value,
@@ -244,6 +243,7 @@ function handleClosed() {
:modal="modal"
:open="state?.isOpen"
:show-close="closable"
:animation-type="animationType"
:z-index="zIndex"
:overlay-blur="overlayBlur"
close-class="top-3"

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.5.7",
"version": "5.5.8",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"homepage": "https://github.com/vbenjs/vue-vben-admin",

View File

@@ -20,6 +20,7 @@ import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
DialogContentProps & {
animationType?: 'scale' | 'slide';
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
@@ -31,7 +32,12 @@ const props = withDefaults(
zIndex?: number;
}
>(),
{ appendTo: 'body', closeDisabled: false, showClose: true },
{
appendTo: 'body',
animationType: 'slide',
closeDisabled: false,
showClose: true,
},
);
const emits = defineEmits<
DialogContentEmits & { close: []; closed: []; opened: [] }
@@ -43,6 +49,7 @@ const delegatedProps = computed(() => {
modal: _modal,
open: _open,
showClose: __,
animationType: ___,
...delegated
} = props;
@@ -100,7 +107,11 @@ defineExpose({
v-bind="forwarded"
:class="
cn(
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
'z-popup bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 w-full p-6 shadow-lg outline-none sm:rounded-xl',
{
'data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%]':
animationType === 'slide',
},
props.class,
)
"

View File

@@ -1,6 +1,6 @@
{
"name": "@vben-core/tabs-ui",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/constants",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/access",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/common-ui",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { AntdDingTalk, MdiGithub, MdiQqchat, MdiWechat } from '@vben/icons';
import { MdiGithub, MdiQqchat, MdiWechat, RiDingding } from '@vben/icons';
import { $t } from '@vben/locales';
import { VbenIconButton } from '@vben-core/shadcn-ui';
@@ -37,7 +37,7 @@ function handleThirdLogin(type: number) {
<MdiWechat />
</VbenIconButton>
<VbenIconButton class="mb-3" @click="handleThirdLogin(20)">
<AntdDingTalk />
<RiDingding />
</VbenIconButton>
<VbenIconButton class="mb-3" @click="handleThirdLogin(0)">
<MdiQqchat />

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/hooks",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -15,11 +15,24 @@ 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;
}
export function isTenantEnable(): boolean {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/layouts",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,9 +1,11 @@
<script lang="ts" setup>
import type { SetupContext } from 'vue';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import type { MenuRecordRaw } from '@vben/types';
import { computed, useSlots, watch } from 'vue';
import { computed, onMounted, useSlots, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useRefresh } from '@vben/hooks';
import { $t, i18n } from '@vben/locales';
@@ -153,6 +155,23 @@ function clickLogo() {
emit('clickLogo');
}
function autoCollapseMenuByRouteMeta(route: RouteLocationNormalizedLoaded) {
// 只在双列模式下生效
if (
preferences.app.layout === 'sidebar-mixed-nav' &&
route.meta &&
route.meta.hideInMenu
) {
sidebarExtraVisible.value = false;
}
}
const route = useRoute();
onMounted(() => {
autoCollapseMenuByRouteMeta(route);
});
watch(
() => preferences.app.layout,
async (val) => {

View File

@@ -32,6 +32,10 @@ function useNavigation() {
return route?.meta?.openInNewWindow ?? false;
};
const resolveHref = (path: string): string => {
return router.resolve(path).href;
};
const navigation = async (path: string) => {
try {
const route = routeMetaMap.get(path);
@@ -40,7 +44,7 @@ function useNavigation() {
if (isHttpUrl(path)) {
openWindow(path, { target: '_blank' });
} else if (openInNewWindow) {
openRouteInNewWindow(path);
openRouteInNewWindow(resolveHref(path));
} else {
await router.push({
path,

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/plugins",
"version": "5.5.6",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/request",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/icons",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -20,8 +20,7 @@ export const MsRefresh = createIconifyIcon('material-symbols:refresh-rounded');
export const TMinimize = createIconifyIcon('tabler:arrows-minimize');
export const AntdDingTalk = createIconifyIcon('ant-design:dingtalk');
export const AntdProfileOutlined = createIconifyIcon(
'ant-design:profile-outlined',
);
export const RiDingding = createIconifyIcon('ri:dingding-fill');

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/locales",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -41,6 +41,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",

View File

@@ -41,6 +41,11 @@
"qrcodeSubtitle": "请用手机扫描二维码登录",
"qrcodePrompt": "扫码后点击 '确认',即可完成登录",
"qrcodeLogin": "扫码登录",
"wechatLogin": "微信登录",
"qqLogin": "QQ登录",
"githubLogin": "Github登录",
"googleLogin": "Google登录",
"dingdingLogin": "钉钉登录",
"codeSubtitle": "请输入您的手机号码以开始管理您的项目",
"code": "验证码",
"codeTip": "请输入{0}位验证码",

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/preferences",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/stores",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/styles",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -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 {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/types",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/utils",
"version": "5.5.7",
"version": "5.5.8",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {