mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 16:46:19 +08:00
perf: improve modal and drawer component documentation and fix known problems (#4264)
* feat: improve modal and drawer component documentation and fix known problems * chore: update ci
This commit is contained in:
@@ -38,7 +38,6 @@ export class DrawerApi {
|
||||
isOpen: false,
|
||||
loading: false,
|
||||
modal: true,
|
||||
sharedData: {},
|
||||
title: '',
|
||||
};
|
||||
|
||||
@@ -93,7 +92,11 @@ export class DrawerApi {
|
||||
* 取消操作
|
||||
*/
|
||||
onCancel() {
|
||||
this.api.onCancel?.();
|
||||
if (this.api.onCancel) {
|
||||
this.api.onCancel?.();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useIsMobile, usePriorityValue } from '@vben-core/composables';
|
||||
import { Info, X } from '@vben-core/icons';
|
||||
import {
|
||||
@@ -31,6 +33,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
drawerApi: undefined,
|
||||
});
|
||||
|
||||
const wrapperRef = ref<HTMLElement>();
|
||||
|
||||
const { isMobile } = useIsMobile();
|
||||
const state = props.drawerApi?.useStore?.();
|
||||
|
||||
@@ -47,6 +51,18 @@ const confirmText = usePriorityValue('confirmText', props, state);
|
||||
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
||||
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
|
||||
|
||||
watch(
|
||||
() => showLoading.value,
|
||||
(v) => {
|
||||
if (v && wrapperRef.value) {
|
||||
wrapperRef.value.scrollTo({
|
||||
// behavior: 'smooth',
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function interactOutside(e: Event) {
|
||||
if (!closeOnClickModal.value) {
|
||||
e.preventDefault();
|
||||
@@ -129,9 +145,10 @@ function pointerDownOutside(e: Event) {
|
||||
</SheetHeader>
|
||||
|
||||
<div
|
||||
ref="wrapperRef"
|
||||
:class="
|
||||
cn('relative flex-1 p-3', contentClass, {
|
||||
'overflow-y-auto': !showLoading,
|
||||
cn('relative flex-1 overflow-y-auto p-3', contentClass, {
|
||||
'overflow-hidden': showLoading,
|
||||
})
|
||||
"
|
||||
>
|
||||
|
@@ -38,10 +38,10 @@ export class ModalApi {
|
||||
footer: true,
|
||||
fullscreen: false,
|
||||
fullscreenButton: true,
|
||||
header: true,
|
||||
isOpen: false,
|
||||
loading: false,
|
||||
modal: true,
|
||||
sharedData: {},
|
||||
title: '',
|
||||
};
|
||||
|
||||
|
@@ -60,12 +60,16 @@ export interface ModalProps {
|
||||
* @default true
|
||||
*/
|
||||
fullscreenButton?: boolean;
|
||||
/**
|
||||
* 是否显示顶栏
|
||||
* @default true
|
||||
*/
|
||||
header?: boolean;
|
||||
/**
|
||||
* 弹窗是否显示
|
||||
* @default false
|
||||
*/
|
||||
loading?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示遮罩
|
||||
* @default true
|
||||
|
@@ -12,7 +12,6 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
VbenButton,
|
||||
VbenIconButton,
|
||||
VbenLoading,
|
||||
@@ -21,8 +20,6 @@ import {
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { cn } from '@vben-core/shared';
|
||||
|
||||
// import { useElementSize } from '@vueuse/core';
|
||||
|
||||
import { useModalDraggable } from './use-modal-draggable';
|
||||
|
||||
interface Props extends ModalProps {
|
||||
@@ -42,15 +39,15 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
|
||||
const contentRef = ref();
|
||||
const wrapperRef = ref<HTMLElement>();
|
||||
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?.();
|
||||
|
||||
const header = usePriorityValue('header', props, state);
|
||||
const title = usePriorityValue('title', props, state);
|
||||
const fullscreen = usePriorityValue('fullscreen', props, state);
|
||||
const description = usePriorityValue('description', props, state);
|
||||
@@ -68,9 +65,12 @@ const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
|
||||
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
||||
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
|
||||
|
||||
const shouldFullscreen = computed(() => fullscreen.value || isMobile.value);
|
||||
const shouldFullscreen = computed(
|
||||
() => (fullscreen.value && header.value) || isMobile.value,
|
||||
);
|
||||
|
||||
const shouldDraggable = computed(
|
||||
() => draggable.value && !shouldFullscreen.value,
|
||||
() => draggable.value && !shouldFullscreen.value && header.value,
|
||||
);
|
||||
|
||||
const { dragging, transform } = useModalDraggable(
|
||||
@@ -79,32 +79,29 @@ const { dragging, transform } = useModalDraggable(
|
||||
shouldDraggable,
|
||||
);
|
||||
|
||||
// const loadingStyle = computed(() => {
|
||||
// // py-5 4px*5*2
|
||||
// const headerPadding = 40;
|
||||
// // p-2 4px*2*2
|
||||
// const footerPadding = 16;
|
||||
|
||||
// return {
|
||||
// bottom: `${footerHeight.value + footerPadding}px`,
|
||||
// height: `calc(100% - ${footerHeight.value + headerHeight.value + headerPadding + footerPadding}px)`,
|
||||
// top: `${headerHeight.value + headerPadding}px`,
|
||||
// };
|
||||
// });
|
||||
|
||||
watch(
|
||||
() => state?.value?.isOpen,
|
||||
async (v) => {
|
||||
if (v) {
|
||||
await nextTick();
|
||||
if (contentRef.value) {
|
||||
const innerContentRef = contentRef.value.getContentRef();
|
||||
dialogRef.value = innerContentRef.$el;
|
||||
if (!contentRef.value) return;
|
||||
const innerContentRef = contentRef.value.getContentRef();
|
||||
dialogRef.value = innerContentRef.$el;
|
||||
// reopen modal reassign value
|
||||
const { offsetX, offsetY } = transform;
|
||||
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// reopen modal reassign value
|
||||
const { offsetX, offsetY } = transform;
|
||||
dialogRef.value.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
|
||||
}
|
||||
watch(
|
||||
() => showLoading.value,
|
||||
(v) => {
|
||||
if (v && wrapperRef.value) {
|
||||
wrapperRef.value.scrollTo({
|
||||
// behavior: 'smooth',
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -142,10 +139,6 @@ function pointerDownOutside(e: Event) {
|
||||
:open="state?.isOpen"
|
||||
@update:open="() => modalApi?.close()"
|
||||
>
|
||||
<DialogTrigger v-if="$slots.trigger" as-child>
|
||||
<slot name="trigger"> </slot>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent
|
||||
ref="contentRef"
|
||||
:class="
|
||||
@@ -170,8 +163,9 @@ function pointerDownOutside(e: Event) {
|
||||
ref="headerRef"
|
||||
:class="
|
||||
cn(
|
||||
'border-b px-6 py-5',
|
||||
'border-b px-5 py-4',
|
||||
{
|
||||
hidden: !header,
|
||||
'cursor-move select-none': shouldDraggable,
|
||||
},
|
||||
props.headerClass,
|
||||
@@ -182,12 +176,14 @@ function pointerDownOutside(e: Event) {
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
|
||||
<VbenTooltip v-if="titleTooltip" side="right">
|
||||
<template #trigger>
|
||||
<Info class="inline-flex size-5 cursor-pointer pb-1" />
|
||||
</template>
|
||||
{{ titleTooltip }}
|
||||
</VbenTooltip>
|
||||
<slot v-if="titleTooltip" name="titleTooltip">
|
||||
<VbenTooltip side="right">
|
||||
<template #trigger>
|
||||
<Info class="inline-flex size-5 cursor-pointer pb-1" />
|
||||
</template>
|
||||
{{ titleTooltip }}
|
||||
</VbenTooltip>
|
||||
</slot>
|
||||
</slot>
|
||||
</DialogTitle>
|
||||
<DialogDescription v-if="description">
|
||||
@@ -201,13 +197,18 @@ function pointerDownOutside(e: Event) {
|
||||
</VisuallyHidden>
|
||||
</DialogHeader>
|
||||
<div
|
||||
ref="wrapperRef"
|
||||
:class="
|
||||
cn('relative min-h-40 flex-1 p-3', contentClass, {
|
||||
'overflow-y-auto': !showLoading,
|
||||
cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {
|
||||
'overflow-hidden': showLoading,
|
||||
})
|
||||
"
|
||||
>
|
||||
<VbenLoading v-if="showLoading" class="size-full" spinning />
|
||||
<VbenLoading
|
||||
v-if="showLoading"
|
||||
class="size-full h-auto min-h-full"
|
||||
spinning
|
||||
/>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
|
@@ -20,9 +20,6 @@ export function useModalDraggable(
|
||||
|
||||
const dragging = ref(false);
|
||||
|
||||
// let isFirstDrag = true;
|
||||
// let initialX = 0;
|
||||
// let initialY = 0;
|
||||
const onMousedown = (e: MouseEvent) => {
|
||||
const downX = e.clientX;
|
||||
const downY = e.clientY;
|
||||
@@ -31,12 +28,6 @@ export function useModalDraggable(
|
||||
return;
|
||||
}
|
||||
|
||||
// if (isFirstDrag) {
|
||||
// const { x, y } = getInitialTransform(targetRef.value);
|
||||
// initialX = x;
|
||||
// initialY = y;
|
||||
// }
|
||||
|
||||
const targetRect = targetRef.value.getBoundingClientRect();
|
||||
|
||||
const { offsetX, offsetY } = transform;
|
||||
@@ -56,12 +47,9 @@ export function useModalDraggable(
|
||||
const onMousemove = (e: MouseEvent) => {
|
||||
let moveX = offsetX + e.clientX - downX;
|
||||
let moveY = offsetY + e.clientY - downY;
|
||||
// const x = isFirstDrag ? initialX : 0;
|
||||
// const y = isFirstDrag ? initialY : 0;
|
||||
|
||||
moveX = Math.min(Math.max(moveX, minLeft), maxLeft);
|
||||
// + x;
|
||||
moveY = Math.min(Math.max(moveY, minTop), maxTop);
|
||||
// + y;
|
||||
|
||||
transform.offsetX = moveX;
|
||||
transform.offsetY = moveY;
|
||||
@@ -73,7 +61,6 @@ export function useModalDraggable(
|
||||
};
|
||||
|
||||
const onMouseup = () => {
|
||||
// isFirstDrag = false;
|
||||
dragging.value = false;
|
||||
document.removeEventListener('mousemove', onMousemove);
|
||||
document.removeEventListener('mouseup', onMouseup);
|
||||
@@ -127,20 +114,3 @@ export function useModalDraggable(
|
||||
transform,
|
||||
};
|
||||
}
|
||||
|
||||
// function getInitialTransform(target: HTMLElement) {
|
||||
// let x = 0;
|
||||
// let y = 0;
|
||||
// const transformValue = window.getComputedStyle(target)?.transform;
|
||||
// if (transformValue) {
|
||||
// const match = transformValue.match(/matrix\(([^)]+)\)/);
|
||||
// if (match) {
|
||||
// const values = match[1]?.split(', ') ?? [];
|
||||
// // 获取 translateX 值
|
||||
// x = Number.parseFloat(`${values[4]}`);
|
||||
// // 获取 translateY 值
|
||||
// y = Number.parseFloat(`${values[5]}`);
|
||||
// }
|
||||
// }
|
||||
// return { x, y };
|
||||
// }
|
||||
|
@@ -69,7 +69,7 @@ function onTransitionEnd() {
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'bg-overlay z-100 pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center backdrop-blur-sm transition-all duration-500',
|
||||
'bg-overlay z-100 pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center transition-all duration-500',
|
||||
{
|
||||
'invisible opacity-0': !showSpinner,
|
||||
},
|
||||
|
@@ -44,7 +44,7 @@ defineExpose({
|
||||
<template>
|
||||
<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"
|
||||
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]"
|
||||
data-dismissable-modal="true"
|
||||
@click="() => emits('close')"
|
||||
/>
|
||||
|
Reference in New Issue
Block a user