This commit is contained in:
dap
2024-08-27 08:14:19 +08:00
72 changed files with 374 additions and 138 deletions

View File

@@ -28,7 +28,7 @@
#app,
body,
html {
@apply size-full overscroll-none;
@apply !pointer-events-auto size-full overscroll-none;
}
body {

View File

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

View File

@@ -7,7 +7,6 @@ import {
toLowerCaseFirstLetter,
} from '../letter';
// 编写测试用例
describe('capitalizeFirstLetter', () => {
it('should capitalize the first letter of a string', () => {
expect(capitalizeFirstLetter('hello')).toBe('Hello');

View File

@@ -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' },

View File

@@ -11,8 +11,9 @@ type ThemeModeType = 'auto' | 'dark' | 'light';
* 偏好设置按钮位置
* fixed 固定在右侧
* header 顶栏
* auto 自动
*/
type PreferencesButtonPositionType = 'fixed' | 'header';
type PreferencesButtonPositionType = 'auto' | 'fixed' | 'header';
type BuiltinThemeType =
| 'custom'

View File

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

View File

@@ -28,7 +28,7 @@ function useContentStyle() {
position: 'fixed',
top: `${top}px`,
width: `${width}px`,
zIndex: 1000,
zIndex: 150,
};
});

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

View File

@@ -20,7 +20,7 @@ const defaultPreferences: Preferences = {
locale: 'zh-CN',
loginExpiredMode: 'page',
name: 'Vben Admin',
preferencesButtonPosition: 'fixed',
preferencesButtonPosition: 'auto',
watermark: false,
},
breadcrumb: {

View File

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

View File

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

View File

@@ -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) {
// 鼠标在侧边栏上时,且侧边栏展开时的宽度

View File

@@ -30,6 +30,8 @@ export class DrawerApi {
const defaultState: DrawerState = {
cancelText: '取消',
closable: true,
closeOnClickModal: true,
closeOnPressEscape: true,
confirmLoading: false,
confirmText: '确定',
footer: true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,6 +73,7 @@ export function useTabsViewScroll(props: TabsProps) {
resizeObserver = new ResizeObserver(
useDebounceFn((_entries: ResizeObserverEntry[]) => {
calcShowScrollbarButton();
scrollToActiveIntoView();
}, 100),
);
resizeObserver.observe(viewportEl);