mirror of
https://github.com/vbenjs/gf-vben-admin.git
synced 2025-01-23 11:50:20 +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: 新增`autoFocusFirstItem`配置。用于配置是否聚焦表单第一个输入框
|
||||
|
||||
### ⚡ Performance Improvements
|
||||
|
||||
- 优化`modal`与`drawer`滚动条组件
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- 修复混合模式下滚动条丢失问题
|
||||
@ -21,6 +25,7 @@
|
||||
- 修复路由类型错误
|
||||
- 修复菜单分割时权限失效问题
|
||||
- 关闭多标签页时 iframe 提前加载
|
||||
- 修复`modal`与`drawer`已知问题
|
||||
|
||||
## 2.0.0-rc.14 (2020-12-15)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition-group
|
||||
class="lazy-container"
|
||||
:class="prefixCls"
|
||||
v-bind="$attrs"
|
||||
ref="elRef"
|
||||
:name="transitionName"
|
||||
@ -25,6 +25,7 @@
|
||||
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
||||
import { useIntersectionObserver } from '/@/hooks/event/useIntersectionObserver';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
interface State {
|
||||
isInit: boolean;
|
||||
@ -70,6 +71,8 @@
|
||||
intersectionObserverInstance: null,
|
||||
});
|
||||
|
||||
const { prefixCls } = useDesign('lazy-container');
|
||||
|
||||
onMounted(() => {
|
||||
immediateInit();
|
||||
initIntersectionObserver();
|
||||
@ -129,13 +132,17 @@
|
||||
}
|
||||
return {
|
||||
elRef,
|
||||
prefixCls,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.lazy-container {
|
||||
@import (reference) '../../../design/index.less';
|
||||
@prefix-cls: ~'@{namespace}-lazy-container';
|
||||
|
||||
.@{prefix-cls} {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="collapse-container p-2">
|
||||
<CollapseHeader v-bind="$props" :show="show" @expand="handleExpand">
|
||||
<div :class="['p-2', prefixCls]">
|
||||
<CollapseHeader v-bind="$props" :prefixCls="prefixCls" :show="show" @expand="handleExpand">
|
||||
<template #title>
|
||||
<slot name="title" />
|
||||
</template>
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
<CollapseTransition :enable="canExpan">
|
||||
<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">
|
||||
<slot />
|
||||
<template #skeleton>
|
||||
@ -35,6 +35,7 @@
|
||||
// hook
|
||||
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CollapseContainer',
|
||||
@ -64,6 +65,9 @@
|
||||
},
|
||||
setup(props) {
|
||||
const show = ref(true);
|
||||
|
||||
const { prefixCls } = useDesign('collapse-container');
|
||||
|
||||
/**
|
||||
* @description: Handling development events
|
||||
*/
|
||||
@ -77,20 +81,20 @@
|
||||
return {
|
||||
show,
|
||||
handleExpand,
|
||||
prefixCls,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.collapse-container {
|
||||
@import (reference) '../../../../design/index.less';
|
||||
@prefix-cls: ~'@{namespace}-collapse-container';
|
||||
|
||||
.@{prefix-cls} {
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.no-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="collapse-container__header">
|
||||
<div :class="`${prefixCls}__header`">
|
||||
<BasicTitle :helpMessage="$attrs.helpMessage">
|
||||
<template v-if="$attrs.title">
|
||||
{{ $attrs.title }}
|
||||
@ -9,7 +9,7 @@
|
||||
</template>
|
||||
</BasicTitle>
|
||||
|
||||
<div class="collapse-container__action">
|
||||
<div :class="`${prefixCls}__action`">
|
||||
<slot name="action" />
|
||||
<BasicArrow v-if="$attrs.canExpan" top :expand="$attrs.show" @click="$emit('expand')" />
|
||||
</div>
|
||||
@ -21,5 +21,8 @@
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
components: { BasicArrow, BasicTitle },
|
||||
props: {
|
||||
prefixCls: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -24,7 +24,7 @@ export default defineComponent({
|
||||
const getMergeProps = computed(() => {
|
||||
return {
|
||||
...props,
|
||||
...(unref(propsRef) as any),
|
||||
...(unref(propsRef) as Recordable),
|
||||
} as DescOptions;
|
||||
});
|
||||
|
||||
|
@ -13,7 +13,7 @@ export default {
|
||||
bordered: propTypes.bool.def(true),
|
||||
|
||||
column: {
|
||||
type: [Number, Object] as PropType<number | any>,
|
||||
type: [Number, Object] as PropType<number | Recordable>,
|
||||
default: () => {
|
||||
return { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 };
|
||||
},
|
||||
|
@ -15,7 +15,10 @@ export interface DescItem {
|
||||
span?: number;
|
||||
show?: (...arg: any) => boolean;
|
||||
// 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 {
|
||||
@ -30,7 +33,7 @@ export interface DescOptions extends DescriptionsProps {
|
||||
* 数据
|
||||
* @type object
|
||||
*/
|
||||
data: any;
|
||||
data: Recordable;
|
||||
/**
|
||||
* Built-in CollapseContainer component configuration
|
||||
* @type CollapseContainerOptions
|
||||
|
@ -19,7 +19,7 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType
|
||||
|
||||
const methods: DescInstance = {
|
||||
setDescProps: (descProps: Partial<DescOptions>): void => {
|
||||
unref(descRef)!.setDescProps(descProps);
|
||||
unref(descRef)?.setDescProps(descProps);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { withInstall } from '../util';
|
||||
|
||||
import BasicDrawer from './src/BasicDrawer';
|
||||
import BasicDrawer from './src/BasicDrawer.vue';
|
||||
|
||||
export { BasicDrawer };
|
||||
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
|
||||
*/
|
||||
showCancelBtn: propTypes.bool.def(true),
|
||||
cancelButtonProps: Object as PropType<any>,
|
||||
cancelButtonProps: Object as PropType<Recordable>,
|
||||
cancelText: propTypes.string.def(t('component.drawer.cancelText')),
|
||||
/**
|
||||
* @description: Show confirmation button
|
||||
*/
|
||||
showOkBtn: propTypes.bool.def(true),
|
||||
okButtonProps: propTypes.any,
|
||||
okButtonProps: Object as PropType<Recordable>,
|
||||
okText: propTypes.string.def(t('component.drawer.okText')),
|
||||
okType: propTypes.string.def('primary'),
|
||||
showFooter: propTypes.bool,
|
||||
@ -28,6 +28,7 @@ export const footerProps = {
|
||||
export const basicProps = {
|
||||
isDetail: propTypes.bool,
|
||||
title: propTypes.string.def(''),
|
||||
loadingText: propTypes.string,
|
||||
showDetailBack: propTypes.bool.def(true),
|
||||
visible: propTypes.bool,
|
||||
loading: propTypes.bool,
|
||||
|
@ -1,13 +1,15 @@
|
||||
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';
|
||||
|
||||
export interface DrawerInstance {
|
||||
setDrawerProps: (props: Partial<DrawerProps> | boolean) => void;
|
||||
emitVisible?: (visible: boolean, uid: number) => void;
|
||||
}
|
||||
|
||||
export interface ReturnMethods extends DrawerInstance {
|
||||
openDrawer: <T = any>(visible?: boolean, data?: T, openOnSet?: boolean) => void;
|
||||
getVisible?: ComputedRef<boolean>;
|
||||
}
|
||||
|
||||
export type RegisterFn = (drawerInstance: DrawerInstance, uuid?: string) => void;
|
||||
@ -16,6 +18,7 @@ export interface ReturnInnerMethods extends DrawerInstance {
|
||||
closeDrawer: () => void;
|
||||
changeLoading: (loading: boolean) => void;
|
||||
changeOkLoading: (loading: boolean) => void;
|
||||
getVisible?: ComputedRef<boolean>;
|
||||
}
|
||||
|
||||
export type UseDrawerReturnType = [RegisterFn, ReturnMethods];
|
||||
|
@ -6,22 +6,32 @@ import type {
|
||||
UseDrawerInnerReturnType,
|
||||
} 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 { isFunction } from '/@/utils/is';
|
||||
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
|
||||
import { tryOnUnmounted, isInSetup } from '/@/utils/helper/vueHelper';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { error } from '/@/utils/log';
|
||||
|
||||
const dataTransferRef = reactive<any>({});
|
||||
|
||||
const visibleData = reactive<{ [key: number]: boolean }>({});
|
||||
|
||||
/**
|
||||
* @description: Applicable to separate drawer and call outside
|
||||
*/
|
||||
export function useDrawer(): UseDrawerReturnType {
|
||||
if (!getCurrentInstance()) {
|
||||
throw new Error('Please put useDrawer function in the setup function!');
|
||||
}
|
||||
isInSetup();
|
||||
|
||||
const drawerRef = ref<DrawerInstance | null>(null);
|
||||
const loadedRef = ref<Nullable<boolean>>(false);
|
||||
@ -41,23 +51,31 @@ export function useDrawer(): UseDrawerReturnType {
|
||||
uidRef.value = uuid;
|
||||
drawerRef.value = drawerInstance;
|
||||
loadedRef.value = true;
|
||||
|
||||
drawerInstance.emitVisible = (visible: boolean, uid: number) => {
|
||||
visibleData[uid] = visible;
|
||||
};
|
||||
}
|
||||
|
||||
const getInstance = () => {
|
||||
const instance = unref(drawerRef);
|
||||
if (!instance) {
|
||||
throw new Error('instance is undefined!');
|
||||
error('useDrawer instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
|
||||
const methods: ReturnMethods = {
|
||||
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 => {
|
||||
getInstance().setDrawerProps({
|
||||
getInstance()?.setDrawerProps({
|
||||
visible: visible,
|
||||
});
|
||||
if (!data) return;
|
||||
@ -79,17 +97,18 @@ export function useDrawer(): UseDrawerReturnType {
|
||||
|
||||
export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
|
||||
const drawerInstanceRef = ref<Nullable<DrawerInstance>>(null);
|
||||
const currentInstall = getCurrentInstance();
|
||||
const currentInstance = getCurrentInstance();
|
||||
const uidRef = ref<string>('');
|
||||
|
||||
if (!currentInstall) {
|
||||
throw new Error('useDrawerInner instance is undefined!');
|
||||
if (!currentInstance) {
|
||||
error('useDrawerInner instance is undefined!');
|
||||
}
|
||||
|
||||
const getInstance = () => {
|
||||
const instance = unref(drawerInstanceRef);
|
||||
if (!instance) {
|
||||
throw new Error('useDrawerInner instance is undefined!');
|
||||
error('useDrawerInner instance is undefined!');
|
||||
return;
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
@ -102,7 +121,7 @@ export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
|
||||
|
||||
uidRef.value = uuid;
|
||||
drawerInstanceRef.value = modalInstance;
|
||||
currentInstall.emit('register', modalInstance, uuid);
|
||||
currentInstance?.emit('register', modalInstance, uuid);
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
@ -118,19 +137,22 @@ export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
|
||||
register,
|
||||
{
|
||||
changeLoading: (loading = true) => {
|
||||
getInstance().setDrawerProps({ loading });
|
||||
getInstance()?.setDrawerProps({ loading });
|
||||
},
|
||||
|
||||
changeOkLoading: (loading = true) => {
|
||||
getInstance().setDrawerProps({ confirmLoading: loading });
|
||||
getInstance()?.setDrawerProps({ confirmLoading: loading });
|
||||
},
|
||||
getVisible: computed((): boolean => {
|
||||
return visibleData[~~unref(uidRef)];
|
||||
}),
|
||||
|
||||
closeDrawer: () => {
|
||||
getInstance().setDrawerProps({ visible: false });
|
||||
getInstance()?.setDrawerProps({ visible: false });
|
||||
},
|
||||
|
||||
setDrawerProps: (props: Partial<DrawerProps>) => {
|
||||
getInstance().setDrawerProps(props);
|
||||
getInstance()?.setDrawerProps(props);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<a-col
|
||||
v-bind="actionColOpt"
|
||||
class="mb-2"
|
||||
:style="{ textAlign: 'right' }"
|
||||
v-if="showActionButtonGroup"
|
||||
>
|
||||
<a-col v-bind="actionColOpt" :style="{ textAlign: 'right' }" v-if="showActionButtonGroup">
|
||||
<FormItem>
|
||||
<slot name="resetBefore" />
|
||||
<Button
|
||||
|
@ -1,10 +1,10 @@
|
||||
import './src/index.less';
|
||||
import { withInstall } from '../util';
|
||||
import BasicModal from './src/BasicModal';
|
||||
import BasicModal from './src/BasicModal.vue';
|
||||
|
||||
withInstall(BasicModal);
|
||||
|
||||
export { BasicModal };
|
||||
export { useModalContext } from './src/useModalContext';
|
||||
export { useModal, useModalInner } from './src/useModal';
|
||||
export { useModalContext } from './src/hooks/useModalContext';
|
||||
export { useModal, useModalInner } from './src/hooks/useModal';
|
||||
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 { defineComponent, toRefs } from 'vue';
|
||||
import { basicProps } from './props';
|
||||
import { useModalDragMove } from './useModalDrag';
|
||||
import { defineComponent, toRefs, unref } from 'vue';
|
||||
import { basicProps } from '../props';
|
||||
import { useModalDragMove } from '../hooks/useModalDrag';
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs';
|
||||
import { extendSlots } from '/@/utils/helper/tsxHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Modal',
|
||||
inheritAttrs: false,
|
||||
props: basicProps,
|
||||
setup(props, { attrs, slots }) {
|
||||
setup(props, { slots }) {
|
||||
const { visible, draggable, destroyOnClose } = toRefs(props);
|
||||
|
||||
const attrs = useAttrs();
|
||||
useModalDragMove({
|
||||
visible,
|
||||
destroyOnClose,
|
||||
@ -18,7 +19,8 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
return () => {
|
||||
const propsData = { ...attrs, ...props } as any;
|
||||
const propsData = { ...unref(attrs), ...props } as Recordable;
|
||||
|
||||
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,
|
||||
ReturnMethods,
|
||||
UseModalInnerReturnType,
|
||||
} from './types';
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
ref,
|
||||
@ -19,16 +19,18 @@ import {
|
||||
import { isProdMode } from '/@/utils/env';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
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 visibleData = reactive<{ [key: number]: boolean }>({});
|
||||
|
||||
/**
|
||||
* @description: Applicable to independent modal and call outside
|
||||
*/
|
||||
export function useModal(): UseModalReturnType {
|
||||
if (!getCurrentInstance()) {
|
||||
throw new Error('Please put useModal function in the setup function!');
|
||||
}
|
||||
isInSetup();
|
||||
const modalRef = ref<Nullable<ModalMethods>>(null);
|
||||
const loadedRef = ref<Nullable<boolean>>(false);
|
||||
const uidRef = ref<string>('');
|
||||
@ -45,23 +47,29 @@ export function useModal(): UseModalReturnType {
|
||||
if (unref(loadedRef) && isProdMode() && modalMethod === unref(modalRef)) return;
|
||||
|
||||
modalRef.value = modalMethod;
|
||||
modalMethod.emitVisible = (visible: boolean, uid: number) => {
|
||||
visibleData[uid] = visible;
|
||||
};
|
||||
}
|
||||
|
||||
const getInstance = () => {
|
||||
const instance = unref(modalRef);
|
||||
if (!instance) {
|
||||
throw new Error('instance is undefined!');
|
||||
error('useModal instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
|
||||
const methods: ReturnMethods = {
|
||||
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 => {
|
||||
getInstance().setModalProps({
|
||||
getInstance()?.setModalProps({
|
||||
visible: visible,
|
||||
});
|
||||
|
||||
@ -83,20 +91,16 @@ export function useModal(): UseModalReturnType {
|
||||
|
||||
export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
|
||||
const modalInstanceRef = ref<Nullable<ModalMethods>>(null);
|
||||
const currentInstall = getCurrentInstance();
|
||||
const currentInstance = getCurrentInstance();
|
||||
const uidRef = ref<string>('');
|
||||
|
||||
if (!currentInstall) {
|
||||
throw new Error('instance is undefined!');
|
||||
}
|
||||
|
||||
// currentInstall.type.emits = [...currentInstall.type.emits, 'register'];
|
||||
// Object.assign(currentInstall.type.emits, ['register']);
|
||||
|
||||
const getInstance = () => {
|
||||
const instance = unref(modalInstanceRef);
|
||||
if (!instance) {
|
||||
throw new Error('instance is undefined!');
|
||||
error('useModalInner instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
@ -108,7 +112,7 @@ export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
|
||||
});
|
||||
uidRef.value = uuid;
|
||||
modalInstanceRef.value = modalInstance;
|
||||
currentInstall.emit('register', modalInstance, uuid);
|
||||
currentInstance?.emit('register', modalInstance, uuid);
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
@ -124,19 +128,22 @@ export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
|
||||
register,
|
||||
{
|
||||
changeLoading: (loading = true) => {
|
||||
getInstance().setModalProps({ loading });
|
||||
getInstance()?.setModalProps({ loading });
|
||||
},
|
||||
getVisible: computed((): boolean => {
|
||||
return visibleData[~~unref(uidRef)];
|
||||
}),
|
||||
|
||||
changeOkLoading: (loading = true) => {
|
||||
getInstance().setModalProps({ confirmLoading: loading });
|
||||
getInstance()?.setModalProps({ confirmLoading: loading });
|
||||
},
|
||||
|
||||
closeModal: () => {
|
||||
getInstance().setModalProps({ visible: false });
|
||||
getInstance()?.setModalProps({ visible: false });
|
||||
},
|
||||
|
||||
setModalProps: (props: Partial<ModalProps>) => {
|
||||
getInstance().setModalProps(props);
|
||||
getInstance()?.setModalProps(props);
|
||||
},
|
||||
},
|
||||
];
|
@ -21,9 +21,12 @@
|
||||
width: 520px;
|
||||
padding-bottom: 0;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
padding: 16px;
|
||||
.scroll-container {
|
||||
padding: 14px;
|
||||
}
|
||||
// .ant-spin-nested-loading {
|
||||
// padding: 16px;
|
||||
// }
|
||||
|
||||
&-title {
|
||||
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 {
|
||||
padding: 0;
|
||||
}
|
||||
@ -96,8 +59,6 @@
|
||||
}
|
||||
|
||||
&-footer {
|
||||
// padding: 10px 26px 26px 16px;
|
||||
|
||||
button + button {
|
||||
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 { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { propTypes, VueNode } from '/@/utils/propTypes';
|
||||
import type { ModalWrapperProps } from './types';
|
||||
const { t } = useI18n();
|
||||
|
||||
export const modalProps = {
|
||||
@ -26,6 +27,7 @@ export const basicProps = Object.assign({}, modalProps, {
|
||||
// Whether to setting wrapper
|
||||
useWrapper: propTypes.bool.def(true),
|
||||
loading: propTypes.bool,
|
||||
loadingTip: propTypes.string,
|
||||
/**
|
||||
* @description: Show close button
|
||||
*/
|
||||
@ -35,65 +37,44 @@ export const basicProps = Object.assign({}, modalProps, {
|
||||
*/
|
||||
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: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
closable: propTypes.bool.def(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>,
|
||||
|
||||
mask: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
mask: propTypes.bool.def(true),
|
||||
|
||||
maskClosable: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
keyboard: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
maskClosable: propTypes.bool.def(true),
|
||||
keyboard: propTypes.bool.def(true),
|
||||
|
||||
maskStyle: Object as PropType<any>,
|
||||
maskStyle: Object as PropType<CSSProperties>,
|
||||
|
||||
okType: {
|
||||
type: String as PropType<string>,
|
||||
default: 'primary',
|
||||
},
|
||||
okType: propTypes.string.def('primary'),
|
||||
|
||||
okButtonProps: Object as PropType<ButtonProps>,
|
||||
|
||||
cancelButtonProps: Object as PropType<ButtonProps>,
|
||||
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
},
|
||||
title: propTypes.string,
|
||||
|
||||
visible: Boolean as PropType<boolean>,
|
||||
visible: propTypes.bool,
|
||||
|
||||
width: [String, Number] as PropType<string | number>,
|
||||
|
||||
wrapClassName: {
|
||||
type: String as PropType<string>,
|
||||
},
|
||||
wrapClassName: propTypes.string,
|
||||
|
||||
zIndex: {
|
||||
type: Number as PropType<number>,
|
||||
},
|
||||
zIndex: propTypes.number,
|
||||
});
|
||||
|
@ -1,16 +1,18 @@
|
||||
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
|
||||
import type { CSSProperties, VNodeChild } from 'vue';
|
||||
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
|
||||
/**
|
||||
* @description: 弹窗对外暴露的方法
|
||||
*/
|
||||
export interface ModalMethods {
|
||||
setModalProps: (props: Partial<ModalProps>) => void;
|
||||
emitVisible?: (visible: boolean, uid: number) => void;
|
||||
}
|
||||
|
||||
export type RegisterFn = (modalMethods: ModalMethods, uuid?: string) => void;
|
||||
|
||||
export interface ReturnMethods extends ModalMethods {
|
||||
openModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void;
|
||||
getVisible?: ComputedRef<boolean>;
|
||||
}
|
||||
|
||||
export type UseModalReturnType = [RegisterFn, ReturnMethods];
|
||||
@ -19,6 +21,7 @@ export interface ReturnInnerMethods extends ModalMethods {
|
||||
closeModal: () => void;
|
||||
changeLoading: (loading: boolean) => void;
|
||||
changeOkLoading: (loading: boolean) => void;
|
||||
getVisible?: ComputedRef<boolean>;
|
||||
}
|
||||
|
||||
export type UseModalInnerReturnType = [RegisterFn, ReturnInnerMethods];
|
||||
@ -38,6 +41,7 @@ export interface ModalProps {
|
||||
useWrapper: boolean;
|
||||
|
||||
loading: boolean;
|
||||
loadingTip?: string;
|
||||
|
||||
wrapperProps: Omit<ModalWrapperProps, 'loading'>;
|
||||
|
||||
@ -193,4 +197,5 @@ export interface ModalWrapperProps {
|
||||
minHeight: number;
|
||||
visible: boolean;
|
||||
fullScreen: boolean;
|
||||
useWrapper: boolean;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getCurrentInstance, reactive, shallowRef, watchEffect } from 'vue';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
interface Params {
|
||||
excludeListeners?: boolean;
|
||||
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]]);
|
||||
}
|
||||
|
||||
export function useAttrs(params: Params = {}) {
|
||||
export function useAttrs(params: Params = {}): Ref<Recordable> | {} {
|
||||
const instance = getCurrentInstance();
|
||||
if (!instance) return {};
|
||||
|
||||
|
@ -37,7 +37,7 @@ const setting: ProjectConfig = {
|
||||
showLogo: true,
|
||||
|
||||
// Whether to show footer
|
||||
showFooter: true,
|
||||
showFooter: false,
|
||||
|
||||
// locale setting
|
||||
locale: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CSSProperties, VNodeChild } from 'vue';
|
||||
import { createTypes, VueTypeValidableDef, VueTypesInterface } from 'vue-types';
|
||||
|
||||
type VueNode = VNodeChild | JSX.Element;
|
||||
export type VueNode = VNodeChild | JSX.Element;
|
||||
|
||||
type PropTypes = VueTypesInterface & {
|
||||
readonly style: VueTypeValidableDef<CSSProperties>;
|
||||
|
@ -1,6 +1,20 @@
|
||||
<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>
|
||||
<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>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
@ -9,7 +23,13 @@
|
||||
export default defineComponent({
|
||||
components: { BasicDrawer },
|
||||
setup() {
|
||||
return {};
|
||||
return {
|
||||
handleOk: () => {
|
||||
console.log('=====================');
|
||||
console.log('ok');
|
||||
console.log('======================');
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<BasicDrawer v-bind="$attrs" :isDetail="true" title="Drawer Title5">
|
||||
<p class="h-20">Content Message</p>
|
||||
<template #titleToolbar> toolbar </template>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
@ -8,6 +9,5 @@
|
||||
import { BasicDrawer } from '/@/components/Drawer';
|
||||
export default defineComponent({
|
||||
components: { BasicDrawer },
|
||||
setup() {},
|
||||
});
|
||||
</script>
|
||||
|
@ -3,10 +3,10 @@
|
||||
<Alert message="使用 useDrawer 进行抽屉操作" show-icon />
|
||||
<a-button type="primary" class="my-4" @click="openDrawerLoading">打开Drawer</a-button>
|
||||
|
||||
<Alert message="内外同时同时显示隐藏" show-icon />
|
||||
<a-button type="primary" class="my-4" @click="openDrawer2">打开Drawer</a-button>
|
||||
<Alert message="内外同时控制显示隐藏" show-icon />
|
||||
<a-button type="primary" class="my-4" @click="openDrawer2(true)">打开Drawer</a-button>
|
||||
<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
|
||||
message="内外数据交互,外部通过 transferModalData 发送,内部通过 receiveDrawerDataRef 接收。该数据具有响应式"
|
||||
@ -14,7 +14,7 @@
|
||||
/>
|
||||
<a-button type="primary" class="my-4" @click="send">打开Drawer并传递数据</a-button>
|
||||
<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" />
|
||||
<Drawer2 @register="register2" />
|
||||
<Drawer3 @register="register3" />
|
||||
|
@ -4,7 +4,7 @@
|
||||
<a-input placeholder="请输入" />
|
||||
</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="closeLeft">关闭左侧</a-button>
|
||||
<a-button class="mr-2" @click="closeRight">关闭右侧</a-button>
|
||||
|
Loading…
Reference in New Issue
Block a user