mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-01-23 01:30:26 +08:00
fix: improve the display of modal and drawer on mobile (#4237)
This commit is contained in:
parent
577cc85851
commit
fd7b3479b4
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -194,6 +194,5 @@
|
|||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"commentTranslate.multiLineMerge": true,
|
"commentTranslate.multiLineMerge": true,
|
||||||
"vue.server.hybridMode": true,
|
"vue.server.hybridMode": true,
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
"vitest.disableWorkspaceWarning": true
|
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
#app,
|
#app,
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
@apply size-full overscroll-none;
|
@apply !pointer-events-auto size-full overscroll-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { getElementVisibleRect } from '../dom'; // 假设函数位于 utils.ts 中
|
import { getElementVisibleRect } from '../dom';
|
||||||
|
|
||||||
describe('getElementVisibleRect', () => {
|
describe('getElementVisibleRect', () => {
|
||||||
// 设置浏览器视口尺寸的 mock
|
// 设置浏览器视口尺寸的 mock
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
toLowerCaseFirstLetter,
|
toLowerCaseFirstLetter,
|
||||||
} from '../letter';
|
} from '../letter';
|
||||||
|
|
||||||
// 编写测试用例
|
|
||||||
describe('capitalizeFirstLetter', () => {
|
describe('capitalizeFirstLetter', () => {
|
||||||
it('should capitalize the first letter of a string', () => {
|
it('should capitalize the first letter of a string', () => {
|
||||||
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
||||||
|
@ -13,8 +13,7 @@ describe('uniqueByField', () => {
|
|||||||
|
|
||||||
const uniqueItems = uniqueByField(items, 'id');
|
const uniqueItems = uniqueByField(items, 'id');
|
||||||
|
|
||||||
// Assert expected results
|
expect(uniqueItems).toHaveLength(3);
|
||||||
expect(uniqueItems).toHaveLength(3); // After deduplication, there should be three objects left
|
|
||||||
expect(uniqueItems).toEqual([
|
expect(uniqueItems).toEqual([
|
||||||
{ id: 1, name: 'Item 1' },
|
{ id: 1, name: 'Item 1' },
|
||||||
{ id: 2, name: 'Item 2' },
|
{ id: 2, name: 'Item 2' },
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from './use-content-style';
|
export * from './use-content-style';
|
||||||
|
export * from './use-is-mobile';
|
||||||
export * from './use-namespace';
|
export * from './use-namespace';
|
||||||
export * from './use-priority-value';
|
export * from './use-priority-value';
|
||||||
export * from './use-sortable';
|
export * from './use-sortable';
|
||||||
|
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 };
|
||||||
|
}
|
@ -30,6 +30,8 @@ export class DrawerApi {
|
|||||||
const defaultState: DrawerState = {
|
const defaultState: DrawerState = {
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
closable: true,
|
closable: true,
|
||||||
|
closeOnClickModal: true,
|
||||||
|
closeOnPressEscape: true,
|
||||||
confirmLoading: false,
|
confirmLoading: false,
|
||||||
confirmText: '确定',
|
confirmText: '确定',
|
||||||
footer: true,
|
footer: true,
|
||||||
|
@ -7,12 +7,21 @@ export interface DrawerProps {
|
|||||||
* 取消按钮文字
|
* 取消按钮文字
|
||||||
*/
|
*/
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示右上角的关闭按钮
|
* 是否显示右上角的关闭按钮
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
closable?: boolean;
|
closable?: boolean;
|
||||||
|
/**
|
||||||
|
* 点击弹窗遮罩是否关闭弹窗
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
closeOnClickModal?: boolean;
|
||||||
|
/**
|
||||||
|
* 按下 ESC 键是否关闭弹窗
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
closeOnPressEscape?: boolean;
|
||||||
/**
|
/**
|
||||||
* 确定按钮 loading
|
* 确定按钮 loading
|
||||||
* @default false
|
* @default false
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
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 { Info, X } from '@vben-core/icons';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
@ -31,6 +31,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
drawerApi: undefined,
|
drawerApi: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isMobile } = useIsMobile();
|
||||||
const state = props.drawerApi?.useStore?.();
|
const state = props.drawerApi?.useStore?.();
|
||||||
|
|
||||||
const title = usePriorityValue('title', props, state);
|
const title = usePriorityValue('title', props, state);
|
||||||
@ -43,6 +44,27 @@ const modal = usePriorityValue('modal', props, state);
|
|||||||
const confirmLoading = usePriorityValue('confirmLoading', props, state);
|
const confirmLoading = usePriorityValue('confirmLoading', props, state);
|
||||||
const cancelText = usePriorityValue('cancelText', props, state);
|
const cancelText = usePriorityValue('cancelText', props, state);
|
||||||
const confirmText = usePriorityValue('confirmText', 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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Sheet
|
<Sheet
|
||||||
@ -50,7 +72,16 @@ const confirmText = usePriorityValue('confirmText', props, state);
|
|||||||
:open="state?.isOpen"
|
:open="state?.isOpen"
|
||||||
@update:open="() => drawerApi?.close()"
|
@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
|
<SheetHeader
|
||||||
:class="
|
:class="
|
||||||
cn('!flex flex-row items-center justify-between border-b px-6 py-5', {
|
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>
|
<div>
|
||||||
<SheetTitle v-if="title">
|
<SheetTitle v-if="title" class="text-left">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
||||||
@ -111,22 +142,17 @@ const confirmText = usePriorityValue('confirmText', props, state);
|
|||||||
|
|
||||||
<SheetFooter
|
<SheetFooter
|
||||||
v-if="showFooter"
|
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="prepend-footer"></slot>
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
<VbenButton
|
<VbenButton variant="ghost" @click="() => drawerApi?.onCancel()">
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
@click="() => drawerApi?.onCancel()"
|
|
||||||
>
|
|
||||||
<slot name="cancelText">
|
<slot name="cancelText">
|
||||||
{{ cancelText }}
|
{{ cancelText }}
|
||||||
</slot>
|
</slot>
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
<VbenButton
|
<VbenButton
|
||||||
:loading="confirmLoading"
|
:loading="confirmLoading"
|
||||||
size="sm"
|
|
||||||
@click="() => drawerApi?.onConfirm()"
|
@click="() => drawerApi?.onConfirm()"
|
||||||
>
|
>
|
||||||
<slot name="confirmText">
|
<slot name="confirmText">
|
||||||
|
@ -3,7 +3,7 @@ import type { ExtendedModalApi, ModalProps } from './modal';
|
|||||||
|
|
||||||
import { computed, nextTick, ref, watch } from 'vue';
|
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 { Expand, Info, Shrink } from '@vben-core/icons';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -46,6 +46,7 @@ const dialogRef = ref();
|
|||||||
const headerRef = ref();
|
const headerRef = ref();
|
||||||
const footerRef = ref();
|
const footerRef = ref();
|
||||||
|
|
||||||
|
const { isMobile } = useIsMobile();
|
||||||
// const { height: headerHeight } = useElementSize(headerRef);
|
// const { height: headerHeight } = useElementSize(headerRef);
|
||||||
// const { height: footerHeight } = useElementSize(footerRef);
|
// const { height: footerHeight } = useElementSize(footerRef);
|
||||||
const state = props.modalApi?.useStore?.();
|
const state = props.modalApi?.useStore?.();
|
||||||
@ -66,7 +67,11 @@ const draggable = usePriorityValue('draggable', props, state);
|
|||||||
const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
|
const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
|
||||||
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
||||||
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', 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);
|
const { dragging } = useModalDraggable(dialogRef, headerRef, shouldDraggable);
|
||||||
|
|
||||||
@ -114,6 +119,14 @@ function escapeKeyDown(e: KeyboardEvent) {
|
|||||||
e.preventDefault();
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -133,8 +146,8 @@ function escapeKeyDown(e: KeyboardEvent) {
|
|||||||
props.class,
|
props.class,
|
||||||
{
|
{
|
||||||
'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':
|
'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':
|
||||||
fullscreen,
|
shouldFullscreen,
|
||||||
'top-1/2 -translate-y-1/2': centered && !fullscreen,
|
'top-1/2 -translate-y-1/2': centered && !shouldFullscreen,
|
||||||
'duration-300': !dragging,
|
'duration-300': !dragging,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -143,6 +156,7 @@ function escapeKeyDown(e: KeyboardEvent) {
|
|||||||
close-class="top-4"
|
close-class="top-4"
|
||||||
@escape-key-down="escapeKeyDown"
|
@escape-key-down="escapeKeyDown"
|
||||||
@interact-outside="interactOutside"
|
@interact-outside="interactOutside"
|
||||||
|
@pointer-down-outside="pointerDownOutside"
|
||||||
>
|
>
|
||||||
<DialogHeader
|
<DialogHeader
|
||||||
ref="headerRef"
|
ref="headerRef"
|
||||||
@ -156,7 +170,7 @@ function escapeKeyDown(e: KeyboardEvent) {
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<DialogTitle v-if="title">
|
<DialogTitle v-if="title" class="text-left">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|
||||||
@ -191,7 +205,7 @@ function escapeKeyDown(e: KeyboardEvent) {
|
|||||||
|
|
||||||
<VbenIconButton
|
<VbenIconButton
|
||||||
v-if="fullscreenButton"
|
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"
|
@click="handleFullscreen"
|
||||||
>
|
>
|
||||||
<Shrink v-if="fullscreen" class="size-3.5" />
|
<Shrink v-if="fullscreen" class="size-3.5" />
|
||||||
@ -201,22 +215,22 @@ function escapeKeyDown(e: KeyboardEvent) {
|
|||||||
<DialogFooter
|
<DialogFooter
|
||||||
v-if="showFooter"
|
v-if="showFooter"
|
||||||
ref="footerRef"
|
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="prepend-footer"></slot>
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
<VbenButton
|
<VbenButton variant="ghost" @click="() => modalApi?.onCancel()">
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
@click="() => modalApi?.onCancel()"
|
|
||||||
>
|
|
||||||
<slot name="cancelText">
|
<slot name="cancelText">
|
||||||
{{ cancelText }}
|
{{ cancelText }}
|
||||||
</slot>
|
</slot>
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
<VbenButton
|
<VbenButton
|
||||||
:loading="confirmLoading"
|
:loading="confirmLoading"
|
||||||
size="sm"
|
|
||||||
@click="() => modalApi?.onConfirm()"
|
@click="() => modalApi?.onConfirm()"
|
||||||
>
|
>
|
||||||
<slot name="confirmText">
|
<slot name="confirmText">
|
||||||
|
@ -94,7 +94,7 @@ async function checkProps(api: ExtendedModalApi, attrs: Record<string, any>) {
|
|||||||
if (stateKeys.has(attr)) {
|
if (stateKeys.has(attr)) {
|
||||||
// connectedComponent存在时,不要传入Modal的props,会造成复杂度提升,如果你需要修改Modal的props,请使用 useModal 或者api
|
// connectedComponent存在时,不要传入Modal的props,会造成复杂度提升,如果你需要修改Modal的props,请使用 useModal 或者api
|
||||||
console.warn(
|
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>
|
<DialogPortal>
|
||||||
<DialogOverlay
|
<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] backdrop-blur-sm"
|
||||||
|
data-dismissable-modal="true"
|
||||||
@click="() => emits('close')"
|
@click="() => emits('close')"
|
||||||
/>
|
/>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
@ -40,6 +40,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay
|
<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]"
|
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
|
<DialogContent
|
||||||
:class="cn(sheetVariants({ side }), 'z-[1000]', props.class)"
|
:class="cn(sheetVariants({ side }), 'z-[1000]', props.class)"
|
||||||
|
@ -159,7 +159,7 @@ function toggleUnlockForm() {
|
|||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<div
|
<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">
|
<div v-if="showUnlockForm" class="enter-x mb-2 text-3xl">
|
||||||
{{ hour }}:{{ minute }} <span class="text-lg">{{ meridiem }}</span>
|
{{ hour }}:{{ minute }} <span class="text-lg">{{ meridiem }}</span>
|
||||||
|
@ -55,7 +55,7 @@ const listen = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Drawer v-bind="attrs" v-on="listen" />
|
<Drawer v-bind="{ ...$attrs, ...attrs }" v-on="listen" />
|
||||||
|
|
||||||
<div @click="() => drawerApi.open()">
|
<div @click="() => drawerApi.open()">
|
||||||
<slot>
|
<slot>
|
||||||
|
3
packages/utils/src/helpers/get-popup-container.ts
Normal file
3
packages/utils/src/helpers/get-popup-container.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
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-menus';
|
||||||
export * from './generate-routes-backend';
|
export * from './generate-routes-backend';
|
||||||
export * from './generate-routes-frontend';
|
export * from './generate-routes-frontend';
|
||||||
|
export * from './get-popup-container';
|
||||||
export * from './merge-route-modules';
|
export * from './merge-route-modules';
|
||||||
export * from './reset-routes';
|
export * from './reset-routes';
|
||||||
export * from './unmount-global-loading';
|
export * from './unmount-global-loading';
|
||||||
|
3
vitest.workspace.ts
Normal file
3
vitest.workspace.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { defineWorkspace } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineWorkspace(['vitest.config.ts']);
|
Loading…
Reference in New Issue
Block a user