This commit is contained in:
dap
2024-12-11 16:00:25 +08:00
49 changed files with 1171 additions and 110 deletions

View File

@@ -7,6 +7,9 @@ export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
/** layout footer 组件的高度 */
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
/** 内容区域的组件ID */
export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;
/**
* @zh_CN 默认命名空间
*/

View File

@@ -142,13 +142,29 @@ defineExpose({
"
:style="queryFormStyle"
>
<template v-if="rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.content }}
</component>
</template>
<!-- 重置按钮前 -->
<slot name="reset-before"></slot>
<component
:is="COMPONENT_MAP.DefaultButton"
v-if="resetButtonOptions.show"
class="mr-3"
class="ml-3"
type="button"
@click="handleReset"
v-bind="resetButtonOptions"
@@ -156,18 +172,21 @@ defineExpose({
{{ resetButtonOptions.content }}
</component>
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<template v-if="!rootProps.actionButtonsReverse">
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.content }}
</component>
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
class="ml-3"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.content }}
</component>
</template>
<!-- 展开按钮前 -->
<slot name="expand-before"></slot>

View File

@@ -307,6 +307,10 @@ export interface VbenFormProps<
FormRenderProps<T>,
'componentBindEventMap' | 'componentMap' | 'form'
> {
/**
* 操作按钮是否反转(提交按钮前置)
*/
actionButtonsReverse?: boolean;
/**
* 表单操作区域class
*/

View File

@@ -62,9 +62,7 @@ function handleKeyDownEnter(event: KeyboardEvent) {
watch(
() => form.values,
useDebounceFn(() => {
(props.handleValuesChange ?? state.value.handleValuesChange)?.(
toRaw(form.values),
);
forward.value.handleValuesChange?.(toRaw(form.values));
state.value.submitOnChange && props.formApi?.submitForm();
}, 300),
{ deep: true },

View File

@@ -40,6 +40,7 @@
"@vben-core/composables": "workspace:*",
"@vben-core/icons": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",
"@vueuse/core": "catalog:",
"vue": "catalog:"

View File

@@ -11,6 +11,7 @@ import {
} from '@vben-core/composables';
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
@@ -457,6 +458,8 @@ function handleHeaderToggle() {
emit('toggleSidebar');
}
}
const idMainContent = ELEMENT_ID_MAIN_CONTENT;
</script>
<template>
@@ -553,6 +556,7 @@ function handleHeaderToggle() {
<!-- </div> -->
<LayoutContent
:id="idMainContent"
:content-compact="contentCompact"
:content-compact-width="contentCompactWidth"
:padding="contentPadding"

View File

@@ -7,6 +7,11 @@ import type { Component, Ref } from 'vue';
export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
export interface DrawerProps {
/**
* 是否挂载到内容区域
* @default false
*/
appendToMain?: boolean;
/**
* 取消按钮文字
*/
@@ -59,12 +64,12 @@ export interface DrawerProps {
* 弹窗头部样式
*/
headerClass?: ClassType;
/**
* 弹窗是否显示
* @default false
*/
loading?: boolean;
/**
* 是否显示遮罩
* @default true
@@ -74,12 +79,12 @@ export interface DrawerProps {
* 是否自动聚焦
*/
openAutoFocus?: boolean;
/**
* 抽屉位置
* @default right
*/
placement?: DrawerPlacement;
/**
* 是否显示取消按钮
* @default true
@@ -98,6 +103,10 @@ export interface DrawerProps {
* 弹窗标题提示
*/
titleTooltip?: string;
/**
* 抽屉层级
*/
zIndex?: number;
}
export interface DrawerState extends DrawerProps {

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
import { provide, ref, useId, watch } from 'vue';
import { computed, provide, ref, useId, watch } from 'vue';
import {
useIsMobile,
@@ -23,6 +23,7 @@ import {
VbenLoading,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
@@ -31,7 +32,9 @@ interface Props extends DrawerProps {
}
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
drawerApi: undefined,
zIndex: 1000,
});
const components = globalShareState.getComponents();
@@ -46,6 +49,7 @@ const { isMobile } = useIsMobile();
const state = props.drawerApi?.useStore?.();
const {
appendToMain,
cancelText,
class: drawerClass,
closable,
@@ -67,6 +71,7 @@ const {
showConfirmButton,
title,
titleTooltip,
zIndex,
} = usePriorityValues(props, state);
watch(
@@ -110,6 +115,10 @@ function handleFocusOutside(e: Event) {
e.preventDefault();
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
});
</script>
<template>
<Sheet
@@ -118,6 +127,7 @@ function handleFocusOutside(e: Event) {
@update:open="() => drawerApi?.close()"
>
<SheetContent
:append-to="getAppendTo"
:class="
cn('flex w-[520px] flex-col', drawerClass, {
'!w-full': isMobile || placement === 'bottom' || placement === 'top',
@@ -127,6 +137,7 @@ function handleFocusOutside(e: Event) {
:modal="modal"
:open="state?.isOpen"
:side="placement"
:z-index="zIndex"
@close-auto-focus="handleFocusOutside"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"

View File

@@ -3,6 +3,11 @@ import type { ModalApi } from './modal-api';
import type { Component, Ref } from 'vue';
export interface ModalProps {
/**
* 是否要挂载到内容区域
* @default false
*/
appendToMain?: boolean;
/**
* 是否显示边框
* @default false
@@ -12,7 +17,6 @@ export interface ModalProps {
* 取消按钮文字
*/
cancelText?: string;
/**
* 是否居中
* @default false
@@ -20,6 +24,7 @@ export interface ModalProps {
centered?: boolean;
class?: string;
/**
* 是否显示右上角的关闭按钮
* @default true
@@ -112,6 +117,10 @@ export interface ModalProps {
* 弹窗标题提示
*/
titleTooltip?: string;
/**
* 弹窗层级
*/
zIndex?: number;
}
export interface ModalState extends ModalProps {

View File

@@ -22,6 +22,7 @@ import {
VbenLoading,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
@@ -32,6 +33,7 @@ interface Props extends ModalProps {
}
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
modalApi: undefined,
});
@@ -52,6 +54,7 @@ const { isMobile } = useIsMobile();
const state = props.modalApi?.useStore?.();
const {
appendToMain,
bordered,
cancelText,
centered,
@@ -78,6 +81,7 @@ const {
showConfirmButton,
title,
titleTooltip,
zIndex,
} = usePriorityValues(props, state);
const shouldFullscreen = computed(
@@ -161,6 +165,9 @@ function handleFocusOutside(e: Event) {
e.preventDefault();
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
});
</script>
<template>
<Dialog
@@ -170,6 +177,7 @@ function handleFocusOutside(e: Event) {
>
<DialogContent
ref="contentRef"
:append-to="getAppendTo"
:class="
cn(
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
@@ -187,6 +195,7 @@ function handleFocusOutside(e: Event) {
:modal="modal"
:open="state?.isOpen"
:show-close="closable"
:z-index="zIndex"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"

View File

@@ -20,14 +20,16 @@ import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
{
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
modal?: boolean;
open?: boolean;
showClose?: boolean;
zIndex?: number;
} & DialogContentProps
>(),
{ showClose: true },
{ appendTo: 'body', showClose: true, zIndex: 1000 },
);
const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits
@@ -45,6 +47,18 @@ const delegatedProps = computed(() => {
return delegated;
});
function isAppendToBody() {
return (
props.appendTo === 'body' ||
props.appendTo === document.body ||
!props.appendTo
);
}
const position = computed(() => {
return isAppendToBody() ? 'fixed' : 'absolute';
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
@@ -64,17 +78,22 @@ defineExpose({
</script>
<template>
<DialogPortal>
<DialogPortal :to="appendTo">
<Transition name="fade">
<DialogOverlay v-if="open && modal" @click="() => emits('close')" />
<DialogOverlay
v-if="open && modal"
:style="{ zIndex, position }"
@click="() => emits('close')"
/>
</Transition>
<DialogContent
ref="contentRef"
:style="{ zIndex, position }"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'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%] fixed z-[1000] w-full p-6 shadow-lg outline-none sm:rounded-xl',
'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',
props.class,
)
"

View File

@@ -7,8 +7,5 @@ useScrollLock();
const id = inject('DISMISSABLE_MODAL_ID');
</script>
<template>
<div
:data-dismissable-modal="id"
class="bg-overlay fixed inset-0 z-[1000]"
></div>
<div :data-dismissable-modal="id" class="bg-overlay inset-0"></div>
</template>

View File

@@ -14,7 +14,10 @@ import {
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<{ class?: any } & DialogContentProps>();
const props = withDefaults(
defineProps<{ class?: any; zIndex?: number } & DialogContentProps>(),
{ zIndex: 1000 },
);
const emits = defineEmits<DialogContentEmits>();
const delegatedProps = computed(() => {
@@ -29,7 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<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 border-border fixed inset-0 z-[1000] grid place-items-center overflow-y-auto border bg-black/80"
:style="{ zIndex }"
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 border-border absolute inset-0 grid place-items-center overflow-y-auto border bg-black/80"
>
<DialogContent
:class="
@@ -38,6 +42,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
props.class,
)
"
:style="{ zIndex }"
v-bind="forwarded"
@pointer-down-outside="
(event) => {

View File

@@ -15,17 +15,22 @@ import { type SheetVariants, sheetVariants } from './sheet';
import SheetOverlay from './SheetOverlay.vue';
interface SheetContentProps extends DialogContentProps {
appendTo?: HTMLElement | string;
class?: any;
modal?: boolean;
open?: boolean;
side?: SheetVariants['side'];
zIndex?: number;
}
defineOptions({
inheritAttrs: false,
});
const props = defineProps<SheetContentProps>();
const props = withDefaults(defineProps<SheetContentProps>(), {
appendTo: 'body',
zIndex: 1000,
});
const emits = defineEmits<DialogContentEmits>();
@@ -41,16 +46,29 @@ const delegatedProps = computed(() => {
return delegated;
});
function isAppendToBody() {
return (
props.appendTo === 'body' ||
props.appendTo === document.body ||
!props.appendTo
);
}
const position = computed(() => {
return isAppendToBody() ? 'fixed' : 'absolute';
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
</script>
<template>
<DialogPortal>
<DialogPortal :to="appendTo">
<Transition name="fade">
<SheetOverlay v-if="open && modal" />
<SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
</Transition>
<DialogContent
:class="cn(sheetVariants({ side }), 'z-[1000]', props.class)"
:class="cn(sheetVariants({ side }), props.class)"
:style="{ zIndex, position }"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot></slot>

View File

@@ -7,8 +7,5 @@ useScrollLock();
const id = inject('DISMISSABLE_DRAWER_ID');
</script>
<template>
<div
:data-dismissable-drawer="id"
class="bg-overlay fixed inset-0 z-[1000]"
></div>
<div :data-dismissable-drawer="id" class="bg-overlay inset-0"></div>
</template>

View File

@@ -1,7 +1,7 @@
import { cva, type VariantProps } from 'class-variance-authority';
export const sheetVariants = cva(
'fixed z-[1000] bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
'bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
{
defaultVariants: {
side: 'right',
@@ -12,7 +12,7 @@ export const sheetVariants = cva(
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
'inset-y-0 right-0 w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
},
},

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { AnyPromiseFunction } from '@vben/types';
import { computed, ref, unref, useAttrs, type VNode, watch } from 'vue';
import { type Component, computed, ref, unref, useAttrs, watch } from 'vue';
import { LoaderCircle } from '@vben/icons';
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
@@ -10,37 +10,56 @@ import { objectOmit } from '@vueuse/core';
type OptionsItem = {
[name: string]: any;
children?: OptionsItem[];
disabled?: boolean;
label?: string;
value?: string;
};
interface Props {
//
component: VNode;
/** 组件 */
component: Component;
/** 是否将value从数字转为string */
numberToString?: boolean;
/** 获取options数据的函数 */
api?: (arg?: any) => Promise<OptionsItem[] | Record<string, any>>;
/** 传递给api的参数 */
params?: Record<string, any>;
/** 从api返回的结果中提取options数组的字段名 */
resultField?: string;
/** label字段名 */
labelField?: string;
/** children字段名需要层级数据的组件可用 */
childrenField?: string;
/** value字段名 */
valueField?: string;
/** 组件接收options数据的属性名 */
optionsPropName?: string;
/** 是否立即调用api */
immediate?: boolean;
/** 每次`visibleEvent`事件发生时都重新请求数据 */
alwaysLoad?: boolean;
/** 在api请求之前的回调函数 */
beforeFetch?: AnyPromiseFunction<any, any>;
/** 在api请求之后的回调函数 */
afterFetch?: AnyPromiseFunction<any, any>;
/** 直接传入选项数据也作为api返回空数据时的后备数据 */
options?: OptionsItem[];
//
/** 组件的插槽名称,用来显示一个"加载中"的图标 */
loadingSlot?: string;
//
/** 触发api请求的事件名 */
visibleEvent?: string;
modelField?: string;
/** 组件的v-model属性名默认为modelValue。部分组件可能为value */
modelPropName?: string;
}
defineOptions({ name: 'ApiSelect', inheritAttrs: false });
defineOptions({ name: 'ApiComponent', inheritAttrs: false });
const props = withDefaults(defineProps<Props>(), {
labelField: 'label',
valueField: 'value',
childrenField: '',
optionsPropName: 'options',
resultField: '',
visibleEvent: '',
numberToString: false,
@@ -50,7 +69,7 @@ const props = withDefaults(defineProps<Props>(), {
loadingSlot: '',
beforeFetch: undefined,
afterFetch: undefined,
modelField: 'modelValue',
modelPropName: 'modelValue',
api: undefined,
options: () => [],
});
@@ -69,29 +88,34 @@ const loading = ref(false);
const isFirstLoaded = ref(false);
const getOptions = computed(() => {
const { labelField, valueField, numberToString } = props;
const { labelField, valueField, childrenField, numberToString } = props;
const data: OptionsItem[] = [];
const refOptionsData = unref(refOptions);
for (const next of refOptionsData) {
if (next) {
const value = get(next, valueField);
data.push({
...objectOmit(next, [labelField, valueField]),
label: get(next, labelField),
function transformData(data: OptionsItem[]): OptionsItem[] {
return data.map((item) => {
const value = get(item, valueField);
return {
...objectOmit(item, [labelField, valueField, childrenField]),
label: get(item, labelField),
value: numberToString ? `${value}` : value,
});
}
...(childrenField && item[childrenField]
? { children: transformData(item[childrenField]) }
: {}),
};
});
}
const data: OptionsItem[] = transformData(refOptionsData);
return data.length > 0 ? data : props.options;
});
const bindProps = computed(() => {
return {
[props.modelField]: unref(modelValue),
[`onUpdate:${props.modelField}`]: (val: string) => {
[props.modelPropName]: unref(modelValue),
[props.optionsPropName]: unref(getOptions),
[`onUpdate:${props.modelPropName}`]: (val: string) => {
modelValue.value = val;
},
...objectOmit(attrs, ['onUpdate:value']),
@@ -168,7 +192,6 @@ function emitChange() {
<component
:is="component"
v-bind="bindProps"
:options="getOptions"
:placeholder="$attrs.placeholder"
>
<template v-for="item in Object.keys($slots)" #[item]="data">

View File

@@ -0,0 +1 @@
export { default as ApiComponent } from './api-component.vue';

View File

@@ -1 +0,0 @@
export { default as ApiSelect } from './api-select.vue';

View File

@@ -1,4 +1,4 @@
export * from './api-select';
export * from './api-component';
export * from './captcha';
export * from './code-mirror';
export * from './ellipsis-text';

View File

@@ -1,5 +1,12 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
import {
computed,
nextTick,
onMounted,
ref,
type StyleValue,
useTemplateRef,
} from 'vue';
import { CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT } from '@vben-core/shared/constants';
import { cn } from '@vben-core/shared/utils';
@@ -29,13 +36,11 @@ const shouldAutoHeight = ref(false);
const headerRef = useTemplateRef<HTMLDivElement>('headerRef');
const footerRef = useTemplateRef<HTMLDivElement>('footerRef');
const contentStyle = computed(() => {
const contentStyle = computed<StyleValue>(() => {
if (autoContentHeight) {
return {
height: shouldAutoHeight.value
? `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`
: '0',
// 'overflow-y': shouldAutoHeight.value?'auto':'unset',
height: `calc(var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT}) - ${headerHeight.value}px)`,
overflowY: shouldAutoHeight.value ? 'auto' : 'unset',
};
}
return {};

View File

@@ -16,6 +16,7 @@ import {
VxeInput,
VxeLoading,
VxeModal,
VxeNumberInput,
VxePager,
// VxeList,
// VxeModal,
@@ -82,6 +83,7 @@ export function initVxeTable() {
// VxeUI.component(VxeList);
VxeUI.component(VxeLoading);
VxeUI.component(VxeModal);
VxeUI.component(VxeNumberInput);
// VxeUI.component(VxeOptgroup);
// VxeUI.component(VxeOption);
VxeUI.component(VxePager);