Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
#app,
|
||||
body,
|
||||
html {
|
||||
@apply size-full overscroll-none;
|
||||
@apply !pointer-events-auto size-full overscroll-none;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getElementVisibleRect } from '../dom'; // 假设函数位于 utils.ts 中
|
||||
import { getElementVisibleRect } from '../dom';
|
||||
|
||||
describe('getElementVisibleRect', () => {
|
||||
// 设置浏览器视口尺寸的 mock
|
||||
|
@@ -7,7 +7,6 @@ import {
|
||||
toLowerCaseFirstLetter,
|
||||
} from '../letter';
|
||||
|
||||
// 编写测试用例
|
||||
describe('capitalizeFirstLetter', () => {
|
||||
it('should capitalize the first letter of a string', () => {
|
||||
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
||||
|
@@ -13,8 +13,7 @@ describe('uniqueByField', () => {
|
||||
|
||||
const uniqueItems = uniqueByField(items, 'id');
|
||||
|
||||
// Assert expected results
|
||||
expect(uniqueItems).toHaveLength(3); // After deduplication, there should be three objects left
|
||||
expect(uniqueItems).toHaveLength(3);
|
||||
expect(uniqueItems).toEqual([
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
|
3
packages/@core/base/typings/src/app.d.ts
vendored
3
packages/@core/base/typings/src/app.d.ts
vendored
@@ -11,8 +11,9 @@ type ThemeModeType = 'auto' | 'dark' | 'light';
|
||||
* 偏好设置按钮位置
|
||||
* fixed 固定在右侧
|
||||
* header 顶栏
|
||||
* auto 自动
|
||||
*/
|
||||
type PreferencesButtonPositionType = 'fixed' | 'header';
|
||||
type PreferencesButtonPositionType = 'auto' | 'fixed' | 'header';
|
||||
|
||||
type BuiltinThemeType =
|
||||
| 'custom'
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export * from './use-content-style';
|
||||
export * from './use-is-mobile';
|
||||
export * from './use-namespace';
|
||||
export * from './use-priority-value';
|
||||
export * from './use-sortable';
|
||||
|
@@ -28,7 +28,7 @@ function useContentStyle() {
|
||||
position: 'fixed',
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
zIndex: 1000,
|
||||
zIndex: 150,
|
||||
};
|
||||
});
|
||||
|
||||
|
7
packages/@core/composables/src/use-is-mobile.ts
Normal file
7
packages/@core/composables/src/use-is-mobile.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||
|
||||
export function useIsMobile() {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller('md');
|
||||
return { isMobile };
|
||||
}
|
@@ -20,7 +20,7 @@ const defaultPreferences: Preferences = {
|
||||
locale: 'zh-CN',
|
||||
loginExpiredMode: 'page',
|
||||
name: 'Vben Admin',
|
||||
preferencesButtonPosition: 'fixed',
|
||||
preferencesButtonPosition: 'auto',
|
||||
watermark: false,
|
||||
},
|
||||
breadcrumb: {
|
||||
|
@@ -149,6 +149,45 @@ function usePreferences() {
|
||||
return enable && globalLockScreen;
|
||||
});
|
||||
|
||||
/**
|
||||
* @zh_CN 偏好设置按钮位置
|
||||
*/
|
||||
const preferencesButtonPosition = computed(() => {
|
||||
const { enablePreferences, preferencesButtonPosition } = preferences.app;
|
||||
|
||||
// 如果没有启用偏好设置按钮
|
||||
if (!enablePreferences) {
|
||||
return {
|
||||
fixed: false,
|
||||
header: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { header, sidebar } = preferences;
|
||||
const headerHidden = header.hidden;
|
||||
const sidebarHidden = sidebar.hidden;
|
||||
|
||||
const contentIsMaximize = headerHidden && sidebarHidden;
|
||||
|
||||
const isHeaderPosition = preferencesButtonPosition === 'header';
|
||||
|
||||
// 如果设置了固定位置
|
||||
if (preferencesButtonPosition !== 'auto') {
|
||||
return {
|
||||
fixed: preferencesButtonPosition === 'fixed',
|
||||
header: isHeaderPosition,
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是全屏模式或者没有固定在顶部,
|
||||
const fixed = contentIsMaximize || isFullContent.value || isMobile.value;
|
||||
|
||||
return {
|
||||
fixed,
|
||||
header: !fixed,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
authPanelCenter,
|
||||
authPanelLeft,
|
||||
@@ -168,6 +207,7 @@ function usePreferences() {
|
||||
isSideNav,
|
||||
keepAlive,
|
||||
layout,
|
||||
preferencesButtonPosition,
|
||||
sidebarCollapsed,
|
||||
theme,
|
||||
};
|
||||
|
@@ -4,6 +4,8 @@ import { computed, shallowRef, useSlots, watchEffect } from 'vue';
|
||||
|
||||
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useScrollLock } from '@vueuse/core';
|
||||
|
||||
import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
|
||||
|
||||
interface Props {
|
||||
@@ -102,6 +104,7 @@ const expandOnHovering = defineModel<boolean>('expandOnHovering');
|
||||
const expandOnHover = defineModel<boolean>('expandOnHover');
|
||||
const extraVisible = defineModel<boolean>('extraVisible');
|
||||
|
||||
const isLocked = useScrollLock(document.body);
|
||||
const slots = useSlots();
|
||||
|
||||
const asideRef = shallowRef<HTMLDivElement | null>();
|
||||
@@ -214,6 +217,7 @@ function handleMouseenter() {
|
||||
if (!expandOnHovering.value) {
|
||||
collapse.value = false;
|
||||
}
|
||||
isLocked.value = true;
|
||||
expandOnHovering.value = true;
|
||||
}
|
||||
|
||||
@@ -224,6 +228,7 @@ function handleMouseleave() {
|
||||
return;
|
||||
}
|
||||
|
||||
isLocked.value = false;
|
||||
expandOnHovering.value = false;
|
||||
collapse.value = true;
|
||||
extraVisible.value = false;
|
||||
|
@@ -242,7 +242,7 @@ const tabbarStyle = computed((): CSSProperties => {
|
||||
let marginLeft = 0;
|
||||
|
||||
// 如果不是混合导航,tabbar 的宽度为 100%
|
||||
if (!isMixedNav.value) {
|
||||
if (!isMixedNav.value || props.sidebarHidden) {
|
||||
width = '100%';
|
||||
} else if (sidebarEnable.value) {
|
||||
// 鼠标在侧边栏上时,且侧边栏展开时的宽度
|
||||
|
@@ -30,6 +30,8 @@ export class DrawerApi {
|
||||
const defaultState: DrawerState = {
|
||||
cancelText: '取消',
|
||||
closable: true,
|
||||
closeOnClickModal: true,
|
||||
closeOnPressEscape: true,
|
||||
confirmLoading: false,
|
||||
confirmText: '确定',
|
||||
footer: true,
|
||||
|
@@ -7,12 +7,21 @@ export interface DrawerProps {
|
||||
* 取消按钮文字
|
||||
*/
|
||||
cancelText?: string;
|
||||
|
||||
/**
|
||||
* 是否显示右上角的关闭按钮
|
||||
* @default true
|
||||
*/
|
||||
closable?: boolean;
|
||||
/**
|
||||
* 点击弹窗遮罩是否关闭弹窗
|
||||
* @default true
|
||||
*/
|
||||
closeOnClickModal?: boolean;
|
||||
/**
|
||||
* 按下 ESC 键是否关闭弹窗
|
||||
* @default true
|
||||
*/
|
||||
closeOnPressEscape?: boolean;
|
||||
/**
|
||||
* 确定按钮 loading
|
||||
* @default false
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
||||
|
||||
import { usePriorityValue } from '@vben-core/composables';
|
||||
import { useIsMobile, usePriorityValue } from '@vben-core/composables';
|
||||
import { Info, X } from '@vben-core/icons';
|
||||
import {
|
||||
Sheet,
|
||||
@@ -31,6 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
drawerApi: undefined,
|
||||
});
|
||||
|
||||
const { isMobile } = useIsMobile();
|
||||
const state = props.drawerApi?.useStore?.();
|
||||
|
||||
const title = usePriorityValue('title', props, state);
|
||||
@@ -43,6 +44,27 @@ const modal = usePriorityValue('modal', props, state);
|
||||
const confirmLoading = usePriorityValue('confirmLoading', props, state);
|
||||
const cancelText = usePriorityValue('cancelText', props, state);
|
||||
const confirmText = usePriorityValue('confirmText', props, state);
|
||||
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
||||
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
|
||||
|
||||
function interactOutside(e: Event) {
|
||||
if (!closeOnClickModal.value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
function escapeKeyDown(e: KeyboardEvent) {
|
||||
if (!closeOnPressEscape.value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
// pointer-down-outside
|
||||
function pointerDownOutside(e: Event) {
|
||||
const target = e.target as HTMLElement;
|
||||
const isDismissableModal = !!target?.dataset.dismissableModal;
|
||||
if (!closeOnClickModal.value || !isDismissableModal) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Sheet
|
||||
@@ -50,7 +72,16 @@ const confirmText = usePriorityValue('confirmText', props, state);
|
||||
:open="state?.isOpen"
|
||||
@update:open="() => drawerApi?.close()"
|
||||
>
|
||||
<SheetContent :class="cn('flex w-[520px] flex-col', props.class, {})">
|
||||
<SheetContent
|
||||
:class="
|
||||
cn('flex w-[520px] flex-col', props.class, {
|
||||
'!w-full': isMobile,
|
||||
})
|
||||
"
|
||||
@escape-key-down="escapeKeyDown"
|
||||
@interact-outside="interactOutside"
|
||||
@pointer-down-outside="pointerDownOutside"
|
||||
>
|
||||
<SheetHeader
|
||||
:class="
|
||||
cn('!flex flex-row items-center justify-between border-b px-6 py-5', {
|
||||
@@ -59,7 +90,7 @@ const confirmText = usePriorityValue('confirmText', props, state);
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<SheetTitle v-if="title">
|
||||
<SheetTitle v-if="title" class="text-left">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
|
||||
@@ -111,22 +142,17 @@ const confirmText = usePriorityValue('confirmText', props, state);
|
||||
|
||||
<SheetFooter
|
||||
v-if="showFooter"
|
||||
class="w-full items-center border-t p-2 px-3"
|
||||
class="w-full flex-row items-center justify-end border-t p-2 px-3"
|
||||
>
|
||||
<slot name="prepend-footer"></slot>
|
||||
<slot name="footer">
|
||||
<VbenButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click="() => drawerApi?.onCancel()"
|
||||
>
|
||||
<VbenButton variant="ghost" @click="() => drawerApi?.onCancel()">
|
||||
<slot name="cancelText">
|
||||
{{ cancelText }}
|
||||
</slot>
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
:loading="confirmLoading"
|
||||
size="sm"
|
||||
@click="() => drawerApi?.onConfirm()"
|
||||
>
|
||||
<slot name="confirmText">
|
||||
|
@@ -3,7 +3,7 @@ import type { ExtendedModalApi, ModalProps } from './modal';
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { usePriorityValue } from '@vben-core/composables';
|
||||
import { useIsMobile, usePriorityValue } from '@vben-core/composables';
|
||||
import { Expand, Info, Shrink } from '@vben-core/icons';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -46,6 +46,7 @@ const dialogRef = ref();
|
||||
const headerRef = ref();
|
||||
const footerRef = ref();
|
||||
|
||||
const { isMobile } = useIsMobile();
|
||||
// const { height: headerHeight } = useElementSize(headerRef);
|
||||
// const { height: footerHeight } = useElementSize(footerRef);
|
||||
const state = props.modalApi?.useStore?.();
|
||||
@@ -66,7 +67,11 @@ const draggable = usePriorityValue('draggable', props, state);
|
||||
const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
|
||||
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
||||
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
|
||||
const shouldDraggable = computed(() => draggable.value && !fullscreen.value);
|
||||
|
||||
const shouldFullscreen = computed(() => fullscreen.value || isMobile.value);
|
||||
const shouldDraggable = computed(
|
||||
() => draggable.value && !shouldFullscreen.value,
|
||||
);
|
||||
|
||||
const { dragging } = useModalDraggable(dialogRef, headerRef, shouldDraggable);
|
||||
|
||||
@@ -114,6 +119,14 @@ function escapeKeyDown(e: KeyboardEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
// pointer-down-outside
|
||||
function pointerDownOutside(e: Event) {
|
||||
const target = e.target as HTMLElement;
|
||||
const isDismissableModal = !!target?.dataset.dismissableModal;
|
||||
if (!closeOnClickModal.value || !isDismissableModal) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Dialog
|
||||
@@ -133,8 +146,8 @@ function escapeKeyDown(e: KeyboardEvent) {
|
||||
props.class,
|
||||
{
|
||||
'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':
|
||||
fullscreen,
|
||||
'top-1/2 -translate-y-1/2': centered && !fullscreen,
|
||||
shouldFullscreen,
|
||||
'top-1/2 -translate-y-1/2': centered && !shouldFullscreen,
|
||||
'duration-300': !dragging,
|
||||
},
|
||||
)
|
||||
@@ -143,6 +156,7 @@ function escapeKeyDown(e: KeyboardEvent) {
|
||||
close-class="top-4"
|
||||
@escape-key-down="escapeKeyDown"
|
||||
@interact-outside="interactOutside"
|
||||
@pointer-down-outside="pointerDownOutside"
|
||||
>
|
||||
<DialogHeader
|
||||
ref="headerRef"
|
||||
@@ -156,7 +170,7 @@ function escapeKeyDown(e: KeyboardEvent) {
|
||||
)
|
||||
"
|
||||
>
|
||||
<DialogTitle v-if="title">
|
||||
<DialogTitle v-if="title" class="text-left">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
|
||||
@@ -191,7 +205,7 @@ function escapeKeyDown(e: KeyboardEvent) {
|
||||
|
||||
<VbenIconButton
|
||||
v-if="fullscreenButton"
|
||||
class="hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-10 top-4 size-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
||||
class="hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-10 top-4 hidden size-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none sm:block"
|
||||
@click="handleFullscreen"
|
||||
>
|
||||
<Shrink v-if="fullscreen" class="size-3.5" />
|
||||
@@ -201,22 +215,22 @@ function escapeKeyDown(e: KeyboardEvent) {
|
||||
<DialogFooter
|
||||
v-if="showFooter"
|
||||
ref="footerRef"
|
||||
:class="cn('items-center border-t p-2', props.footerClass)"
|
||||
:class="
|
||||
cn(
|
||||
'flex-row items-center justify-end border-t p-2',
|
||||
props.footerClass,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot name="prepend-footer"></slot>
|
||||
<slot name="footer">
|
||||
<VbenButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
@click="() => modalApi?.onCancel()"
|
||||
>
|
||||
<VbenButton variant="ghost" @click="() => modalApi?.onCancel()">
|
||||
<slot name="cancelText">
|
||||
{{ cancelText }}
|
||||
</slot>
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
:loading="confirmLoading"
|
||||
size="sm"
|
||||
@click="() => modalApi?.onConfirm()"
|
||||
>
|
||||
<slot name="confirmText">
|
||||
|
@@ -94,7 +94,7 @@ async function checkProps(api: ExtendedModalApi, attrs: Record<string, any>) {
|
||||
if (stateKeys.has(attr)) {
|
||||
// connectedComponent存在时,不要传入Modal的props,会造成复杂度提升,如果你需要修改Modal的props,请使用 useModal 或者api
|
||||
console.warn(
|
||||
`[Vben Modal]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Modal, please use useModal or api.`,
|
||||
`[Vben Modal]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Modal, please use useVbenModal or api.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -45,6 +45,7 @@ defineExpose({
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-overlay fixed inset-0 z-[1000] backdrop-blur-sm"
|
||||
data-dismissable-modal="true"
|
||||
@click="() => emits('close')"
|
||||
/>
|
||||
<DialogContent
|
||||
|
@@ -40,6 +40,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[1000]"
|
||||
data-dismissable-modal="true"
|
||||
/>
|
||||
<DialogContent
|
||||
:class="cn(sheetVariants({ side }), 'z-[1000]', props.class)"
|
||||
|
@@ -73,6 +73,7 @@ export function useTabsViewScroll(props: TabsProps) {
|
||||
resizeObserver = new ResizeObserver(
|
||||
useDebounceFn((_entries: ResizeObserverEntry[]) => {
|
||||
calcShowScrollbarButton();
|
||||
scrollToActiveIntoView();
|
||||
}, 100),
|
||||
);
|
||||
resizeObserver.observe(viewportEl);
|
||||
|
@@ -1 +0,0 @@
|
||||
export * from './echarts';
|
@@ -28,7 +28,7 @@ withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const { globalSearchShortcutKey } = usePreferences();
|
||||
const { globalSearchShortcutKey, preferencesButtonPosition } = usePreferences();
|
||||
const slots = useSlots();
|
||||
const rightSlots = computed(() => {
|
||||
const list = [{ index: 100, name: 'user-dropdown' }];
|
||||
@@ -39,10 +39,7 @@ const rightSlots = computed(() => {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
preferences.app.enablePreferences &&
|
||||
preferences.app.preferencesButtonPosition === 'header'
|
||||
) {
|
||||
if (preferencesButtonPosition.value.header) {
|
||||
list.push({
|
||||
index: 10,
|
||||
name: 'preferences',
|
||||
@@ -121,7 +118,7 @@ const leftSlots = computed(() => {
|
||||
<GlobalSearch
|
||||
:enable-shortcut-key="globalSearchShortcutKey"
|
||||
:menus="accessStore.accessMenus"
|
||||
class="mr-4"
|
||||
class="mr-1 sm:mr-4"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@@ -40,6 +40,7 @@ const {
|
||||
isMobile,
|
||||
isSideMixedNav,
|
||||
layout,
|
||||
preferencesButtonPosition,
|
||||
sidebarCollapsed,
|
||||
theme,
|
||||
} = usePreferences();
|
||||
@@ -326,12 +327,7 @@ const headerSlots = computed(() => {
|
||||
<slot v-if="lockStore.isLockScreen" name="lock-screen"></slot>
|
||||
</Transition>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
preferences.app.enablePreferences &&
|
||||
preferences.app.preferencesButtonPosition === 'fixed'
|
||||
"
|
||||
>
|
||||
<template v-if="preferencesButtonPosition.fixed">
|
||||
<Preferences
|
||||
class="z-100 fixed bottom-20 right-0"
|
||||
@clear-preferences-and-logout="clearPreferencesAndLogout"
|
||||
|
@@ -127,7 +127,7 @@ onMounted(() => {
|
||||
@click="toggleOpen()"
|
||||
>
|
||||
<Search
|
||||
class="text-muted-foreground group-hover:text-foreground size-3 group-hover:opacity-100"
|
||||
class="text-muted-foreground group-hover:text-foreground size-4 group-hover:opacity-100"
|
||||
/>
|
||||
<span
|
||||
class="text-muted-foreground group-hover:text-foreground hidden text-xs duration-300 md:block"
|
||||
|
@@ -159,7 +159,7 @@ function toggleUnlockForm() {
|
||||
</transition>
|
||||
|
||||
<div
|
||||
class="enter-y absolute bottom-5 w-full text-center text-gray-300 xl:text-xl 2xl:text-3xl"
|
||||
class="enter-y absolute bottom-5 w-full text-center xl:text-xl 2xl:text-3xl"
|
||||
>
|
||||
<div v-if="showUnlockForm" class="enter-x mb-2 text-3xl">
|
||||
{{ hour }}:{{ minute }} <span class="text-lg">{{ meridiem }}</span>
|
||||
|
@@ -24,6 +24,10 @@ const appPreferencesButtonPosition = defineModel<string>(
|
||||
);
|
||||
|
||||
const positionItems = computed((): SelectOption[] => [
|
||||
{
|
||||
label: $t('preferences.position.auto'),
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
label: $t('preferences.position.header'),
|
||||
value: 'header',
|
||||
|
@@ -55,7 +55,7 @@ const listen = computed(() => {
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<Drawer v-bind="attrs" v-on="listen" />
|
||||
<Drawer v-bind="{ ...$attrs, ...attrs }" v-on="listen" />
|
||||
|
||||
<div @click="() => drawerApi.open()">
|
||||
<slot>
|
||||
|
28
packages/effects/plugins/README.md
Normal file
28
packages/effects/plugins/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# @vben/plugins
|
||||
|
||||
该目录用于存放项目中集成的第三方库及其相关插件。每个插件都包含了可重用的逻辑、配置和组件,方便在项目中进行统一管理和调用。
|
||||
|
||||
## 注意
|
||||
|
||||
所有的第三方插件都必须以 `subpath` 形式引入,例:
|
||||
|
||||
以 `echarts` 为例,引入方式如下:
|
||||
|
||||
**packages.json**
|
||||
|
||||
```json
|
||||
"exports": {
|
||||
"./echarts": {
|
||||
"types": "./src/echarts/index.ts",
|
||||
"default": "./src/echarts/index.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用方式**
|
||||
|
||||
```ts
|
||||
import { useEcharts } from '@vben/plugins/echarts';
|
||||
```
|
||||
|
||||
这样做的好处是,应用可以自行选择是否使用插件,而不会因为插件的引入及副作用而导致打包体积增大,只引入需要的插件即可。
|
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vben/chart-ui",
|
||||
"name": "@vben/plugins",
|
||||
"version": "5.1.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/effects/chart-ui"
|
||||
"directory": "packages/effects/plugins"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -14,9 +14,9 @@
|
||||
"**/*.css"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
"./echarts": {
|
||||
"types": "./src/echarts/index.ts",
|
||||
"default": "./src/echarts/index.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
@@ -5,11 +5,12 @@ import type EchartsUI from './echarts-ui.vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { computed, nextTick, watch } from 'vue';
|
||||
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
|
||||
import {
|
||||
tryOnUnmounted,
|
||||
useDebounceFn,
|
||||
useResizeObserver,
|
||||
useTimeoutFn,
|
||||
useWindowSize,
|
||||
} from '@vueuse/core';
|
||||
@@ -86,6 +87,8 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
|
||||
resizeHandler?.();
|
||||
});
|
||||
|
||||
useResizeObserver(chartRef as never, resizeHandler);
|
||||
|
||||
watch(isDark, () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
@@ -95,21 +98,6 @@ function useEcharts(chartRef: Ref<EchartsUIType>) {
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
() => preferences.sidebar.collapsed,
|
||||
() => preferences.sidebar.extraCollapse,
|
||||
() => preferences.sidebar.hidden,
|
||||
() => preferences.app.contentCompact,
|
||||
],
|
||||
() => {
|
||||
// 折叠动画200ms
|
||||
setTimeout(() => {
|
||||
resize();
|
||||
}, 200);
|
||||
},
|
||||
);
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
// 销毁实例,释放资源
|
||||
chartInstance?.dispose();
|
@@ -182,6 +182,7 @@
|
||||
"position": {
|
||||
"title": "Preferences Postion",
|
||||
"header": "Header",
|
||||
"auto": "Auto",
|
||||
"fixed": "Fixed"
|
||||
},
|
||||
"sidebar": {
|
||||
|
@@ -182,6 +182,7 @@
|
||||
"position": {
|
||||
"title": "偏好设置位置",
|
||||
"header": "顶栏",
|
||||
"auto": "自动",
|
||||
"fixed": "固定"
|
||||
},
|
||||
"sidebar": {
|
||||
|
6
packages/utils/src/helpers/get-popup-container.ts
Normal file
6
packages/utils/src/helpers/get-popup-container.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Returns the parent node of the given element or the document body if the element is not provided.it
|
||||
*/
|
||||
export function getPopupContainer(node?: HTMLElement): HTMLElement {
|
||||
return (node?.parentNode as HTMLElement) ?? document.body;
|
||||
}
|
@@ -2,6 +2,7 @@ export * from './find-menu-by-path';
|
||||
export * from './generate-menus';
|
||||
export * from './generate-routes-backend';
|
||||
export * from './generate-routes-frontend';
|
||||
export * from './get-popup-container';
|
||||
export * from './merge-route-modules';
|
||||
export * from './reset-routes';
|
||||
export * from './unmount-global-loading';
|
||||
|
Reference in New Issue
Block a user