mirror of
https://github.com/vbenjs/gf-vben-admin.git
synced 2025-02-02 19:08:40 +08:00
perf(modal-drawer): replace the scrollbar assembly
This commit is contained in:
parent
73cee06daa
commit
ebf7c8aa53
@ -12,6 +12,10 @@
|
|||||||
- form: 新增远程下拉`ApiSelect`及示例
|
- form: 新增远程下拉`ApiSelect`及示例
|
||||||
- form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框
|
- form: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框
|
||||||
|
|
||||||
|
### ⚡ Performance Improvements
|
||||||
|
|
||||||
|
- 优化`modal`与`drawer`滚动条组件
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
- 修复混合模式下滚动条丢失问题
|
- 修复混合模式下滚动条丢失问题
|
||||||
@ -21,6 +25,7 @@
|
|||||||
- 修复路由类型错误
|
- 修复路由类型错误
|
||||||
- 修复菜单分割时权限失效问题
|
- 修复菜单分割时权限失效问题
|
||||||
- 关闭多标签页时 iframe 提前加载
|
- 关闭多标签页时 iframe 提前加载
|
||||||
|
- 修复`modal`与`drawer`已知问题
|
||||||
|
|
||||||
## 2.0.0-rc.14 (2020-12-15)
|
## 2.0.0-rc.14 (2020-12-15)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition-group
|
<transition-group
|
||||||
class="lazy-container"
|
:class="prefixCls"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
ref="elRef"
|
ref="elRef"
|
||||||
:name="transitionName"
|
:name="transitionName"
|
||||||
@ -25,6 +25,7 @@
|
|||||||
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
||||||
import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
|
import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
|
||||||
import { propTypes } from '/@/utils/propTypes';
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
isInit: boolean;
|
isInit: boolean;
|
||||||
@ -70,6 +71,8 @@
|
|||||||
intersectionObserverInstance: null,
|
intersectionObserverInstance: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { prefixCls } = useDesign('lazy-container');
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
immediateInit();
|
immediateInit();
|
||||||
initIntersectionObserver();
|
initIntersectionObserver();
|
||||||
@ -129,13 +132,17 @@
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
elRef,
|
elRef,
|
||||||
|
prefixCls,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.lazy-container {
|
@import (reference) '../../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-lazy-container';
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="collapse-container p-2">
|
<div :class="['p-2', prefixCls]">
|
||||||
<CollapseHeader v-bind="$props" :show="show" @expand="handleExpand">
|
<CollapseHeader v-bind="$props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
|
||||||
<template #title>
|
<template #title>
|
||||||
<slot name="title" />
|
<slot name="title" />
|
||||||
</template>
|
</template>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<CollapseTransition :enable="canExpan">
|
<CollapseTransition :enable="canExpan">
|
||||||
<Skeleton v-if="loading" />
|
<Skeleton v-if="loading" />
|
||||||
<div class="collapse-container__body" v-else v-show="show">
|
<div :class="`${prefixCls}__body`" v-else v-show="show">
|
||||||
<LazyContainer :timeout="lazyTime" v-if="lazy">
|
<LazyContainer :timeout="lazyTime" v-if="lazy">
|
||||||
<slot />
|
<slot />
|
||||||
<template #skeleton>
|
<template #skeleton>
|
||||||
@ -35,6 +35,7 @@
|
|||||||
// hook
|
// hook
|
||||||
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
||||||
import { propTypes } from '/@/utils/propTypes';
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'CollapseContainer',
|
name: 'CollapseContainer',
|
||||||
@ -64,6 +65,9 @@
|
|||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const show = ref(true);
|
const show = ref(true);
|
||||||
|
|
||||||
|
const { prefixCls } = useDesign('collapse-container');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Handling development events
|
* @description: Handling development events
|
||||||
*/
|
*/
|
||||||
@ -77,20 +81,20 @@
|
|||||||
return {
|
return {
|
||||||
show,
|
show,
|
||||||
handleExpand,
|
handleExpand,
|
||||||
|
prefixCls,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.collapse-container {
|
@import (reference) '../../../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-collapse-container';
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
&.no-shadow {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="collapse-container__header">
|
<div :class="`${prefixCls}__header`">
|
||||||
<BasicTitle :helpMessage="$attrs.helpMessage">
|
<BasicTitle :helpMessage="$attrs.helpMessage">
|
||||||
<template v-if="$attrs.title">
|
<template v-if="$attrs.title">
|
||||||
{{ $attrs.title }}
|
{{ $attrs.title }}
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</BasicTitle>
|
</BasicTitle>
|
||||||
|
|
||||||
<div class="collapse-container__action">
|
<div :class="`${prefixCls}__action`">
|
||||||
<slot name="action" />
|
<slot name="action" />
|
||||||
<BasicArrow v-if="$attrs.canExpan" top :expand="$attrs.show" @click="$emit('expand')" />
|
<BasicArrow v-if="$attrs.canExpan" top :expand="$attrs.show" @click="$emit('expand')" />
|
||||||
</div>
|
</div>
|
||||||
@ -21,5 +21,8 @@
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
components: { BasicArrow, BasicTitle },
|
components: { BasicArrow, BasicTitle },
|
||||||
|
props: {
|
||||||
|
prefixCls: String,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -24,7 +24,7 @@ export default defineComponent({
|
|||||||
const getMergeProps = computed(() => {
|
const getMergeProps = computed(() => {
|
||||||
return {
|
return {
|
||||||
...props,
|
...props,
|
||||||
...(unref(propsRef) as any),
|
...(unref(propsRef) as Recordable),
|
||||||
} as DescOptions;
|
} as DescOptions;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ export default {
|
|||||||
bordered: propTypes.bool.def(true),
|
bordered: propTypes.bool.def(true),
|
||||||
|
|
||||||
column: {
|
column: {
|
||||||
type: [Number, Object] as PropType<number | any>,
|
type: [Number, Object] as PropType<number | Recordable>,
|
||||||
default: () => {
|
default: () => {
|
||||||
return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
||||||
},
|
},
|
||||||
|
@ -15,7 +15,10 @@ export interface DescItem {
|
|||||||
span?: number;
|
span?: number;
|
||||||
show?: (...arg: any) => boolean;
|
show?: (...arg: any) => boolean;
|
||||||
// render
|
// render
|
||||||
render?: (val: string, data: any) => VNode | undefined | JSX.Element | Element | string | number;
|
render?: (
|
||||||
|
val: string,
|
||||||
|
data: Recordable
|
||||||
|
) => VNode | undefined | JSX.Element | Element | string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DescOptions extends DescriptionsProps {
|
export interface DescOptions extends DescriptionsProps {
|
||||||
@ -30,7 +33,7 @@ export interface DescOptions extends DescriptionsProps {
|
|||||||
* 数据
|
* 数据
|
||||||
* @type object
|
* @type object
|
||||||
*/
|
*/
|
||||||
data: any;
|
data: Recordable;
|
||||||
/**
|
/**
|
||||||
* Built-in CollapseContainer component configuration
|
* Built-in CollapseContainer component configuration
|
||||||
* @type CollapseContainerOptions
|
* @type CollapseContainerOptions
|
||||||
|
@ -19,7 +19,7 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType
|
|||||||
|
|
||||||
const methods: DescInstance = {
|
const methods: DescInstance = {
|
||||||
setDescProps: (descProps: Partial<DescOptions>): void => {
|
setDescProps: (descProps: Partial<DescOptions>): void => {
|
||||||
unref(descRef)!.setDescProps(descProps);
|
unref(descRef)?.setDescProps(descProps);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { withInstall } from '../util';
|
import { withInstall } from '../util';
|
||||||
|
|
||||||
import BasicDrawer from './src/BasicDrawer';
|
import BasicDrawer from './src/BasicDrawer.vue';
|
||||||
|
|
||||||
export { BasicDrawer };
|
export { BasicDrawer };
|
||||||
export * from './src/types';
|
export * from './src/types';
|
||||||
|
@ -1,246 +0,0 @@
|
|||||||
import './index.less';
|
|
||||||
|
|
||||||
import type { DrawerInstance, DrawerProps } from './types';
|
|
||||||
import type { CSSProperties } from 'vue';
|
|
||||||
|
|
||||||
import { defineComponent, ref, computed, watchEffect, watch, unref, nextTick, toRaw } from 'vue';
|
|
||||||
import { Drawer, Row, Col, Button } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { BasicTitle } from '/@/components/Basic';
|
|
||||||
import { Loading } from '/@/components/Loading';
|
|
||||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
import { useI18n } from '/@/hooks/web/useI18n';
|
|
||||||
|
|
||||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
|
||||||
import { isFunction, isNumber } from '/@/utils/is';
|
|
||||||
import { deepMerge } from '/@/utils';
|
|
||||||
import { tryTsxEmit } from '/@/utils/helper/vueHelper';
|
|
||||||
|
|
||||||
import { basicProps } from './props';
|
|
||||||
|
|
||||||
const prefixCls = 'basic-drawer';
|
|
||||||
export default defineComponent({
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: basicProps,
|
|
||||||
emits: ['visible-change', 'ok', 'close', 'register'],
|
|
||||||
setup(props, { slots, emit, attrs }) {
|
|
||||||
const scrollRef = ref<ElRef>(null);
|
|
||||||
const visibleRef = ref(false);
|
|
||||||
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null);
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const getMergeProps = computed(
|
|
||||||
(): DrawerProps => {
|
|
||||||
return deepMerge(toRaw(props), unref(propsRef));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getProps = computed(
|
|
||||||
(): DrawerProps => {
|
|
||||||
const opt = {
|
|
||||||
placement: 'right',
|
|
||||||
...attrs,
|
|
||||||
...unref(getMergeProps),
|
|
||||||
visible: unref(visibleRef),
|
|
||||||
};
|
|
||||||
opt.title = undefined;
|
|
||||||
const { isDetail, width, wrapClassName, getContainer } = opt;
|
|
||||||
if (isDetail) {
|
|
||||||
if (!width) {
|
|
||||||
opt.width = '100%';
|
|
||||||
}
|
|
||||||
const detailCls = `${prefixCls}__detail`;
|
|
||||||
|
|
||||||
opt.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
|
|
||||||
|
|
||||||
if (!getContainer) {
|
|
||||||
// TODO type error?
|
|
||||||
opt.getContainer = '.layout-content' as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return opt as DrawerProps;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getBindValues = computed(
|
|
||||||
(): DrawerProps => {
|
|
||||||
return {
|
|
||||||
...attrs,
|
|
||||||
...unref(getProps),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Custom implementation of the bottom button,
|
|
||||||
const getFooterHeight = computed(() => {
|
|
||||||
const { footerHeight, showFooter } = unref(getProps);
|
|
||||||
|
|
||||||
if (showFooter && footerHeight) {
|
|
||||||
return isNumber(footerHeight) ? `${footerHeight}px` : `${footerHeight.replace('px', '')}px`;
|
|
||||||
}
|
|
||||||
return `0px`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getScrollContentStyle = computed(
|
|
||||||
(): CSSProperties => {
|
|
||||||
const footerHeight = unref(getFooterHeight);
|
|
||||||
return {
|
|
||||||
position: 'relative',
|
|
||||||
height: `calc(100% - ${footerHeight})`,
|
|
||||||
overflow: 'auto',
|
|
||||||
padding: '16px',
|
|
||||||
paddingBottom: '30px',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getLoading = computed(() => {
|
|
||||||
return !!unref(getProps)?.loading;
|
|
||||||
});
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
visibleRef.value = props.visible;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => visibleRef.value,
|
|
||||||
(visible) => {
|
|
||||||
nextTick(() => {
|
|
||||||
emit('visible-change', visible);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cancel event
|
|
||||||
async function onClose(e: ChangeEvent) {
|
|
||||||
const { closeFunc } = unref(getProps);
|
|
||||||
emit('close', e);
|
|
||||||
if (closeFunc && isFunction(closeFunc)) {
|
|
||||||
const res = await closeFunc();
|
|
||||||
visibleRef.value = !res;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
visibleRef.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDrawerProps(props: Partial<DrawerProps>): void {
|
|
||||||
// Keep the last setDrawerProps
|
|
||||||
propsRef.value = deepMerge(unref(propsRef) || {}, props);
|
|
||||||
|
|
||||||
if (Reflect.has(props, 'visible')) {
|
|
||||||
visibleRef.value = !!props.visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFooter() {
|
|
||||||
if (slots?.footer) {
|
|
||||||
return getSlot(slots, 'footer');
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
showCancelBtn,
|
|
||||||
cancelButtonProps,
|
|
||||||
cancelText,
|
|
||||||
showOkBtn,
|
|
||||||
okType,
|
|
||||||
okText,
|
|
||||||
okButtonProps,
|
|
||||||
confirmLoading,
|
|
||||||
showFooter,
|
|
||||||
} = unref(getProps);
|
|
||||||
if (!showFooter) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={`${prefixCls}__footer`}>
|
|
||||||
{getSlot(slots, 'insertFooter')}
|
|
||||||
{showCancelBtn && (
|
|
||||||
<Button {...cancelButtonProps} onClick={onClose} class="mr-2">
|
|
||||||
{() => cancelText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{getSlot(slots, 'centerFooter')}
|
|
||||||
{showOkBtn && (
|
|
||||||
<Button
|
|
||||||
type={okType}
|
|
||||||
onClick={() => {
|
|
||||||
emit('ok');
|
|
||||||
}}
|
|
||||||
{...okButtonProps}
|
|
||||||
loading={confirmLoading}
|
|
||||||
>
|
|
||||||
{() => okText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{getSlot(slots, 'appendFooter')}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHeader() {
|
|
||||||
if (slots?.title) {
|
|
||||||
return getSlot(slots, 'title');
|
|
||||||
}
|
|
||||||
const { title } = unref(getMergeProps);
|
|
||||||
|
|
||||||
if (!props.isDetail) {
|
|
||||||
return <BasicTitle>{() => title || getSlot(slots, 'title')}</BasicTitle>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Row type="flex" align="middle" class={`${prefixCls}__detail-header`}>
|
|
||||||
{() => (
|
|
||||||
<>
|
|
||||||
{props.showDetailBack && (
|
|
||||||
<Button size="small" type="link" onClick={onClose}>
|
|
||||||
{() => <LeftOutlined />}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{title && (
|
|
||||||
<Col style="flex:1" class={[`${prefixCls}__detail-title`, 'ellipsis', 'px-2']}>
|
|
||||||
{() => title}
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{getSlot(slots, 'titleToolbar')}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawerInstance: DrawerInstance = {
|
|
||||||
setDrawerProps: setDrawerProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
tryTsxEmit((instance) => {
|
|
||||||
emit('register', drawerInstance, instance.uid);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<Drawer class={prefixCls} onClose={onClose} {...unref(getBindValues)}>
|
|
||||||
{{
|
|
||||||
title: () => renderHeader(),
|
|
||||||
default: () => (
|
|
||||||
<>
|
|
||||||
<div ref={scrollRef} style={unref(getScrollContentStyle)}>
|
|
||||||
<Loading
|
|
||||||
absolute
|
|
||||||
tip={t('component.drawer.loadingText')}
|
|
||||||
loading={unref(getLoading)}
|
|
||||||
/>
|
|
||||||
{getSlot(slots)}
|
|
||||||
</div>
|
|
||||||
{renderFooter()}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
259
src/components/Drawer/src/BasicDrawer.vue
Normal file
259
src/components/Drawer/src/BasicDrawer.vue
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<Drawer :class="prefixCls" @close="onClose" v-bind="getBindValues">
|
||||||
|
<template #title v-if="!$slots.title">
|
||||||
|
<DrawerHeader
|
||||||
|
:title="getMergeProps.title"
|
||||||
|
:isDetail="isDetail"
|
||||||
|
:showDetailBack="showDetailBack"
|
||||||
|
@close="onClose"
|
||||||
|
>
|
||||||
|
<template #titleToolbar>
|
||||||
|
<slot name="titleToolbar" />
|
||||||
|
</template>
|
||||||
|
</DrawerHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ScrollContainer
|
||||||
|
:style="getScrollContentStyle"
|
||||||
|
v-loading="getLoading"
|
||||||
|
:loading-tip="loadingText || t('component.drawer.loadingText')"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ScrollContainer>
|
||||||
|
<DrawerFooter v-bind="getProps" @close="onClose" @ok="handleOk" :height="getFooterHeight">
|
||||||
|
<template #[item]="data" v-for="item in Object.keys($slots)">
|
||||||
|
<slot :name="item" v-bind="data" />
|
||||||
|
</template>
|
||||||
|
</DrawerFooter>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { DrawerInstance, DrawerProps } from './types';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
ref,
|
||||||
|
computed,
|
||||||
|
watchEffect,
|
||||||
|
watch,
|
||||||
|
unref,
|
||||||
|
nextTick,
|
||||||
|
toRaw,
|
||||||
|
getCurrentInstance,
|
||||||
|
} from 'vue';
|
||||||
|
import { Drawer } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useI18n } from '/@/hooks/web/useI18n';
|
||||||
|
|
||||||
|
import { isFunction, isNumber } from '/@/utils/is';
|
||||||
|
import { deepMerge } from '/@/utils';
|
||||||
|
import DrawerFooter from './components/DrawerFooter.vue';
|
||||||
|
import DrawerHeader from './components/DrawerHeader.vue';
|
||||||
|
import { ScrollContainer } from '/@/components/Container';
|
||||||
|
|
||||||
|
import { basicProps } from './props';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
components: { Drawer, ScrollContainer, DrawerFooter, DrawerHeader },
|
||||||
|
props: basicProps,
|
||||||
|
emits: ['visible-change', 'ok', 'close', 'register'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const visibleRef = ref(false);
|
||||||
|
const attrs = useAttrs();
|
||||||
|
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { prefixVar, prefixCls } = useDesign('basic-drawer');
|
||||||
|
|
||||||
|
const drawerInstance: DrawerInstance = {
|
||||||
|
setDrawerProps: setDrawerProps,
|
||||||
|
emitVisible: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
|
||||||
|
instance && emit('register', drawerInstance, instance.uid);
|
||||||
|
|
||||||
|
const getMergeProps = computed(
|
||||||
|
(): DrawerProps => {
|
||||||
|
return deepMerge(toRaw(props), unref(propsRef));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getProps = computed(
|
||||||
|
(): DrawerProps => {
|
||||||
|
const opt = {
|
||||||
|
placement: 'right',
|
||||||
|
...unref(attrs),
|
||||||
|
...unref(getMergeProps),
|
||||||
|
visible: unref(visibleRef),
|
||||||
|
};
|
||||||
|
opt.title = undefined;
|
||||||
|
const { isDetail, width, wrapClassName, getContainer } = opt;
|
||||||
|
if (isDetail) {
|
||||||
|
if (!width) {
|
||||||
|
opt.width = '100%';
|
||||||
|
}
|
||||||
|
const detailCls = `${prefixCls}__detail`;
|
||||||
|
opt.wrapClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls;
|
||||||
|
|
||||||
|
if (!getContainer) {
|
||||||
|
// TODO type error?
|
||||||
|
opt.getContainer = `.${prefixVar}-layout-content` as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opt as DrawerProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getBindValues = computed(
|
||||||
|
(): DrawerProps => {
|
||||||
|
return {
|
||||||
|
...attrs,
|
||||||
|
...unref(getProps),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom implementation of the bottom button,
|
||||||
|
const getFooterHeight = computed(() => {
|
||||||
|
const { footerHeight, showFooter } = unref(getProps);
|
||||||
|
if (showFooter && footerHeight) {
|
||||||
|
return isNumber(footerHeight)
|
||||||
|
? `${footerHeight}px`
|
||||||
|
: `${footerHeight.replace('px', '')}px`;
|
||||||
|
}
|
||||||
|
return `0px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getScrollContentStyle = computed(
|
||||||
|
(): CSSProperties => {
|
||||||
|
const footerHeight = unref(getFooterHeight);
|
||||||
|
return {
|
||||||
|
position: 'relative',
|
||||||
|
height: `calc(100% - ${footerHeight})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLoading = computed(() => {
|
||||||
|
return !!unref(getProps)?.loading;
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
visibleRef.value = props.visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => visibleRef.value,
|
||||||
|
(visible) => {
|
||||||
|
nextTick(() => {
|
||||||
|
emit('visible-change', visible);
|
||||||
|
instance && drawerInstance.emitVisible?.(visible, instance.uid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cancel event
|
||||||
|
async function onClose(e: Recordable) {
|
||||||
|
const { closeFunc } = unref(getProps);
|
||||||
|
emit('close', e);
|
||||||
|
if (closeFunc && isFunction(closeFunc)) {
|
||||||
|
const res = await closeFunc();
|
||||||
|
visibleRef.value = !res;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visibleRef.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDrawerProps(props: Partial<DrawerProps>): void {
|
||||||
|
// Keep the last setDrawerProps
|
||||||
|
propsRef.value = deepMerge(unref(propsRef) || {}, props);
|
||||||
|
|
||||||
|
if (Reflect.has(props, 'visible')) {
|
||||||
|
visibleRef.value = !!props.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOk() {
|
||||||
|
emit('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onClose,
|
||||||
|
t,
|
||||||
|
prefixCls,
|
||||||
|
getMergeProps,
|
||||||
|
getScrollContentStyle,
|
||||||
|
getProps,
|
||||||
|
getLoading,
|
||||||
|
getBindValues,
|
||||||
|
getFooterHeight,
|
||||||
|
handleOk,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
@import (reference) '../../../design/index.less';
|
||||||
|
@header-height: 60px;
|
||||||
|
@detail-header-height: 40px;
|
||||||
|
@prefix-cls: ~'@{namespace}-basic-drawer';
|
||||||
|
@prefix-cls-detail: ~'@{namespace}-basic-drawer__detail';
|
||||||
|
|
||||||
|
.@{prefix-cls} {
|
||||||
|
.ant-drawer-wrapper-body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-close {
|
||||||
|
&:hover {
|
||||||
|
color: @error-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-body {
|
||||||
|
height: calc(100% - @header-height);
|
||||||
|
padding: 0;
|
||||||
|
background-color: @background-color-dark;
|
||||||
|
|
||||||
|
.scrollbar__wrap {
|
||||||
|
padding: 16px !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.@{prefix-cls-detail} {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.ant-drawer-header {
|
||||||
|
width: 100%;
|
||||||
|
height: @detail-header-height;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 1px solid @border-color-base;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-title {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-close {
|
||||||
|
height: @detail-header-height;
|
||||||
|
line-height: @detail-header-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar__wrap {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-body {
|
||||||
|
height: calc(100% - @detail-header-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
85
src/components/Drawer/src/components/DrawerFooter.vue
Normal file
85
src/components/Drawer/src/components/DrawerFooter.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="prefixCls" :style="getStyle" v-if="showFooter || $slots.footer">
|
||||||
|
<template v-if="!$slots.footer">
|
||||||
|
<slot name="insertFooter" />
|
||||||
|
<a-button v-bind="cancelButtonProps" @click="handleClose" class="mr-2" v-if="showCancelBtn">
|
||||||
|
{{ cancelText }}
|
||||||
|
</a-button>
|
||||||
|
<slot name="centerFooter" />
|
||||||
|
<a-button
|
||||||
|
:type="okType"
|
||||||
|
@click="handleOk"
|
||||||
|
v-bind="okButtonProps"
|
||||||
|
class="mr-2"
|
||||||
|
:loading="confirmLoading"
|
||||||
|
v-if="showOkBtn"
|
||||||
|
>
|
||||||
|
{{ okText }}
|
||||||
|
</a-button>
|
||||||
|
<slot name="appendFooter" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<slot name="footer" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import { defineComponent, computed } from 'vue';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
|
||||||
|
import { footerProps } from '../props';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BasicDrawerFooter',
|
||||||
|
props: {
|
||||||
|
...footerProps,
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '60px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['ok', 'close'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const { prefixCls } = useDesign('basic-drawer-footer');
|
||||||
|
|
||||||
|
const getStyle = computed(
|
||||||
|
(): CSSProperties => {
|
||||||
|
const heightStr = `${props.height}`;
|
||||||
|
return {
|
||||||
|
height: heightStr,
|
||||||
|
lineHeight: heightStr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleOk() {
|
||||||
|
emit('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
return { handleOk, prefixCls, handleClose, getStyle };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import (reference) '../../../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-basic-drawer-footer';
|
||||||
|
@footer-height: 60px;
|
||||||
|
.@{prefix-cls} {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 12px 0 20px;
|
||||||
|
text-align: right;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid @border-color-base;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
73
src/components/Drawer/src/components/DrawerHeader.vue
Normal file
73
src/components/Drawer/src/components/DrawerHeader.vue
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<BasicTitle v-if="!isDetail" :class="prefixCls">
|
||||||
|
<slot name="title" />
|
||||||
|
{{ !$slots.title ? title : '' }}
|
||||||
|
</BasicTitle>
|
||||||
|
|
||||||
|
<div :class="[prefixCls, `${prefixCls}--detail`]" v-else>
|
||||||
|
<span :class="`${prefixCls}__twrap`">
|
||||||
|
<span @click="handleClose" v-if="showDetailBack">
|
||||||
|
<ArrowLeftOutlined :class="`${prefixCls}__back`" />
|
||||||
|
</span>
|
||||||
|
<span v-if="title">{{ title }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span :class="`${prefixCls}__toolbar`">
|
||||||
|
<slot name="titleToolbar" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { BasicTitle } from '/@/components/Basic';
|
||||||
|
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
|
||||||
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BasicDrawerHeader',
|
||||||
|
components: { BasicTitle, ArrowLeftOutlined },
|
||||||
|
props: {
|
||||||
|
isDetail: propTypes.bool,
|
||||||
|
showDetailBack: propTypes.bool,
|
||||||
|
title: propTypes.string,
|
||||||
|
},
|
||||||
|
setup(_, { emit }) {
|
||||||
|
const { prefixCls } = useDesign('basic-drawer-header');
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
return { prefixCls, handleClose };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import (reference) '../../../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-basic-drawer-header';
|
||||||
|
@footer-height: 60px;
|
||||||
|
.@{prefix-cls} {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__back {
|
||||||
|
padding: 0 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__twrap {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,66 +0,0 @@
|
|||||||
@import (reference) '../../../design/index.less';
|
|
||||||
@header-height: 40px;
|
|
||||||
@footer-height: 60px;
|
|
||||||
|
|
||||||
.basic-drawer {
|
|
||||||
.ant-drawer-wrapper-body {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-drawer-close {
|
|
||||||
&:hover {
|
|
||||||
color: @error-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-drawer-body {
|
|
||||||
height: calc(100% - @header-height);
|
|
||||||
padding: 0;
|
|
||||||
background-color: @background-color-dark;
|
|
||||||
|
|
||||||
.scrollbar__wrap {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__detail {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
&-header {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-drawer-header {
|
|
||||||
width: 100%;
|
|
||||||
height: @header-height;
|
|
||||||
padding: 0;
|
|
||||||
border-top: 1px solid @border-color-base;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-drawer-title {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-drawer-close {
|
|
||||||
height: @header-height;
|
|
||||||
line-height: @header-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar__wrap {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: @footer-height;
|
|
||||||
padding: 0 26px;
|
|
||||||
line-height: @footer-height;
|
|
||||||
text-align: right;
|
|
||||||
background: #fff;
|
|
||||||
border-top: 1px solid @border-color-base;
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,13 +10,13 @@ export const footerProps = {
|
|||||||
* @description: Show close button
|
* @description: Show close button
|
||||||
*/
|
*/
|
||||||
showCancelBtn: propTypes.bool.def(true),
|
showCancelBtn: propTypes.bool.def(true),
|
||||||
cancelButtonProps: Object as PropType<any>,
|
cancelButtonProps: Object as PropType<Recordable>,
|
||||||
cancelText: propTypes.string.def(t('component.drawer.cancelText')),
|
cancelText: propTypes.string.def(t('component.drawer.cancelText')),
|
||||||
/**
|
/**
|
||||||
* @description: Show confirmation button
|
* @description: Show confirmation button
|
||||||
*/
|
*/
|
||||||
showOkBtn: propTypes.bool.def(true),
|
showOkBtn: propTypes.bool.def(true),
|
||||||
okButtonProps: propTypes.any,
|
okButtonProps: Object as PropType<Recordable>,
|
||||||
okText: propTypes.string.def(t('component.drawer.okText')),
|
okText: propTypes.string.def(t('component.drawer.okText')),
|
||||||
okType: propTypes.string.def('primary'),
|
okType: propTypes.string.def('primary'),
|
||||||
showFooter: propTypes.bool,
|
showFooter: propTypes.bool,
|
||||||
@ -28,6 +28,7 @@ export const footerProps = {
|
|||||||
export const basicProps = {
|
export const basicProps = {
|
||||||
isDetail: propTypes.bool,
|
isDetail: propTypes.bool,
|
||||||
title: propTypes.string.def(''),
|
title: propTypes.string.def(''),
|
||||||
|
loadingText: propTypes.string,
|
||||||
showDetailBack: propTypes.bool.def(true),
|
showDetailBack: propTypes.bool.def(true),
|
||||||
visible: propTypes.bool,
|
visible: propTypes.bool,
|
||||||
loading: propTypes.bool,
|
loading: propTypes.bool,
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
|
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
|
||||||
import type { CSSProperties, VNodeChild } from 'vue';
|
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
|
||||||
import type { ScrollContainerOptions } from '/@/components/Container/index';
|
import type { ScrollContainerOptions } from '/@/components/Container/index';
|
||||||
|
|
||||||
export interface DrawerInstance {
|
export interface DrawerInstance {
|
||||||
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void;
|
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void;
|
||||||
|
emitVisible?: (visible: boolean, uid: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReturnMethods extends DrawerInstance {
|
export interface ReturnMethods extends DrawerInstance {
|
||||||
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void;
|
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void;
|
||||||
|
getVisible?: ComputedRef<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void;
|
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void;
|
||||||
@ -16,6 +18,7 @@ export interface ReturnInnerMethods extends DrawerInstance {
|
|||||||
closeDrawer: () => void;
|
closeDrawer: () => void;
|
||||||
changeLoading: (loading: boolean) => void;
|
changeLoading: (loading: boolean) => void;
|
||||||
changeOkLoading: (loading: boolean) => void;
|
changeOkLoading: (loading: boolean) => void;
|
||||||
|
getVisible?: ComputedRef<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseDrawerReturnType = [RegisterFn, ReturnMethods];
|
export type UseDrawerReturnType = [RegisterFn, ReturnMethods];
|
||||||
|
@ -6,22 +6,32 @@ import type {
|
|||||||
UseDrawerInnerReturnType,
|
UseDrawerInnerReturnType,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
import { ref, getCurrentInstance, unref, reactive, watchEffect, nextTick, toRaw } from 'vue';
|
import {
|
||||||
|
ref,
|
||||||
|
getCurrentInstance,
|
||||||
|
unref,
|
||||||
|
reactive,
|
||||||
|
watchEffect,
|
||||||
|
nextTick,
|
||||||
|
toRaw,
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import { isProdMode } from '/@/utils/env';
|
import { isProdMode } from '/@/utils/env';
|
||||||
import { isFunction } from '/@/utils/is';
|
import { isFunction } from '/@/utils/is';
|
||||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
import { tryOnUnmounted, isInSetup } from '/@/utils/helper/vueHelper';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { error } from '/@/utils/log';
|
||||||
|
|
||||||
const dataTransferRef = reactive<any>({});
|
const dataTransferRef = reactive<any>({});
|
||||||
|
|
||||||
|
const visibleData = reactive<{ [key: number]: boolean }>({});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Applicable to separate drawer and call outside
|
* @description: Applicable to separate drawer and call outside
|
||||||
*/
|
*/
|
||||||
export function useDrawer(): UseDrawerReturnType {
|
export function useDrawer(): UseDrawerReturnType {
|
||||||
if (!getCurrentInstance()) {
|
isInSetup();
|
||||||
throw new Error('Please put useDrawer function in the setup function!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const drawerRef = ref<DrawerInstance | null>(null);
|
const drawerRef = ref<DrawerInstance | null>(null);
|
||||||
const loadedRef = ref<Nullable<boolean>>(false);
|
const loadedRef = ref<Nullable<boolean>>(false);
|
||||||
@ -41,23 +51,31 @@ export function useDrawer(): UseDrawerReturnType {
|
|||||||
uidRef.value = uuid;
|
uidRef.value = uuid;
|
||||||
drawerRef.value = drawerInstance;
|
drawerRef.value = drawerInstance;
|
||||||
loadedRef.value = true;
|
loadedRef.value = true;
|
||||||
|
|
||||||
|
drawerInstance.emitVisible = (visible: boolean, uid: number) => {
|
||||||
|
visibleData[uid] = visible;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInstance = () => {
|
const getInstance = () => {
|
||||||
const instance = unref(drawerRef);
|
const instance = unref(drawerRef);
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
throw new Error('instance is undefined!');
|
error('useDrawer instance is undefined!');
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
const methods: ReturnMethods = {
|
const methods: ReturnMethods = {
|
||||||
setDrawerProps: (props: Partial<DrawerProps>): void => {
|
setDrawerProps: (props: Partial<DrawerProps>): void => {
|
||||||
getInstance().setDrawerProps(props);
|
getInstance()?.setDrawerProps(props);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getVisible: computed((): boolean => {
|
||||||
|
return visibleData[~~unref(uidRef)];
|
||||||
|
}),
|
||||||
|
|
||||||
openDrawer: <T = any>(visible = true, data?: T, openOnSet = true): void => {
|
openDrawer: <T = any>(visible = true, data?: T, openOnSet = true): void => {
|
||||||
getInstance().setDrawerProps({
|
getInstance()?.setDrawerProps({
|
||||||
visible: visible,
|
visible: visible,
|
||||||
});
|
});
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@ -79,17 +97,18 @@ export function useDrawer(): UseDrawerReturnType {
|
|||||||
|
|
||||||
export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
|
export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
|
||||||
const drawerInstanceRef = ref<Nullable<DrawerInstance>>(null);
|
const drawerInstanceRef = ref<Nullable<DrawerInstance>>(null);
|
||||||
const currentInstall = getCurrentInstance();
|
const currentInstance = getCurrentInstance();
|
||||||
const uidRef = ref<string>('');
|
const uidRef = ref<string>('');
|
||||||
|
|
||||||
if (!currentInstall) {
|
if (!currentInstance) {
|
||||||
throw new Error('useDrawerInner instance is undefined!');
|
error('useDrawerInner instance is undefined!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInstance = () => {
|
const getInstance = () => {
|
||||||
const instance = unref(drawerInstanceRef);
|
const instance = unref(drawerInstanceRef);
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
throw new Error('useDrawerInner instance is undefined!');
|
error('useDrawerInner instance is undefined!');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
@ -102,7 +121,7 @@ export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
|
|||||||
|
|
||||||
uidRef.value = uuid;
|
uidRef.value = uuid;
|
||||||
drawerInstanceRef.value = modalInstance;
|
drawerInstanceRef.value = modalInstance;
|
||||||
currentInstall.emit('register', modalInstance, uuid);
|
currentInstance?.emit('register', modalInstance, uuid);
|
||||||
};
|
};
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@ -118,19 +137,22 @@ export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
|
|||||||
register,
|
register,
|
||||||
{
|
{
|
||||||
changeLoading: (loading = true) => {
|
changeLoading: (loading = true) => {
|
||||||
getInstance().setDrawerProps({ loading });
|
getInstance()?.setDrawerProps({ loading });
|
||||||
},
|
},
|
||||||
|
|
||||||
changeOkLoading: (loading = true) => {
|
changeOkLoading: (loading = true) => {
|
||||||
getInstance().setDrawerProps({ confirmLoading: loading });
|
getInstance()?.setDrawerProps({ confirmLoading: loading });
|
||||||
},
|
},
|
||||||
|
getVisible: computed((): boolean => {
|
||||||
|
return visibleData[~~unref(uidRef)];
|
||||||
|
}),
|
||||||
|
|
||||||
closeDrawer: () => {
|
closeDrawer: () => {
|
||||||
getInstance().setDrawerProps({ visible: false });
|
getInstance()?.setDrawerProps({ visible: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
setDrawerProps: (props: Partial<DrawerProps>) => {
|
setDrawerProps: (props: Partial<DrawerProps>) => {
|
||||||
getInstance().setDrawerProps(props);
|
getInstance()?.setDrawerProps(props);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-col
|
<a-col v-bind="actionColOpt" :style="{ textAlign: 'right' }" v-if="showActionButtonGroup">
|
||||||
v-bind="actionColOpt"
|
|
||||||
class="mb-2"
|
|
||||||
:style="{ textAlign: 'right' }"
|
|
||||||
v-if="showActionButtonGroup"
|
|
||||||
>
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<slot name="resetBefore" />
|
<slot name="resetBefore" />
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import './src/index.less';
|
import './src/index.less';
|
||||||
import { withInstall } from '../util';
|
import { withInstall } from '../util';
|
||||||
import BasicModal from './src/BasicModal';
|
import BasicModal from './src/BasicModal.vue';
|
||||||
|
|
||||||
withInstall(BasicModal);
|
withInstall(BasicModal);
|
||||||
|
|
||||||
export { BasicModal };
|
export { BasicModal };
|
||||||
export { useModalContext } from './src/useModalContext';
|
export { useModalContext } from './src/hooks/useModalContext';
|
||||||
export { useModal, useModalInner } from './src/useModal';
|
export { useModal, useModalInner } from './src/hooks/useModal';
|
||||||
export * from './src/types';
|
export * from './src/types';
|
||||||
|
@ -1,232 +0,0 @@
|
|||||||
import type { ModalProps, ModalMethods } from './types';
|
|
||||||
|
|
||||||
import { defineComponent, computed, ref, watch, unref, watchEffect, toRef } from 'vue';
|
|
||||||
|
|
||||||
import Modal from './Modal';
|
|
||||||
import { Button } from '/@/components/Button';
|
|
||||||
import ModalWrapper from './ModalWrapper';
|
|
||||||
import { BasicTitle } from '/@/components/Basic';
|
|
||||||
import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
|
||||||
|
|
||||||
import { getSlot, extendSlots } from '/@/utils/helper/tsxHelper';
|
|
||||||
import { isFunction } from '/@/utils/is';
|
|
||||||
import { deepMerge } from '/@/utils';
|
|
||||||
import { tryTsxEmit } from '/@/utils/helper/vueHelper';
|
|
||||||
|
|
||||||
import { basicProps } from './props';
|
|
||||||
import { useFullScreen } from './useFullScreen';
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'BasicModal',
|
|
||||||
props: basicProps,
|
|
||||||
emits: ['visible-change', 'height-change', 'cancel', 'ok', 'register'],
|
|
||||||
setup(props, { slots, emit, attrs }) {
|
|
||||||
const visibleRef = ref(false);
|
|
||||||
const propsRef = ref<Partial<ModalProps> | null>(null);
|
|
||||||
const modalWrapperRef = ref<ComponentRef>(null);
|
|
||||||
// modal Bottom and top height
|
|
||||||
const extHeightRef = ref(0);
|
|
||||||
// Unexpanded height of the popup
|
|
||||||
|
|
||||||
// Custom title component: get title
|
|
||||||
const getMergeProps = computed(
|
|
||||||
(): ModalProps => {
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
...(unref(propsRef) as any),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({
|
|
||||||
modalWrapperRef,
|
|
||||||
extHeightRef,
|
|
||||||
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'),
|
|
||||||
});
|
|
||||||
|
|
||||||
// modal component does not need title
|
|
||||||
const getProps = computed(
|
|
||||||
(): ModalProps => {
|
|
||||||
const opt = {
|
|
||||||
...unref(getMergeProps),
|
|
||||||
visible: unref(visibleRef),
|
|
||||||
title: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...opt,
|
|
||||||
wrapClassName: unref(getWrapClassName),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const getModalBindValue = computed((): any => {
|
|
||||||
return { ...attrs, ...unref(getProps) };
|
|
||||||
});
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
visibleRef.value = !!props.visible;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => unref(visibleRef),
|
|
||||||
(v) => {
|
|
||||||
emit('visible-change', v);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description: 渲染标题
|
|
||||||
*/
|
|
||||||
function renderTitle() {
|
|
||||||
const { helpMessage } = unref(getProps);
|
|
||||||
const { title } = unref(getMergeProps);
|
|
||||||
return (
|
|
||||||
<BasicTitle helpMessage={helpMessage}>
|
|
||||||
{() => (slots.title ? getSlot(slots, 'title') : title)}
|
|
||||||
</BasicTitle>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消事件
|
|
||||||
async function handleCancel(e: Event) {
|
|
||||||
e?.stopPropagation();
|
|
||||||
|
|
||||||
if (props.closeFunc && isFunction(props.closeFunc)) {
|
|
||||||
const isClose: boolean = await props.closeFunc();
|
|
||||||
visibleRef.value = !isClose;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleRef.value = false;
|
|
||||||
emit('cancel');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description: 设置modal参数
|
|
||||||
*/
|
|
||||||
function setModalProps(props: Partial<ModalProps>): void {
|
|
||||||
// Keep the last setModalProps
|
|
||||||
propsRef.value = deepMerge(unref(propsRef) || {}, props);
|
|
||||||
if (!Reflect.has(props, 'visible')) return;
|
|
||||||
visibleRef.value = !!props.visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderContent() {
|
|
||||||
type OmitWrapperType = Omit<
|
|
||||||
ModalProps,
|
|
||||||
'fullScreen' | 'modalFooterHeight' | 'visible' | 'loading'
|
|
||||||
>;
|
|
||||||
const { useWrapper, loading, wrapperProps } = unref(getProps);
|
|
||||||
if (!useWrapper) return getSlot(slots);
|
|
||||||
|
|
||||||
const showFooter = props.footer !== undefined && !props.footer ? 0 : undefined;
|
|
||||||
return (
|
|
||||||
<ModalWrapper
|
|
||||||
footerOffset={props.wrapperFooterOffset}
|
|
||||||
fullScreen={unref(fullScreenRef)}
|
|
||||||
ref={modalWrapperRef}
|
|
||||||
loading={loading}
|
|
||||||
visible={unref(visibleRef)}
|
|
||||||
modalFooterHeight={showFooter}
|
|
||||||
{...((wrapperProps as unknown) as OmitWrapperType)}
|
|
||||||
onGetExtHeight={(height: number) => {
|
|
||||||
extHeightRef.value = height;
|
|
||||||
}}
|
|
||||||
onHeightChange={(height: string) => {
|
|
||||||
emit('height-change', height);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{() => getSlot(slots)}
|
|
||||||
</ModalWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 底部按钮自定义实现,
|
|
||||||
function renderFooter() {
|
|
||||||
const {
|
|
||||||
showCancelBtn,
|
|
||||||
cancelButtonProps,
|
|
||||||
cancelText,
|
|
||||||
showOkBtn,
|
|
||||||
okType,
|
|
||||||
okText,
|
|
||||||
okButtonProps,
|
|
||||||
confirmLoading,
|
|
||||||
} = unref(getProps);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{getSlot(slots, 'insertFooter')}
|
|
||||||
{showCancelBtn && (
|
|
||||||
<Button {...cancelButtonProps} onClick={handleCancel}>
|
|
||||||
{() => cancelText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{getSlot(slots, 'centerdFooter')}
|
|
||||||
{showOkBtn && (
|
|
||||||
<Button
|
|
||||||
type={okType as any}
|
|
||||||
loading={confirmLoading}
|
|
||||||
onClick={() => {
|
|
||||||
emit('ok');
|
|
||||||
}}
|
|
||||||
{...okButtonProps}
|
|
||||||
>
|
|
||||||
{() => okText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{getSlot(slots, 'appendFooter')}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description: 关闭按钮
|
|
||||||
*/
|
|
||||||
function renderClose() {
|
|
||||||
const { canFullscreen } = unref(getProps);
|
|
||||||
|
|
||||||
const fullScreen = unref(fullScreenRef) ? (
|
|
||||||
<FullscreenExitOutlined role="full" onClick={handleFullScreen} />
|
|
||||||
) : (
|
|
||||||
<FullscreenOutlined role="close" onClick={handleFullScreen} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const cls = [
|
|
||||||
'custom-close-icon',
|
|
||||||
{
|
|
||||||
'can-full': canFullscreen,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={cls}>
|
|
||||||
{canFullscreen && fullScreen}
|
|
||||||
<CloseOutlined onClick={handleCancel} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const modalMethods: ModalMethods = {
|
|
||||||
setModalProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
tryTsxEmit((instance) => {
|
|
||||||
emit('register', modalMethods, instance.uid);
|
|
||||||
});
|
|
||||||
return () => (
|
|
||||||
<Modal onCancel={handleCancel} {...unref(getModalBindValue)}>
|
|
||||||
{{
|
|
||||||
footer: () => renderFooter(),
|
|
||||||
closeIcon: () => renderClose(),
|
|
||||||
title: () => renderTitle(),
|
|
||||||
...extendSlots(slots, ['default']),
|
|
||||||
default: () => renderContent(),
|
|
||||||
}}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
184
src/components/Modal/src/BasicModal.vue
Normal file
184
src/components/Modal/src/BasicModal.vue
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<Modal @cancel="handleCancel" v-bind="getBindValue">
|
||||||
|
<template #closeIcon v-if="!$slots.closeIcon">
|
||||||
|
<ModalClose
|
||||||
|
:canFullscreen="getProps.canFullscreen"
|
||||||
|
:fullScreen="fullScreenRef"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@fullscreen="handleFullScreen"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #title v-if="!$slots.title">
|
||||||
|
<ModalHeader :helpMessage="getProps.helpMessage" :title="getMergeProps.title" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer v-if="!$slots.footer">
|
||||||
|
<ModalFooter v-bind="getProps" @ok="handleOk" @cancel="handleCancel" />
|
||||||
|
</template>
|
||||||
|
<ModalWrapper
|
||||||
|
:useWrapper="getProps.useWrapper"
|
||||||
|
:footerOffset="wrapperFooterOffset"
|
||||||
|
:fullScreen="fullScreenRef"
|
||||||
|
ref="modalWrapperRef"
|
||||||
|
:loading="getProps.loading"
|
||||||
|
:visible="visibleRef"
|
||||||
|
:modalFooterHeight="footer !== undefined && !footer ? 0 : undefined"
|
||||||
|
v-bind="omit(getProps.wrapperProps, 'visible')"
|
||||||
|
@ext-height="handleExtHeight"
|
||||||
|
@height-change="handleHeightChange"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ModalWrapper>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ModalProps, ModalMethods } from './types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
unref,
|
||||||
|
watchEffect,
|
||||||
|
toRef,
|
||||||
|
getCurrentInstance,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import Modal from './components/Modal';
|
||||||
|
import ModalWrapper from './components/ModalWrapper.vue';
|
||||||
|
import ModalClose from './components/ModalClose.vue';
|
||||||
|
import ModalFooter from './components/ModalFooter.vue';
|
||||||
|
import ModalHeader from './components/ModalHeader.vue';
|
||||||
|
|
||||||
|
import { isFunction } from '/@/utils/is';
|
||||||
|
import { deepMerge } from '/@/utils';
|
||||||
|
|
||||||
|
import { basicProps } from './props';
|
||||||
|
import { useFullScreen } from './hooks/useModalFullScreen';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BasicModal',
|
||||||
|
components: { Modal, ModalWrapper, ModalClose, ModalFooter, ModalHeader },
|
||||||
|
props: basicProps,
|
||||||
|
emits: ['visible-change', 'height-change', 'cancel', 'ok', 'register'],
|
||||||
|
setup(props, { emit, attrs }) {
|
||||||
|
const visibleRef = ref(false);
|
||||||
|
const propsRef = ref<Partial<ModalProps> | null>(null);
|
||||||
|
const modalWrapperRef = ref<ComponentRef>(null);
|
||||||
|
// modal Bottom and top height
|
||||||
|
const extHeightRef = ref(0);
|
||||||
|
const modalMethods: ModalMethods = {
|
||||||
|
setModalProps,
|
||||||
|
emitVisible: undefined,
|
||||||
|
};
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
if (instance) {
|
||||||
|
emit('register', modalMethods, instance.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom title component: get title
|
||||||
|
const getMergeProps = computed(
|
||||||
|
(): ModalProps => {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
...(unref(propsRef) as any),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({
|
||||||
|
modalWrapperRef,
|
||||||
|
extHeightRef,
|
||||||
|
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// modal component does not need title
|
||||||
|
const getProps = computed(
|
||||||
|
(): ModalProps => {
|
||||||
|
const opt = {
|
||||||
|
...unref(getMergeProps),
|
||||||
|
visible: unref(visibleRef),
|
||||||
|
title: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...opt,
|
||||||
|
wrapClassName: unref(getWrapClassName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getBindValue = computed((): any => {
|
||||||
|
return { ...attrs, ...unref(getProps) };
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
visibleRef.value = !!props.visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => unref(visibleRef),
|
||||||
|
(v) => {
|
||||||
|
emit('visible-change', v);
|
||||||
|
instance && modalMethods.emitVisible?.(v, instance.uid);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 取消事件
|
||||||
|
async function handleCancel(e: Event) {
|
||||||
|
e?.stopPropagation();
|
||||||
|
|
||||||
|
if (props.closeFunc && isFunction(props.closeFunc)) {
|
||||||
|
const isClose: boolean = await props.closeFunc();
|
||||||
|
visibleRef.value = !isClose;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleRef.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 设置modal参数
|
||||||
|
*/
|
||||||
|
function setModalProps(props: Partial<ModalProps>): void {
|
||||||
|
// Keep the last setModalProps
|
||||||
|
propsRef.value = deepMerge(unref(propsRef) || {}, props);
|
||||||
|
if (!Reflect.has(props, 'visible')) return;
|
||||||
|
visibleRef.value = !!props.visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOk() {
|
||||||
|
emit('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHeightChange(height: string) {
|
||||||
|
emit('height-change', height);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExtHeight(height: number) {
|
||||||
|
extHeightRef.value = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleCancel,
|
||||||
|
getBindValue,
|
||||||
|
getProps,
|
||||||
|
handleFullScreen,
|
||||||
|
fullScreenRef,
|
||||||
|
getMergeProps,
|
||||||
|
handleOk,
|
||||||
|
visibleRef,
|
||||||
|
omit,
|
||||||
|
modalWrapperRef,
|
||||||
|
handleExtHeight,
|
||||||
|
handleHeightChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,161 +0,0 @@
|
|||||||
import type { ModalWrapperProps } from './types';
|
|
||||||
import type { CSSProperties } from 'vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
defineComponent,
|
|
||||||
computed,
|
|
||||||
ref,
|
|
||||||
watchEffect,
|
|
||||||
unref,
|
|
||||||
watch,
|
|
||||||
onMounted,
|
|
||||||
nextTick,
|
|
||||||
onUnmounted,
|
|
||||||
} from 'vue';
|
|
||||||
import { Spin } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
|
|
||||||
|
|
||||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
|
||||||
import { useElResize } from '/@/hooks/event/useElResize';
|
|
||||||
import { propTypes } from '/@/utils/propTypes';
|
|
||||||
import { createModalContext } from './useModalContext';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'ModalWrapper',
|
|
||||||
props: {
|
|
||||||
loading: propTypes.bool,
|
|
||||||
modalHeaderHeight: propTypes.number.def(50),
|
|
||||||
modalFooterHeight: propTypes.number.def(54),
|
|
||||||
minHeight: propTypes.number.def(200),
|
|
||||||
footerOffset: propTypes.number.def(0),
|
|
||||||
visible: propTypes.bool,
|
|
||||||
fullScreen: propTypes.bool,
|
|
||||||
},
|
|
||||||
emits: ['heightChange', 'getExtHeight'],
|
|
||||||
setup(props: ModalWrapperProps, { slots, emit }) {
|
|
||||||
const wrapperRef = ref<ElRef>(null);
|
|
||||||
const spinRef = ref<ComponentRef>(null);
|
|
||||||
const realHeightRef = ref(0);
|
|
||||||
|
|
||||||
let stopElResizeFn: Fn = () => {};
|
|
||||||
|
|
||||||
useWindowSizeFn(setModalHeight);
|
|
||||||
|
|
||||||
createModalContext({
|
|
||||||
redoModalHeight: setModalHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapStyle = computed(
|
|
||||||
(): CSSProperties => {
|
|
||||||
return {
|
|
||||||
minHeight: `${props.minHeight}px`,
|
|
||||||
height: `${unref(realHeightRef)}px`,
|
|
||||||
overflow: 'auto',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
setModalHeight();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.fullScreen,
|
|
||||||
(v) => {
|
|
||||||
!v && setModalHeight();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const { modalHeaderHeight, modalFooterHeight } = props;
|
|
||||||
emit('getExtHeight', modalHeaderHeight + modalFooterHeight);
|
|
||||||
listenElResize();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopElResizeFn && stopElResizeFn();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function setModalHeight() {
|
|
||||||
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度
|
|
||||||
// 加上这个,就必须在使用的时候传递父级的visible
|
|
||||||
if (!props.visible) return;
|
|
||||||
const wrapperRefDom = unref(wrapperRef);
|
|
||||||
if (!wrapperRefDom) return;
|
|
||||||
const bodyDom = wrapperRefDom.parentElement;
|
|
||||||
if (!bodyDom) return;
|
|
||||||
bodyDom.style.padding = '0';
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement;
|
|
||||||
if (!modalDom) return;
|
|
||||||
|
|
||||||
const modalRect = getComputedStyle(modalDom).top;
|
|
||||||
const modalTop = Number.parseInt(modalRect);
|
|
||||||
let maxHeight =
|
|
||||||
window.innerHeight -
|
|
||||||
modalTop * 2 +
|
|
||||||
(props.footerOffset! || 0) -
|
|
||||||
props.modalFooterHeight -
|
|
||||||
props.modalHeaderHeight;
|
|
||||||
|
|
||||||
// 距离顶部过进会出现滚动条
|
|
||||||
if (modalTop < 40) {
|
|
||||||
maxHeight -= 26;
|
|
||||||
}
|
|
||||||
await nextTick();
|
|
||||||
const spinEl = unref(spinRef);
|
|
||||||
|
|
||||||
if (!spinEl) return;
|
|
||||||
|
|
||||||
const spinContainerEl = spinEl.$el.querySelector('.ant-spin-container') as HTMLElement;
|
|
||||||
if (!spinContainerEl) return;
|
|
||||||
|
|
||||||
const realHeight = spinContainerEl.scrollHeight;
|
|
||||||
|
|
||||||
if (props.fullScreen) {
|
|
||||||
realHeightRef.value =
|
|
||||||
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight;
|
|
||||||
} else {
|
|
||||||
realHeightRef.value = realHeight > maxHeight ? maxHeight : realHeight + 16 + 30;
|
|
||||||
}
|
|
||||||
emit('heightChange', unref(realHeightRef));
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
const el = spinEl.$el;
|
|
||||||
if (el) {
|
|
||||||
el.style.height = `${unref(realHeightRef)}px`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function listenElResize() {
|
|
||||||
const wrapper = unref(wrapperRef);
|
|
||||||
if (!wrapper) return;
|
|
||||||
|
|
||||||
const container = wrapper.querySelector('.ant-spin-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const [start, stop] = useElResize(container, () => {
|
|
||||||
setModalHeight();
|
|
||||||
});
|
|
||||||
stopElResizeFn = stop;
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return (
|
|
||||||
<div ref={wrapperRef} style={unref(wrapStyle)}>
|
|
||||||
<Spin ref={spinRef} spinning={props.loading}>
|
|
||||||
{() => getSlot(slots)}
|
|
||||||
</Spin>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,16 +1,17 @@
|
|||||||
import { Modal } from 'ant-design-vue';
|
import { Modal } from 'ant-design-vue';
|
||||||
import { defineComponent, toRefs } from 'vue';
|
import { defineComponent, toRefs, unref } from 'vue';
|
||||||
import { basicProps } from './props';
|
import { basicProps } from '../props';
|
||||||
import { useModalDragMove } from './useModalDrag';
|
import { useModalDragMove } from '../hooks/useModalDrag';
|
||||||
|
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||||
import { extendSlots } from '/@/utils/helper/tsxHelper';
|
import { extendSlots } from '/@/utils/helper/tsxHelper';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Modal',
|
name: 'Modal',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: basicProps,
|
props: basicProps,
|
||||||
setup(props, { attrs, slots }) {
|
setup(props, { slots }) {
|
||||||
const { visible, draggable, destroyOnClose } = toRefs(props);
|
const { visible, draggable, destroyOnClose } = toRefs(props);
|
||||||
|
const attrs = useAttrs();
|
||||||
useModalDragMove({
|
useModalDragMove({
|
||||||
visible,
|
visible,
|
||||||
destroyOnClose,
|
destroyOnClose,
|
||||||
@ -18,7 +19,8 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const propsData = { ...attrs, ...props } as any;
|
const propsData = { ...unref(attrs), ...props } as Recordable;
|
||||||
|
|
||||||
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
|
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
|
||||||
};
|
};
|
||||||
},
|
},
|
98
src/components/Modal/src/components/ModalClose.vue
Normal file
98
src/components/Modal/src/components/ModalClose.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="getClass">
|
||||||
|
<template v-if="canFullscreen">
|
||||||
|
<FullscreenExitOutlined role="full" @click="handleFullScreen" v-if="fullScreen" />
|
||||||
|
|
||||||
|
<FullscreenOutlined role="close" @click="handleFullScreen" v-else />
|
||||||
|
</template>
|
||||||
|
<CloseOutlined @click="handleCancel" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed } from 'vue';
|
||||||
|
import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { useDesign } from '/@/hooks/web/useDesign';
|
||||||
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ModalClose',
|
||||||
|
components: { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined },
|
||||||
|
props: {
|
||||||
|
canFullscreen: propTypes.bool.def(true),
|
||||||
|
fullScreen: propTypes.bool,
|
||||||
|
},
|
||||||
|
emits: ['cancel', 'fullscreen'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const { prefixCls } = useDesign('basic-modal-close');
|
||||||
|
|
||||||
|
const getClass = computed(() => {
|
||||||
|
return [
|
||||||
|
prefixCls,
|
||||||
|
`${prefixCls}--custom`,
|
||||||
|
{
|
||||||
|
[`${prefixCls}--can-full`]: props.canFullscreen,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
function handleFullScreen(e: Event) {
|
||||||
|
e?.stopPropagation();
|
||||||
|
e?.preventDefault();
|
||||||
|
emit('fullscreen');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getClass,
|
||||||
|
prefixCls,
|
||||||
|
handleCancel,
|
||||||
|
handleFullScreen,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less">
|
||||||
|
@import (reference) '../../../../design/index.less';
|
||||||
|
@prefix-cls: ~'@{namespace}-basic-modal-close';
|
||||||
|
.@{prefix-cls} {
|
||||||
|
display: flex;
|
||||||
|
height: 95%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin-left: 48px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--can-full {
|
||||||
|
> span {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(&--can-full) {
|
||||||
|
> span:nth-child(1) {
|
||||||
|
&:hover {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& span:nth-child(1) {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& span:nth-child(2) {
|
||||||
|
&:hover {
|
||||||
|
color: @error-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
39
src/components/Modal/src/components/ModalFooter.vue
Normal file
39
src/components/Modal/src/components/ModalFooter.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot name="insertFooter" />
|
||||||
|
<a-button v-bind="cancelButtonProps" @click="handleCancel" v-if="showCancelBtn">
|
||||||
|
{{ cancelText }}
|
||||||
|
</a-button>
|
||||||
|
<slot name="centerFooter" />
|
||||||
|
<a-button
|
||||||
|
:type="okType"
|
||||||
|
@click="handleOk"
|
||||||
|
:loading="confirmLoading"
|
||||||
|
v-bind="okButtonProps"
|
||||||
|
v-if="showOkBtn"
|
||||||
|
>
|
||||||
|
{{ okText }}
|
||||||
|
</a-button>
|
||||||
|
<slot name="appendFooter" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import { basicProps } from '../props';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BasicModalFooter',
|
||||||
|
props: basicProps,
|
||||||
|
emits: ['ok', 'cancel'],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
function handleOk() {
|
||||||
|
emit('ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
emit('cancel');
|
||||||
|
}
|
||||||
|
return { handleOk, handleCancel };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
22
src/components/Modal/src/components/ModalHeader.vue
Normal file
22
src/components/Modal/src/components/ModalHeader.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<BasicTitle :helpMessage="helpMessage">
|
||||||
|
{{ title }}
|
||||||
|
</BasicTitle>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { BasicTitle } from '/@/components/Basic';
|
||||||
|
|
||||||
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BasicModalHeader',
|
||||||
|
components: { BasicTitle },
|
||||||
|
props: {
|
||||||
|
helpMessage: {
|
||||||
|
type: [String, Array] as PropType<string | string[]>,
|
||||||
|
},
|
||||||
|
title: propTypes.string,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
152
src/components/Modal/src/components/ModalWrapper.vue
Normal file
152
src/components/Modal/src/components/ModalWrapper.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<ScrollContainer ref="wrapperRef" :style="wrapStyle">
|
||||||
|
<div ref="spinRef" :style="spinStyle" v-loading="loading" :loading-tip="loadingTip">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</ScrollContainer>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import type { ModalWrapperProps } from '../types';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
defineComponent,
|
||||||
|
computed,
|
||||||
|
ref,
|
||||||
|
watchEffect,
|
||||||
|
unref,
|
||||||
|
watch,
|
||||||
|
onMounted,
|
||||||
|
nextTick,
|
||||||
|
onUnmounted,
|
||||||
|
} from 'vue';
|
||||||
|
import { Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
|
||||||
|
import { ScrollContainer } from '/@/components/Container';
|
||||||
|
|
||||||
|
// import { useElResize } from '/@/hooks/event/useElResize';
|
||||||
|
import { propTypes } from '/@/utils/propTypes';
|
||||||
|
import { createModalContext } from '../hooks/useModalContext';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ModalWrapper',
|
||||||
|
components: { Spin, ScrollContainer },
|
||||||
|
props: {
|
||||||
|
loading: propTypes.bool,
|
||||||
|
useWrapper: propTypes.bool.def(true),
|
||||||
|
modalHeaderHeight: propTypes.number.def(50),
|
||||||
|
modalFooterHeight: propTypes.number.def(54),
|
||||||
|
minHeight: propTypes.number.def(200),
|
||||||
|
footerOffset: propTypes.number.def(0),
|
||||||
|
visible: propTypes.bool,
|
||||||
|
fullScreen: propTypes.bool,
|
||||||
|
loadingTip: propTypes.string,
|
||||||
|
},
|
||||||
|
emits: ['height-change', 'ext-height'],
|
||||||
|
setup(props: ModalWrapperProps, { emit }) {
|
||||||
|
const wrapperRef = ref<ComponentRef>(null);
|
||||||
|
const spinRef = ref<ElRef>(null);
|
||||||
|
const realHeightRef = ref(0);
|
||||||
|
|
||||||
|
let stopElResizeFn: Fn = () => {};
|
||||||
|
|
||||||
|
useWindowSizeFn(setModalHeight);
|
||||||
|
|
||||||
|
createModalContext({
|
||||||
|
redoModalHeight: setModalHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapStyle = computed(
|
||||||
|
(): CSSProperties => {
|
||||||
|
return {
|
||||||
|
minHeight: `${props.minHeight}px`,
|
||||||
|
height: `${unref(realHeightRef)}px`,
|
||||||
|
// overflow: 'auto',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const spinStyle = computed(
|
||||||
|
(): CSSProperties => {
|
||||||
|
return {
|
||||||
|
// padding 28
|
||||||
|
height: `${unref(realHeightRef) - 28}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
props.useWrapper && setModalHeight();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.fullScreen,
|
||||||
|
() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setModalHeight();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const { modalHeaderHeight, modalFooterHeight } = props;
|
||||||
|
emit('ext-height', modalHeaderHeight + modalFooterHeight);
|
||||||
|
// listenElResize();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopElResizeFn && stopElResizeFn();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function setModalHeight() {
|
||||||
|
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度
|
||||||
|
// 加上这个,就必须在使用的时候传递父级的visible
|
||||||
|
if (!props.visible) return;
|
||||||
|
const wrapperRefDom = unref(wrapperRef);
|
||||||
|
if (!wrapperRefDom) return;
|
||||||
|
const bodyDom = wrapperRefDom.$el.parentElement;
|
||||||
|
if (!bodyDom) return;
|
||||||
|
bodyDom.style.padding = '0';
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement;
|
||||||
|
if (!modalDom) return;
|
||||||
|
|
||||||
|
const modalRect = getComputedStyle(modalDom).top;
|
||||||
|
const modalTop = Number.parseInt(modalRect);
|
||||||
|
let maxHeight =
|
||||||
|
window.innerHeight -
|
||||||
|
modalTop * 2 +
|
||||||
|
(props.footerOffset! || 0) -
|
||||||
|
props.modalFooterHeight -
|
||||||
|
props.modalHeaderHeight;
|
||||||
|
|
||||||
|
// 距离顶部过进会出现滚动条
|
||||||
|
if (modalTop < 40) {
|
||||||
|
maxHeight -= 26;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
const spinEl = unref(spinRef);
|
||||||
|
|
||||||
|
if (!spinEl) return;
|
||||||
|
|
||||||
|
const realHeight = spinEl.scrollHeight;
|
||||||
|
|
||||||
|
if (props.fullScreen) {
|
||||||
|
realHeightRef.value =
|
||||||
|
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight;
|
||||||
|
} else {
|
||||||
|
realHeightRef.value = realHeight > maxHeight ? maxHeight : realHeight + 16 + 30;
|
||||||
|
}
|
||||||
|
emit('height-change', unref(realHeightRef));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { wrapStyle, wrapperRef, spinRef, spinStyle };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -4,7 +4,7 @@ import type {
|
|||||||
ModalProps,
|
ModalProps,
|
||||||
ReturnMethods,
|
ReturnMethods,
|
||||||
UseModalInnerReturnType,
|
UseModalInnerReturnType,
|
||||||
} from './types';
|
} from '../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
@ -19,16 +19,18 @@ import {
|
|||||||
import { isProdMode } from '/@/utils/env';
|
import { isProdMode } from '/@/utils/env';
|
||||||
import { isFunction } from '/@/utils/is';
|
import { isFunction } from '/@/utils/is';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
import { tryOnUnmounted, isInSetup } from '/@/utils/helper/vueHelper';
|
||||||
|
import { error } from '/@/utils/log';
|
||||||
|
import { computed } from 'vue';
|
||||||
const dataTransferRef = reactive<any>({});
|
const dataTransferRef = reactive<any>({});
|
||||||
|
|
||||||
|
const visibleData = reactive<{ [key: number]: boolean }>({});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: Applicable to independent modal and call outside
|
* @description: Applicable to independent modal and call outside
|
||||||
*/
|
*/
|
||||||
export function useModal(): UseModalReturnType {
|
export function useModal(): UseModalReturnType {
|
||||||
if (!getCurrentInstance()) {
|
isInSetup();
|
||||||
throw new Error('Please put useModal function in the setup function!');
|
|
||||||
}
|
|
||||||
const modalRef = ref<Nullable<ModalMethods>>(null);
|
const modalRef = ref<Nullable<ModalMethods>>(null);
|
||||||
const loadedRef = ref<Nullable<boolean>>(false);
|
const loadedRef = ref<Nullable<boolean>>(false);
|
||||||
const uidRef = ref<string>('');
|
const uidRef = ref<string>('');
|
||||||
@ -45,23 +47,29 @@ export function useModal(): UseModalReturnType {
|
|||||||
if (unref(loadedRef) && isProdMode() && modalMethod === unref(modalRef)) return;
|
if (unref(loadedRef) && isProdMode() && modalMethod === unref(modalRef)) return;
|
||||||
|
|
||||||
modalRef.value = modalMethod;
|
modalRef.value = modalMethod;
|
||||||
|
modalMethod.emitVisible = (visible: boolean, uid: number) => {
|
||||||
|
visibleData[uid] = visible;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInstance = () => {
|
const getInstance = () => {
|
||||||
const instance = unref(modalRef);
|
const instance = unref(modalRef);
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
throw new Error('instance is undefined!');
|
error('useModal instance is undefined!');
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
|
|
||||||
const methods: ReturnMethods = {
|
const methods: ReturnMethods = {
|
||||||
setModalProps: (props: Partial<ModalProps>): void => {
|
setModalProps: (props: Partial<ModalProps>): void => {
|
||||||
getInstance().setModalProps(props);
|
getInstance()?.setModalProps(props);
|
||||||
},
|
},
|
||||||
|
getVisible: computed((): boolean => {
|
||||||
|
return visibleData[~~unref(uidRef)];
|
||||||
|
}),
|
||||||
|
|
||||||
openModal: <T = any>(visible = true, data?: T, openOnSet = true): void => {
|
openModal: <T = any>(visible = true, data?: T, openOnSet = true): void => {
|
||||||
getInstance().setModalProps({
|
getInstance()?.setModalProps({
|
||||||
visible: visible,
|
visible: visible,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -83,20 +91,16 @@ export function useModal(): UseModalReturnType {
|
|||||||
|
|
||||||
export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
|
export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
|
||||||
const modalInstanceRef = ref<Nullable<ModalMethods>>(null);
|
const modalInstanceRef = ref<Nullable<ModalMethods>>(null);
|
||||||
const currentInstall = getCurrentInstance();
|
const currentInstance = getCurrentInstance();
|
||||||
const uidRef = ref<string>('');
|
const uidRef = ref<string>('');
|
||||||
|
|
||||||
if (!currentInstall) {
|
|
||||||
throw new Error('instance is undefined!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// currentInstall.type.emits = [...currentInstall.type.emits, 'register'];
|
// currentInstall.type.emits = [...currentInstall.type.emits, 'register'];
|
||||||
// Object.assign(currentInstall.type.emits, ['register']);
|
// Object.assign(currentInstall.type.emits, ['register']);
|
||||||
|
|
||||||
const getInstance = () => {
|
const getInstance = () => {
|
||||||
const instance = unref(modalInstanceRef);
|
const instance = unref(modalInstanceRef);
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
throw new Error('instance is undefined!');
|
error('useModalInner instance is undefined!');
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
};
|
};
|
||||||
@ -108,7 +112,7 @@ export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
|
|||||||
});
|
});
|
||||||
uidRef.value = uuid;
|
uidRef.value = uuid;
|
||||||
modalInstanceRef.value = modalInstance;
|
modalInstanceRef.value = modalInstance;
|
||||||
currentInstall.emit('register', modalInstance, uuid);
|
currentInstance?.emit('register', modalInstance, uuid);
|
||||||
};
|
};
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@ -124,19 +128,22 @@ export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
|
|||||||
register,
|
register,
|
||||||
{
|
{
|
||||||
changeLoading: (loading = true) => {
|
changeLoading: (loading = true) => {
|
||||||
getInstance().setModalProps({ loading });
|
getInstance()?.setModalProps({ loading });
|
||||||
},
|
},
|
||||||
|
getVisible: computed((): boolean => {
|
||||||
|
return visibleData[~~unref(uidRef)];
|
||||||
|
}),
|
||||||
|
|
||||||
changeOkLoading: (loading = true) => {
|
changeOkLoading: (loading = true) => {
|
||||||
getInstance().setModalProps({ confirmLoading: loading });
|
getInstance()?.setModalProps({ confirmLoading: loading });
|
||||||
},
|
},
|
||||||
|
|
||||||
closeModal: () => {
|
closeModal: () => {
|
||||||
getInstance().setModalProps({ visible: false });
|
getInstance()?.setModalProps({ visible: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
setModalProps: (props: Partial<ModalProps>) => {
|
setModalProps: (props: Partial<ModalProps>) => {
|
||||||
getInstance().setModalProps(props);
|
getInstance()?.setModalProps(props);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
@ -21,9 +21,12 @@
|
|||||||
width: 520px;
|
width: 520px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|
||||||
.ant-spin-nested-loading {
|
.scroll-container {
|
||||||
padding: 16px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
// .ant-spin-nested-loading {
|
||||||
|
// padding: 16px;
|
||||||
|
// }
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -35,46 +38,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-close-icon {
|
|
||||||
display: flex;
|
|
||||||
height: 95%;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
margin-left: 48px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.can-full {
|
|
||||||
> span {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.can-full) {
|
|
||||||
> span:nth-child(1) {
|
|
||||||
&:hover {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& span:nth-child(1) {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: @primary-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& span:nth-child(2) {
|
|
||||||
&:hover {
|
|
||||||
color: @error-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-modal-body {
|
.ant-modal-body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -96,8 +59,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-footer {
|
&-footer {
|
||||||
// padding: 10px 26px 26px 16px;
|
|
||||||
|
|
||||||
button + button {
|
button + button {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import type { PropType } from 'vue';
|
import type { PropType, CSSProperties } from 'vue';
|
||||||
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
||||||
|
|
||||||
import { useI18n } from '/@/hooks/web/useI18n';
|
import { useI18n } from '/@/hooks/web/useI18n';
|
||||||
import { propTypes } from '/@/utils/propTypes';
|
import { propTypes, VueNode } from '/@/utils/propTypes';
|
||||||
|
import type { ModalWrapperProps } from './types';
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
export const modalProps = {
|
export const modalProps = {
|
||||||
@ -26,6 +27,7 @@ export const basicProps = Object.assign({}, modalProps, {
|
|||||||
// Whether to setting wrapper
|
// Whether to setting wrapper
|
||||||
useWrapper: propTypes.bool.def(true),
|
useWrapper: propTypes.bool.def(true),
|
||||||
loading: propTypes.bool,
|
loading: propTypes.bool,
|
||||||
|
loadingTip: propTypes.string,
|
||||||
/**
|
/**
|
||||||
* @description: Show close button
|
* @description: Show close button
|
||||||
*/
|
*/
|
||||||
@ -35,65 +37,44 @@ export const basicProps = Object.assign({}, modalProps, {
|
|||||||
*/
|
*/
|
||||||
showOkBtn: propTypes.bool.def(true),
|
showOkBtn: propTypes.bool.def(true),
|
||||||
|
|
||||||
wrapperProps: Object as PropType<any>,
|
wrapperProps: Object as PropType<Partial<ModalWrapperProps>>,
|
||||||
|
|
||||||
afterClose: Function as PropType<() => Promise<any>>,
|
afterClose: Function as PropType<() => Promise<VueNode>>,
|
||||||
|
|
||||||
bodyStyle: Object as PropType<any>,
|
bodyStyle: Object as PropType<CSSProperties>,
|
||||||
|
|
||||||
closable: {
|
closable: propTypes.bool.def(true),
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
closeIcon: Object as PropType<any>,
|
closeIcon: Object as PropType<VueNode>,
|
||||||
|
|
||||||
confirmLoading: Boolean as PropType<boolean>,
|
confirmLoading: propTypes.bool,
|
||||||
|
|
||||||
destroyOnClose: Boolean as PropType<boolean>,
|
destroyOnClose: propTypes.bool,
|
||||||
|
|
||||||
footer: Object as PropType<any>,
|
footer: Object as PropType<VueNode>,
|
||||||
|
|
||||||
getContainer: Function as PropType<() => any>,
|
getContainer: Function as PropType<() => any>,
|
||||||
|
|
||||||
mask: {
|
mask: propTypes.bool.def(true),
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
maskClosable: {
|
maskClosable: propTypes.bool.def(true),
|
||||||
type: Boolean as PropType<boolean>,
|
keyboard: propTypes.bool.def(true),
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
keyboard: {
|
|
||||||
type: Boolean as PropType<boolean>,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
maskStyle: Object as PropType<any>,
|
maskStyle: Object as PropType<CSSProperties>,
|
||||||
|
|
||||||
okType: {
|
okType: propTypes.string.def('primary'),
|
||||||
type: String as PropType<string>,
|
|
||||||
default: 'primary',
|
|
||||||
},
|
|
||||||
|
|
||||||
okButtonProps: Object as PropType<ButtonProps>,
|
okButtonProps: Object as PropType<ButtonProps>,
|
||||||
|
|
||||||
cancelButtonProps: Object as PropType<ButtonProps>,
|
cancelButtonProps: Object as PropType<ButtonProps>,
|
||||||
|
|
||||||
title: {
|
title: propTypes.string,
|
||||||
type: String as PropType<string>,
|
|
||||||
},
|
|
||||||
|
|
||||||
visible: Boolean as PropType<boolean>,
|
visible: propTypes.bool,
|
||||||
|
|
||||||
width: [String, Number] as PropType<string | number>,
|
width: [String, Number] as PropType<string | number>,
|
||||||
|
|
||||||
wrapClassName: {
|
wrapClassName: propTypes.string,
|
||||||
type: String as PropType<string>,
|
|
||||||
},
|
|
||||||
|
|
||||||
zIndex: {
|
zIndex: propTypes.number,
|
||||||
type: Number as PropType<number>,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
|
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
|
||||||
import type { CSSProperties, VNodeChild } from 'vue';
|
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
|
||||||
/**
|
/**
|
||||||
* @description: 弹窗对外暴露的方法
|
* @description: 弹窗对外暴露的方法
|
||||||
*/
|
*/
|
||||||
export interface ModalMethods {
|
export interface ModalMethods {
|
||||||
setModalProps: (props: Partial<ModalProps>) => void;
|
setModalProps: (props: Partial<ModalProps>) => void;
|
||||||
|
emitVisible?: (visible: boolean, uid: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RegisterFn = (modalMethods: ModalMethods, uuid?: string) => void;
|
export type RegisterFn = (modalMethods: ModalMethods, uuid?: string) => void;
|
||||||
|
|
||||||
export interface ReturnMethods extends ModalMethods {
|
export interface ReturnMethods extends ModalMethods {
|
||||||
openModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void;
|
openModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void;
|
||||||
|
getVisible?: ComputedRef<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseModalReturnType = [RegisterFn, ReturnMethods];
|
export type UseModalReturnType = [RegisterFn, ReturnMethods];
|
||||||
@ -19,6 +21,7 @@ export interface ReturnInnerMethods extends ModalMethods {
|
|||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
changeLoading: (loading: boolean) => void;
|
changeLoading: (loading: boolean) => void;
|
||||||
changeOkLoading: (loading: boolean) => void;
|
changeOkLoading: (loading: boolean) => void;
|
||||||
|
getVisible?: ComputedRef<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UseModalInnerReturnType = [RegisterFn, ReturnInnerMethods];
|
export type UseModalInnerReturnType = [RegisterFn, ReturnInnerMethods];
|
||||||
@ -38,6 +41,7 @@ export interface ModalProps {
|
|||||||
useWrapper: boolean;
|
useWrapper: boolean;
|
||||||
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
loadingTip?: string;
|
||||||
|
|
||||||
wrapperProps: Omit<ModalWrapperProps, 'loading'>;
|
wrapperProps: Omit<ModalWrapperProps, 'loading'>;
|
||||||
|
|
||||||
@ -193,4 +197,5 @@ export interface ModalWrapperProps {
|
|||||||
minHeight: number;
|
minHeight: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
|
useWrapper: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getCurrentInstance, reactive, shallowRef, watchEffect } from 'vue';
|
import { getCurrentInstance, reactive, shallowRef, watchEffect } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
interface Params {
|
interface Params {
|
||||||
excludeListeners?: boolean;
|
excludeListeners?: boolean;
|
||||||
excludeKeys?: string[];
|
excludeKeys?: string[];
|
||||||
@ -12,7 +12,7 @@ export function entries<T>(obj: Hash<T>): [string, T][] {
|
|||||||
return Object.keys(obj).map((key: string) => [key, obj[key]]);
|
return Object.keys(obj).map((key: string) => [key, obj[key]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAttrs(params: Params = {}) {
|
export function useAttrs(params: Params = {}): Ref<Recordable> | {} {
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
if (!instance) return {};
|
if (!instance) return {};
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ const setting: ProjectConfig = {
|
|||||||
showLogo: true,
|
showLogo: true,
|
||||||
|
|
||||||
// Whether to show footer
|
// Whether to show footer
|
||||||
showFooter: true,
|
showFooter: false,
|
||||||
|
|
||||||
// locale setting
|
// locale setting
|
||||||
locale: {
|
locale: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CSSProperties, VNodeChild } from 'vue';
|
import { CSSProperties, VNodeChild } from 'vue';
|
||||||
import { createTypes, VueTypeValidableDef, VueTypesInterface } from 'vue-types';
|
import { createTypes, VueTypeValidableDef, VueTypesInterface } from 'vue-types';
|
||||||
|
|
||||||
type VueNode = VNodeChild | JSX.Element;
|
export type VueNode = VNodeChild | JSX.Element;
|
||||||
|
|
||||||
type PropTypes = VueTypesInterface & {
|
type PropTypes = VueTypesInterface & {
|
||||||
readonly style: VueTypeValidableDef<CSSProperties>;
|
readonly style: VueTypeValidableDef<CSSProperties>;
|
||||||
|
@ -1,6 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicDrawer v-bind="$attrs" title="Modal Title" width="50%" showFooter>
|
<BasicDrawer v-bind="$attrs" title="Modal Title" width="50%" showFooter @ok="handleOk">
|
||||||
<p class="h-20" v-for="index in 40" :key="index">根据屏幕高度自适应</p>
|
<p class="h-20" v-for="index in 40" :key="index">根据屏幕高度自适应</p>
|
||||||
|
<template #insertFooter>
|
||||||
|
<a-button> btn</a-button>
|
||||||
|
</template>
|
||||||
|
<template #centerFooter>
|
||||||
|
<a-button> btn2</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #appendFooter>
|
||||||
|
<a-button> btn3</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- <template #footer>
|
||||||
|
<a-button> customerFooter</a-button>
|
||||||
|
</template> -->
|
||||||
</BasicDrawer>
|
</BasicDrawer>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -9,7 +23,13 @@
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { BasicDrawer },
|
components: { BasicDrawer },
|
||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {
|
||||||
|
handleOk: () => {
|
||||||
|
console.log('=====================');
|
||||||
|
console.log('ok');
|
||||||
|
console.log('======================');
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicDrawer v-bind="$attrs" :isDetail="true" title="Drawer Title5">
|
<BasicDrawer v-bind="$attrs" :isDetail="true" title="Drawer Title5">
|
||||||
<p class="h-20">Content Message</p>
|
<p class="h-20">Content Message</p>
|
||||||
|
<template #titleToolbar> toolbar </template>
|
||||||
</BasicDrawer>
|
</BasicDrawer>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -8,6 +9,5 @@
|
|||||||
import { BasicDrawer } from '/@/components/Drawer';
|
import { BasicDrawer } from '/@/components/Drawer';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { BasicDrawer },
|
components: { BasicDrawer },
|
||||||
setup() {},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
<Alert message="使用 useDrawer 进行抽屉操作" show-icon />
|
<Alert message="使用 useDrawer 进行抽屉操作" show-icon />
|
||||||
<a-button type="primary" class="my-4" @click="openDrawerLoading">打开Drawer</a-button>
|
<a-button type="primary" class="my-4" @click="openDrawerLoading">打开Drawer</a-button>
|
||||||
|
|
||||||
<Alert message="内外同时同时显示隐藏" show-icon />
|
<Alert message="内外同时控制显示隐藏" show-icon />
|
||||||
<a-button type="primary" class="my-4" @click="openDrawer2">打开Drawer</a-button>
|
<a-button type="primary" class="my-4" @click="openDrawer2(true)">打开Drawer</a-button>
|
||||||
<Alert message="自适应高度/显示footer" show-icon />
|
<Alert message="自适应高度/显示footer" show-icon />
|
||||||
<a-button type="primary" class="my-4" @click="openDrawer3">打开Drawer</a-button>
|
<a-button type="primary" class="my-4" @click="openDrawer3(true)">打开Drawer</a-button>
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
message="内外数据交互,外部通过 transferModalData 发送,内部通过 receiveDrawerDataRef 接收。该数据具有响应式"
|
message="内外数据交互,外部通过 transferModalData 发送,内部通过 receiveDrawerDataRef 接收。该数据具有响应式"
|
||||||
@ -14,7 +14,7 @@
|
|||||||
/>
|
/>
|
||||||
<a-button type="primary" class="my-4" @click="send">打开Drawer并传递数据</a-button>
|
<a-button type="primary" class="my-4" @click="send">打开Drawer并传递数据</a-button>
|
||||||
<Alert message="详情页模式" show-icon />
|
<Alert message="详情页模式" show-icon />
|
||||||
<a-button type="primary" class="my-4" @click="openDrawer5">打开详情Drawer</a-button>
|
<a-button type="primary" class="my-4" @click="openDrawer5(true)">打开详情Drawer</a-button>
|
||||||
<Drawer1 @register="register1" />
|
<Drawer1 @register="register1" />
|
||||||
<Drawer2 @register="register2" />
|
<Drawer2 @register="register2" />
|
||||||
<Drawer3 @register="register3" />
|
<Drawer3 @register="register3" />
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<a-input placeholder="请输入" />
|
<a-input placeholder="请输入" />
|
||||||
</CollapseContainer>
|
</CollapseContainer>
|
||||||
|
|
||||||
<CollapseContainer class="mt-4 px-4" title="标签页操作">
|
<CollapseContainer class="mt-4" title="标签页操作">
|
||||||
<a-button class="mr-2" @click="closeAll">关闭所有</a-button>
|
<a-button class="mr-2" @click="closeAll">关闭所有</a-button>
|
||||||
<a-button class="mr-2" @click="closeLeft">关闭左侧</a-button>
|
<a-button class="mr-2" @click="closeLeft">关闭左侧</a-button>
|
||||||
<a-button class="mr-2" @click="closeRight">关闭右侧</a-button>
|
<a-button class="mr-2" @click="closeRight">关闭右侧</a-button>
|
||||||
|
Loading…
Reference in New Issue
Block a user