提交 81baf1d5 编写于 作者: V vben

perf: perf modal and drawer

上级 819127e8
......@@ -18,12 +18,18 @@
- 缓存可以配置是否加密,默认生产环境开启 Aes 加密
- 新增标签页拖拽排序
- 新增 LayoutFooter.默认显示,可以在配置内关闭
### ⚡ Performance Improvements
- 优化`Modal`组件全屏动画不流畅问题
### 🐛 Bug Fixes
- 修复 tree 文本超出挡住操作按钮问题
- 修复通过 useRedo 刷新页面参数丢失问题
- 修复表单校验先设置在校验及控制台错误信息问题
- 修复`modal``drawer`组件传递数组参数问题
### 🎫 Chores
......
export { default as BasicDrawer } from './src/BasicDrawer';
import BasicDrawerLib from './src/BasicDrawer';
import { withInstall } from '../util';
export { useDrawer, useDrawerInner } from './src/useDrawer';
export * from './src/types';
export { useDrawer, useDrawerInner } from './src/useDrawer';
export const BasicDrawer = withInstall(BasicDrawerLib);
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';
......@@ -9,53 +10,96 @@ import { BasicTitle } from '/@/components/Basic';
import { FullLoading } from '/@/components/Loading/index';
import { LeftOutlined } from '@ant-design/icons-vue';
import { basicProps } from './props';
import { useI18n } from '/@/hooks/web/useI18n';
import { getSlot } from '/@/utils/helper/tsxHelper';
import { isFunction, isNumber } from '/@/utils/is';
import { buildUUID } from '/@/utils/uuid';
import { deepMerge } from '/@/utils';
import { useI18n } from '/@/hooks/web/useI18n';
import { tryTsxEmit } from '/@/utils/helper/vueHelper';
import { basicProps } from './props';
const prefixCls = 'basic-drawer';
export default defineComponent({
// inheritAttrs: false,
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<DrawerProps> | null>(null);
const propsRef = ref<Partial<Nullable<DrawerProps>>>(null);
const { t } = useI18n('component.drawer');
const getMergeProps = computed((): any => {
return deepMerge(toRaw(props), unref(propsRef));
});
const getProps = computed(() => {
const opt: any = {
placement: 'right',
...attrs,
...props,
...(unref(propsRef) as any),
visible: unref(visibleRef),
};
opt.title = undefined;
const getMergeProps = computed(
(): DrawerProps => {
return deepMerge(toRaw(props), unref(propsRef));
}
);
if (opt.isDetail) {
if (!opt.width) {
opt.width = '100%';
}
opt.wrapClassName = opt.wrapClassName
? `${opt.wrapClassName} ${prefixCls}__detail`
: `${prefixCls}__detail`;
if (!opt.getContainer) {
opt.getContainer = '.layout-content';
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;
}
return opt;
);
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 {
hidden: !unref(getProps).loading,
};
});
watchEffect(() => {
......@@ -74,22 +118,13 @@ export default defineComponent({
}
);
// Custom implementation of the bottom button,
const getFooterHeight = computed(() => {
const { footerHeight, showFooter }: DrawerProps = unref(getProps);
if (showFooter && footerHeight) {
return isNumber(footerHeight) ? `${footerHeight}px` : `${footerHeight.replace('px', '')}px`;
}
return `0px`;
});
// Cancel event
async function onClose(e: any) {
async function onClose(e: ChangeEvent) {
const { closeFunc } = unref(getProps);
emit('close', e);
if (closeFunc && isFunction(closeFunc)) {
const res = await closeFunc();
res && (visibleRef.value = false);
visibleRef.value = !res;
return;
}
visibleRef.value = false;
......@@ -98,12 +133,16 @@ export default defineComponent({
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,
......@@ -114,65 +153,64 @@ export default defineComponent({
okButtonProps,
confirmLoading,
showFooter,
}: DrawerProps = unref(getProps);
} = unref(getProps);
if (!showFooter) {
return null;
}
return (
getSlot(slots, 'footer') ||
(showFooter && (
<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>
))
<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);
return props.isDetail ? (
getSlot(slots, 'title') || (
<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>
)
) : (
<BasicTitle>{() => title || getSlot(slots, 'title')}</BasicTitle>
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>
);
}
......@@ -180,41 +218,20 @@ export default defineComponent({
setDrawerProps: setDrawerProps,
};
const uuid = buildUUID();
emit('register', drawerInstance, uuid);
tryTsxEmit((instance) => {
emit('register', drawerInstance, instance.uid);
});
return () => {
const footerHeight = unref(getFooterHeight);
return (
<Drawer
class={prefixCls}
onClose={onClose}
{...{
...attrs,
...unref(getProps),
}}
>
<Drawer class={prefixCls} onClose={onClose} {...unref(getBindValues)}>
{{
title: () => renderHeader(),
default: () => (
<>
<div
ref={scrollRef}
{...attrs}
style={{
position: 'relative',
height: `calc(100% - ${footerHeight})`,
overflow: 'auto',
padding: '16px',
paddingBottom: '30px',
}}
>
<FullLoading
absolute
tip={t('loadingText')}
class={[!unref(getProps).loading ? 'hidden' : '']}
/>
{getSlot(slots, 'default')}
<div ref={scrollRef} style={unref(getScrollContentStyle)}>
<FullLoading absolute tip={t('loadingText')} class={unref(getLoading)} />
{getSlot(slots)}
</div>
{renderFooter()}
</>
......
import type { PropType } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
const { t } = useI18n('component.drawer');
export const footerProps = {
confirmLoading: Boolean as PropType<boolean>,
confirmLoading: propTypes.bool,
/**
* @description: Show close button
*/
showCancelBtn: {
type: Boolean as PropType<boolean>,
default: true,
},
showCancelBtn: propTypes.bool.def(true),
cancelButtonProps: Object as PropType<any>,
cancelText: {
type: String as PropType<string>,
default: t('cancelText'),
},
cancelText: propTypes.string.def(t('cancelText')),
/**
* @description: Show confirmation button
*/
showOkBtn: {
type: Boolean as PropType<boolean>,
default: true,
},
okButtonProps: Object as PropType<any>,
okText: {
type: String as PropType<string>,
default: t('okText'),
},
okType: {
type: String as PropType<string>,
default: 'primary',
},
showFooter: {
type: Boolean as PropType<boolean>,
default: false,
},
showOkBtn: propTypes.bool.def(true),
okButtonProps: propTypes.any,
okText: propTypes.string.def(t('okText')),
okType: propTypes.string.def('primary'),
showFooter: propTypes.bool,
footerHeight: {
type: [String, Number] as PropType<string | number>,
default: 60,
},
};
export const basicProps = {
isDetail: {
type: Boolean as PropType<boolean>,
default: false,
},
title: {
type: String as PropType<string>,
default: '',
},
showDetailBack: {
type: Boolean as PropType<boolean>,
default: true,
},
visible: {
type: Boolean as PropType<boolean>,
default: false,
},
loading: {
type: Boolean as PropType<boolean>,
default: false,
},
maskClosable: {
type: Boolean as PropType<boolean>,
default: true,
},
isDetail: propTypes.bool,
title: propTypes.string.def(''),
showDetailBack: propTypes.bool.def(true),
visible: propTypes.bool,
loading: propTypes.bool,
maskClosable: propTypes.bool.def(true),
getContainer: {
type: [Object, String] as PropType<any>,
},
......@@ -78,10 +43,7 @@ export const basicProps = {
type: [Function, Object] as PropType<any>,
default: null,
},
triggerWindowResize: {
type: Boolean as PropType<boolean>,
default: false,
},
destroyOnClose: Boolean as PropType<boolean>,
triggerWindowResize: propTypes.bool,
destroyOnClose: propTypes.bool,
...footerProps,
};
......@@ -75,7 +75,7 @@ export interface DrawerProps extends DrawerFooterProps {
* @type ScrollContainerOptions
*/
scrollOptions?: ScrollContainerOptions;
closeFunc?: () => Promise<void>;
closeFunc?: () => Promise<any>;
triggerWindowResize?: boolean;
/**
* Whether a close (x) button is visible on top right of the Drawer dialog or not.
......
......@@ -6,12 +6,15 @@ import type {
UseDrawerInnerReturnType,
} from './types';
import { ref, getCurrentInstance, onUnmounted, unref, reactive, watchEffect, nextTick } from 'vue';
import { ref, getCurrentInstance, unref, reactive, watchEffect, nextTick, toRaw } from 'vue';
import { isProdMode } from '/@/utils/env';
import { isFunction } from '/@/utils/is';
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
import { isEqual } from 'lodash-es';
const dataTransferRef = reactive<any>({});
/**
* @description: Applicable to separate drawer and call outside
*/
......@@ -19,21 +22,23 @@ export function useDrawer(): UseDrawerReturnType {
if (!getCurrentInstance()) {
throw new Error('Please put useDrawer function in the setup function!');
}
const drawerRef = ref<DrawerInstance | null>(null);
const loadedRef = ref<boolean | null>(false);
const loadedRef = ref<Nullable<boolean>>(false);
const uidRef = ref<string>('');
function getDrawer(drawerInstance: DrawerInstance, uuid: string) {
uidRef.value = uuid;
function register(drawerInstance: DrawerInstance, uuid: string) {
isProdMode() &&
onUnmounted(() => {
tryOnUnmounted(() => {
drawerRef.value = null;
loadedRef.value = null;
dataTransferRef[unref(uidRef)] = null;
});
if (unref(loadedRef) && isProdMode() && drawerInstance === unref(drawerRef)) {
return;
}
uidRef.value = uuid;
drawerRef.value = drawerInstance;
loadedRef.value = true;
}
......@@ -55,37 +60,46 @@ export function useDrawer(): UseDrawerReturnType {
getInstance().setDrawerProps({
visible: visible,
});
if (data) {
dataTransferRef[unref(uidRef)] = openOnSet
? {
...data,
__t__: Date.now(),
}
: data;
if (!data) return;
if (openOnSet) {
dataTransferRef[unref(uidRef)] = null;
dataTransferRef[unref(uidRef)] = data;
return;
}
const equal = isEqual(toRaw(dataTransferRef[unref(uidRef)]), data);
if (!equal) {
dataTransferRef[unref(uidRef)] = data;
}
},
};
return [getDrawer, methods];
return [register, methods];
}
export const useDrawerInner = (callbackFn?: Fn): UseDrawerInnerReturnType => {
const drawerInstanceRef = ref<DrawerInstance | null>(null);
const drawerInstanceRef = ref<Nullable<DrawerInstance>>(null);
const currentInstall = getCurrentInstance();
const uidRef = ref<string>('');
if (!currentInstall) {
throw new Error('instance is undefined!');
throw new Error('useDrawerInner instance is undefined!');
}
const getInstance = () => {
const instance = unref(drawerInstanceRef);
if (!instance) {
throw new Error('instance is undefined!');
throw new Error('useDrawerInner instance is undefined!');
}
return instance;
};
const register = (modalInstance: DrawerInstance, uuid: string) => {
isProdMode() &&
tryOnUnmounted(() => {
drawerInstanceRef.value = null;
});
uidRef.value = uuid;
drawerInstanceRef.value = modalInstance;
currentInstall.emit('register', modalInstance);
......
import './src/index.less';
export { default as BasicModal } from './src/BasicModal';
export { default as Modal } from './src/Modal';
import BasicModalLib from './src/BasicModal';
import { withInstall } from '../util';
export { useModalContext } from './src/useModalContext';
export { useModal, useModalInner } from './src/useModal';
export * from './src/types';
export const BasicModal = withInstall(BasicModalLib);
import type { ModalProps, ModalMethods } from './types';
import { defineComponent, computed, ref, watch, unref, watchEffect } from 'vue';
import { defineComponent, computed, ref, watch, unref, watchEffect, toRef } from 'vue';
import Modal from './Modal';
import { Button } from '/@/components/Button';
......@@ -11,10 +11,10 @@ import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-
import { getSlot, extendSlots } from '/@/utils/helper/tsxHelper';
import { isFunction } from '/@/utils/is';
import { deepMerge } from '/@/utils';
import { buildUUID } from '/@/utils/uuid';
import { tryTsxEmit } from '/@/utils/helper/vueHelper';
import { basicProps } from './props';
// import { triggerWindowResize } from '@/utils/event/triggerWindowResizeEvent';
import { useFullScreen } from './useFullScreen';
export default defineComponent({
name: 'BasicModal',
props: basicProps,
......@@ -26,31 +26,41 @@ export default defineComponent({
// modal Bottom and top height
const extHeightRef = ref(0);
// Unexpanded height of the popup
const formerHeightRef = ref(0);
const fullScreenRef = ref(false);
// Custom title component: get title
const getMergeProps = computed(() => {
return {
...props,
...(unref(propsRef) as any),
};
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((): any => {
const opt = {
...props,
...((unref(propsRef) || {}) as any),
visible: unref(visibleRef),
title: undefined,
};
const { wrapClassName = '' } = opt;
const className = unref(fullScreenRef) ? `${wrapClassName} fullscreen-modal` : wrapClassName;
return {
...opt,
wrapClassName: className,
};
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(() => {
......@@ -80,7 +90,35 @@ export default defineComponent({
);
}
// 取消事件
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);
......@@ -93,7 +131,7 @@ export default defineComponent({
loading={loading}
visible={unref(visibleRef)}
modalFooterHeight={showFooter}
{...wrapperProps}
{...((wrapperProps as unknown) as OmitWrapperType)}
onGetExtHeight={(height: number) => {
extHeightRef.value = height;
}}
......@@ -106,18 +144,6 @@ export default defineComponent({
);
}
// 取消事件
async function handleCancel(e: Event) {
e && e.stopPropagation();
if (props.closeFunc && isFunction(props.closeFunc)) {
const isClose: boolean = await props.closeFunc();
visibleRef.value = !isClose;
return;
}
visibleRef.value = false;
emit('cancel');
}
// 底部按钮自定义实现,
function renderFooter() {
const {
......@@ -162,64 +188,37 @@ export default defineComponent({
*/
function renderClose() {
const { canFullscreen } = unref(getProps);
if (!canFullscreen) {
return null;
}
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="custom-close-icon">
{unref(fullScreenRef) ? (
<FullscreenExitOutlined role="full" onClick={handleFullScreen} />
) : (
<FullscreenOutlined role="close" onClick={handleFullScreen} />
)}
<div class={cls}>
{canFullscreen && fullScreen}
<CloseOutlined onClick={handleCancel} />
</div>
);
}
function handleFullScreen(e: Event) {
e && e.stopPropagation();
fullScreenRef.value = !unref(fullScreenRef);
const modalWrapper = unref(modalWrapperRef);
if (!modalWrapper) return;
const wrapperEl = modalWrapper.$el as HTMLElement;
if (!wrapperEl) return;
const modalWrapSpinEl = wrapperEl.querySelector('.ant-spin-nested-loading') as HTMLElement;
if (!modalWrapSpinEl) return;
if (!unref(formerHeightRef) && unref(fullScreenRef)) {
formerHeightRef.value = modalWrapSpinEl.offsetHeight;
}
if (unref(fullScreenRef)) {
modalWrapSpinEl.style.height = `${window.innerHeight - unref(extHeightRef)}px`;
} else {
modalWrapSpinEl.style.height = `${unref(formerHeightRef)}px`;
}
}
/**
* @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;
}
const modalMethods: ModalMethods = {
setModalProps,
};
const uuid = buildUUID();
emit('register', modalMethods, uuid);
tryTsxEmit((instance) => {
emit('register', modalMethods, instance.uid);
});
return () => (
<Modal onCancel={handleCancel} {...{ ...attrs, ...props, ...unref(getProps) }}>
<Modal onCancel={handleCancel} {...unref(getModalBindValue)}>
{{
footer: () => renderFooter(),
closeIcon: () => renderClose(),
......
import { Modal } from 'ant-design-vue';
import { defineComponent, watchEffect } from 'vue';
import { defineComponent, toRefs } from 'vue';
import { basicProps } from './props';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
import { useModalDragMove } from './useModalDrag';
import { extendSlots } from '/@/utils/helper/tsxHelper';
export default defineComponent({
......@@ -9,99 +9,12 @@ export default defineComponent({
inheritAttrs: false,
props: basicProps,
setup(props, { attrs, slots }) {
const getStyle = (dom: any, attr: any) => {
return getComputedStyle(dom)[attr];
};
const drag = (wrap: any) => {
if (!wrap) return;
wrap.setAttribute('data-drag', props.draggable);
const dialogHeaderEl = wrap.querySelector('.ant-modal-header');
const dragDom = wrap.querySelector('.ant-modal');
if (!dialogHeaderEl || !dragDom || !props.draggable) return;
dialogHeaderEl.style.cursor = 'move';
dialogHeaderEl.onmousedown = (e: any) => {
if (!e) return;
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX;
const disY = e.clientY;
const screenWidth = document.body.clientWidth; // body当前宽度
const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
const dragDomheight = dragDom.offsetHeight; // 对话框高度
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
// 获取到的值带px 正则匹配替换
const domLeft = getStyle(dragDom, 'left');
const domTop = getStyle(dragDom, 'top');
let styL = +domLeft;
let styT = +domTop;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (domLeft.includes('%')) {
styL = +document.body.clientWidth * (+domLeft.replace(/%/g, '') / 100);
styT = +document.body.clientHeight * (+domTop.replace(/%/g, '') / 100);
} else {
styL = +domLeft.replace(/px/g, '');
styT = +domTop.replace(/px/g, '');
}
document.onmousemove = function (e) {
// 通过事件委托,计算移动的距离
let left = e.clientX - disX;
let top = e.clientY - disY;
// 边界处理
if (-left > minDragDomLeft) {
left = -minDragDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDragDomTop) {
top = -minDragDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
};
};
const handleDrag = () => {
const dragWraps = document.querySelectorAll('.ant-modal-wrap');
for (const wrap of dragWraps as any) {
if (!wrap) continue;
const display = getStyle(wrap, 'display');
const draggable = wrap.getAttribute('data-drag');
if (display !== 'none') {
// 拖拽位置
(draggable === null || props.destroyOnClose) && drag(wrap);
}
}
};
const { visible, draggable, destroyOnClose } = toRefs(props);
watchEffect(() => {
if (!props.visible) {
return;
}
useTimeoutFn(() => {
handleDrag();
}, 30);
useModalDragMove({
visible,
destroyOnClose,
draggable,
});
return () => {
......
import type { PropType } from 'vue';
import type { ModalWrapperProps } from './types';
import type { CSSProperties } from 'vue';
import {
defineComponent,
......@@ -18,59 +18,44 @@ import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { getSlot } from '/@/utils/helper/tsxHelper';
import { useElResize } from '/@/hooks/event/useElResize';
import { provideModal } from './provideModal';
import { propTypes } from '/@/utils/propTypes';
import { createModalContext } from './useModalContext';
export default defineComponent({
name: 'ModalWrapper',
props: {
loading: {
type: Boolean as PropType<boolean>,
default: false,
},
modalHeaderHeight: {
type: Number as PropType<number>,
default: 50,
},
modalFooterHeight: {
type: Number as PropType<number>,
default: 70,
},
minHeight: {
type: Number as PropType<number>,
default: 200,
},
footerOffset: {
type: Number as PropType<number>,
default: 0,
},
visible: {
type: Boolean as PropType<boolean>,
default: false,
},
fullScreen: {
type: Boolean as PropType<boolean>,
default: false,
},
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<HTMLElement | null>(null);
const wrapperRef = ref<ElRef>(null);
const spinRef = ref<ComponentRef>(null);
const realHeightRef = ref(0);
// 重试次数
// let tryCount = 0;
let stopElResizeFn: Fn = () => {};
provideModal(setModalHeight);
useWindowSizeFn(setModalHeight);
const wrapStyle = computed(() => {
return {
minHeight: `${props.minHeight}px`,
height: `${unref(realHeightRef)}px`,
overflow: 'auto',
};
createModalContext({
redoModalHeight: setModalHeight,
});
const wrapStyle = computed(
(): CSSProperties => {
return {
minHeight: `${props.minHeight}px`,
height: `${unref(realHeightRef)}px`,
overflow: 'auto',
};
}
);
watchEffect(() => {
setModalHeight();
});
......@@ -92,8 +77,6 @@ export default defineComponent({
stopElResizeFn && stopElResizeFn();
});
useWindowSizeFn(setModalHeight);
async function setModalHeight() {
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度
// 加上这个,就必须在使用的时候传递父级的visible
......@@ -107,9 +90,8 @@ export default defineComponent({
try {
const modalDom = bodyDom.parentElement && bodyDom.parentElement.parentElement;
if (!modalDom) {
return;
}
if (!modalDom) return;
const modalRect = getComputedStyle(modalDom).top;
const modalTop = Number.parseInt(modalRect);
let maxHeight =
......@@ -135,11 +117,12 @@ export default defineComponent({
if (props.fullScreen) {
realHeightRef.value =
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight - 6;
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) {
......@@ -154,8 +137,10 @@ export default defineComponent({
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();
});
......
......@@ -9,6 +9,11 @@
bottom: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100%;
&-content {
height: 100%;
}
}
}
......@@ -35,8 +40,23 @@
height: 95%;
align-items: center;
> * {
margin-left: 12px;
> 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) {
......@@ -76,7 +96,7 @@
}
&-footer {
padding: 10px 26px 26px 16px;
// padding: 10px 26px 26px 16px;
button + button {
margin-left: 10px;
......
......@@ -2,66 +2,38 @@ import type { PropType } from 'vue';
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
const { t } = useI18n('component.modal');
export const modalProps = {
visible: Boolean as PropType<boolean>,
visible: propTypes.bool,
// open drag
draggable: {
type: Boolean as PropType<boolean>,
default: true,
},
centered: {
type: Boolean as PropType<boolean>,
default: false,
},
cancelText: {
type: String as PropType<string>,
default: t('cancelText'),
},
okText: {
type: String as PropType<string>,
default: t('okText'),
},
draggable: propTypes.bool.def(true),
centered: propTypes.bool,
cancelText: propTypes.string.def(t('cancelText')),
okText: propTypes.string.def(t('okText')),
closeFunc: Function as PropType<() => Promise<boolean>>,
};
export const basicProps = Object.assign({}, modalProps, {
// Can it be full screen
canFullscreen: {
type: Boolean as PropType<boolean>,
default: true,
},
canFullscreen: propTypes.bool.def(true),
// After enabling the wrapper, the bottom can be increased in height
wrapperFooterOffset: {
type: Number as PropType<number>,
default: 0,
},
wrapperFooterOffset: propTypes.number.def(0),
// Warm reminder message
helpMessage: [String, Array] as PropType<string | string[]>,
// Whether to setting wrapper
useWrapper: {
type: Boolean as PropType<boolean>,
default: true,
},
loading: {
type: Boolean as PropType<boolean>,
default: false,
},
useWrapper: propTypes.bool.def(true),
loading: propTypes.bool,
/**
* @description: Show close button
*/
showCancelBtn: {
type: Boolean as PropType<boolean>,
default: true,
},
showCancelBtn: propTypes.bool.def(true),
/**
* @description: Show confirmation button
*/
showOkBtn: {
type: Boolean as PropType<boolean>,
default: true,
},
showOkBtn: propTypes.bool.def(true),
wrapperProps: Object as PropType<any>,
......
import { provide, inject } from 'vue';
const key = Symbol('basic-modal');
export function provideModal(redoHeight: Fn) {
provide(key, redoHeight);
}
export function injectModal(): Fn {
return inject(key, () => {}) as Fn;
}
......@@ -8,9 +8,11 @@ export interface ModalMethods {
}
export type RegisterFn = (modalMethods: ModalMethods, uuid?: string) => void;
export interface ReturnMethods extends ModalMethods {
openModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void;
}
export type UseModalReturnType = [RegisterFn, ReturnMethods];
export interface ReturnInnerMethods extends ModalMethods {
......@@ -18,6 +20,7 @@ export interface ReturnInnerMethods extends ModalMethods {
changeLoading: (loading: boolean) => void;
changeOkLoading: (loading: boolean) => void;
}
export type UseModalInnerReturnType = [RegisterFn, ReturnInnerMethods];
export interface ModalProps {
......
import { computed, Ref, ref, unref } from 'vue';
export interface UseFullScreenContext {
wrapClassName: Ref<string | undefined>;
modalWrapperRef: Ref<ComponentRef>;
extHeightRef: Ref<number>;
}
export function useFullScreen(context: UseFullScreenContext) {
const formerHeightRef = ref(0);
const fullScreenRef = ref(false);
const getWrapClassName = computed(() => {
const clsName = unref(context.wrapClassName) || '';
return unref(fullScreenRef) ? `fullscreen-modal ${clsName} ` : unref(clsName);
});
function handleFullScreen(e: Event) {
e && e.stopPropagation();
fullScreenRef.value = !unref(fullScreenRef);
const modalWrapper = unref(context.modalWrapperRef);
if (!modalWrapper) return;
const wrapperEl = modalWrapper.$el as HTMLElement;
if (!wrapperEl) return;
const modalWrapSpinEl = wrapperEl.querySelector('.ant-spin-nested-loading') as HTMLElement;
if (!modalWrapSpinEl) return;
if (!unref(formerHeightRef) && unref(fullScreenRef)) {
formerHeightRef.value = modalWrapSpinEl.offsetHeight;
}
if (unref(fullScreenRef)) {
modalWrapSpinEl.style.height = `${window.innerHeight - unref(context.extHeightRef)}px`;
} else {
modalWrapSpinEl.style.height = `${unref(formerHeightRef)}px`;
}
}
return { getWrapClassName, handleFullScreen, fullScreenRef };
}
......@@ -5,9 +5,21 @@ import type {
ReturnMethods,
UseModalInnerReturnType,
} from './types';
import { ref, onUnmounted, unref, getCurrentInstance, reactive, watchEffect, nextTick } from 'vue';
import {
ref,
onUnmounted,
unref,
getCurrentInstance,
reactive,
watchEffect,
nextTick,
toRaw,
} from 'vue';
import { isProdMode } from '/@/utils/env';
import { isFunction } from '/@/utils/is';
import { isEqual } from 'lodash-es';
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
const dataTransferRef = reactive<any>({});
/**
......@@ -20,6 +32,7 @@ export function useModal(): UseModalReturnType {
const modalRef = ref<Nullable<ModalMethods>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
const uidRef = ref<string>('');
function register(modalMethod: ModalMethods, uuid: string) {
uidRef.value = uuid;
......@@ -52,13 +65,16 @@ export function useModal(): UseModalReturnType {
visible: visible,
});
if (data) {
dataTransferRef[unref(uidRef)] = openOnSet
? {
...data,
__t__: Date.now(),
}
: data;
if (!data) return;
if (openOnSet) {
dataTransferRef[unref(uidRef)] = null;
dataTransferRef[unref(uidRef)] = data;
return;
}
const equal = isEqual(toRaw(dataTransferRef[unref(uidRef)]), data);
if (!equal) {
dataTransferRef[unref(uidRef)] = data;
}
},
};
......@@ -66,7 +82,7 @@ export function useModal(): UseModalReturnType {
}
export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
const modalInstanceRef = ref<ModalMethods | null>(null);
const modalInstanceRef = ref<Nullable<ModalMethods>>(null);
const currentInstall = getCurrentInstance();
const uidRef = ref<string>('');
......@@ -83,6 +99,11 @@ export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
};
const register = (modalInstance: ModalMethods, uuid: string) => {
isProdMode() &&
tryOnUnmounted(() => {
modalInstanceRef.value = null;
});
uidRef.value = uuid;
modalInstanceRef.value = modalInstance;
currentInstall.emit('register', modalInstance);
......
import { InjectionKey } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext';
export interface ModalContextProps {
redoModalHeight: () => void;
}
const modalContextInjectKey: InjectionKey<ModalContextProps> = Symbol();
export function createModalContext(context: ModalContextProps) {
return createContext<ModalContextProps>(context, modalContextInjectKey);
}
export function useModalContext() {
return useContext<ModalContextProps>(modalContextInjectKey);
}
import { Ref, unref, watchEffect } from 'vue';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
export interface UseModalDragMoveContext {
draggable: Ref<boolean>;
destroyOnClose: Ref<boolean | undefined> | undefined;
visible: Ref<boolean>;
}
export function useModalDragMove(context: UseModalDragMoveContext) {
const getStyle = (dom: any, attr: any) => {
return getComputedStyle(dom)[attr];
};
const drag = (wrap: any) => {
if (!wrap) return;
wrap.setAttribute('data-drag', unref(context.draggable));
const dialogHeaderEl = wrap.querySelector('.ant-modal-header');
const dragDom = wrap.querySelector('.ant-modal');
if (!dialogHeaderEl || !dragDom || !unref(context.draggable)) return;
dialogHeaderEl.style.cursor = 'move';
dialogHeaderEl.onmousedown = (e: any) => {
if (!e) return;
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX;
const disY = e.clientY;
const screenWidth = document.body.clientWidth; // body当前宽度
const screenHeight = document.documentElement.clientHeight; // 可见区域高度(应为body高度,可某些环境下无法获取)
const dragDomWidth = dragDom.offsetWidth; // 对话框宽度
const dragDomheight = dragDom.offsetHeight; // 对话框高度
const minDragDomLeft = dragDom.offsetLeft;
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
const minDragDomTop = dragDom.offsetTop;
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
// 获取到的值带px 正则匹配替换
const domLeft = getStyle(dragDom, 'left');
const domTop = getStyle(dragDom, 'top');
let styL = +domLeft;
let styT = +domTop;
// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
if (domLeft.includes('%')) {
styL = +document.body.clientWidth * (+domLeft.replace(/%/g, '') / 100);
styT = +document.body.clientHeight * (+domTop.replace(/%/g, '') / 100);
} else {
styL = +domLeft.replace(/px/g, '');
styT = +domTop.replace(/px/g, '');
}
document.onmousemove = function (e) {
// 通过事件委托,计算移动的距离
let left = e.clientX - disX;
let top = e.clientY - disY;
// 边界处理
if (-left > minDragDomLeft) {
left = -minDragDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDragDomTop) {
top = -minDragDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
};
document.onmouseup = () => {
document.onmousemove = null;
document.onmouseup = null;
};
};
};
const handleDrag = () => {
const dragWraps = document.querySelectorAll('.ant-modal-wrap');
for (const wrap of Array.from(dragWraps)) {
if (!wrap) continue;
const display = getStyle(wrap, 'display');
const draggable = wrap.getAttribute('data-drag');
if (display !== 'none') {
// 拖拽位置
if (draggable === null || unref(context.destroyOnClose)) {
drag(wrap);
}
}
}
};
watchEffect(() => {
if (!unref(context.visible) || !unref(context.draggable)) {
return;
}
useTimeoutFn(() => {
handleDrag();
}, 30);
});
}
......@@ -65,7 +65,7 @@ export default defineComponent({
}
onMounted(() => {
tryTsxEmit((instance) => {
tryTsxEmit<any>((instance) => {
instance.wrap = unref(wrapElRef);
});
......
import type { BasicTableProps } from '../types/table';
import { computed, Ref, onMounted, unref, ref, nextTick, ComputedRef, watch } from 'vue';
import { injectModal } from '/@/components/Modal/src/provideModal';
import { getViewportOffset } from '/@/utils/domUtils';
import { isBoolean } from '/@/utils/is';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { useProps } from './useProps';
import { useModalContext } from '/@/components/Modal';
export function useTableScroll(refProps: ComputedRef<BasicTableProps>, tableElRef: Ref<any>) {
const { propsRef } = useProps(refProps);
const tableHeightRef: Ref<number | null> = ref(null);
const redoModalHeight = injectModal();
const modalFn = useModalContext();
watch(
() => unref(propsRef).canResize,
......@@ -93,7 +92,7 @@ export function useTableScroll(refProps: ComputedRef<BasicTableProps>, tableElRe
tableHeightRef.value =
tableHeightRef.value! > maxHeight! ? (maxHeight as number) : tableHeightRef.value;
// 解决表格放modal内的时候,modal自适应高度计算问题
redoModalHeight && redoModalHeight();
modalFn?.redoModalHeight?.();
}, 16);
}
......
import './index.less';
import type { ReplaceFields, TreeItem, Keys, CheckKeys } from './types';
import type { ReplaceFields, TreeItem, Keys, CheckKeys, TreeActionType } from './types';
import { defineComponent, reactive, computed, unref, ref, watchEffect, CSSProperties } from 'vue';
import { Tree } from 'ant-design-vue';
......@@ -124,7 +124,6 @@ export default defineComponent({
title: () => (
<span class={`${prefixCls}-title`}>
<span class={`${prefixCls}__content`} style={unref(getContentStyle)}>
{' '}
{titleField && anyItem[titleField]}
</span>
<span class={`${prefixCls}__actions`}> {renderAction(item)}</span>
......@@ -183,7 +182,7 @@ export default defineComponent({
state.checkedKeys = props.checkedKeys;
});
tryTsxEmit((currentInstance) => {
tryTsxEmit<TreeActionType>((currentInstance) => {
currentInstance.setExpandedKeys = setExpandedKeys;
currentInstance.getExpandedKeys = getExpandedKeys;
currentInstance.setSelectedKeys = setSelectedKeys;
......
......@@ -10,7 +10,7 @@ export function useTree(
getReplaceFields: ComputedRef<ReplaceFields>
) {
// 更新节点
function updateNodeByKey(key: string, node: TreeItem, list: TreeItem[]) {
function updateNodeByKey(key: string, node: TreeItem, list?: TreeItem[]) {
if (!key) return;
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
......@@ -75,7 +75,7 @@ export function useTree(
}
// 删除节点
function deleteNodeByKey(key: string, list: TreeItem[]) {
function deleteNodeByKey(key: string, list?: TreeItem[]) {
if (!key) return;
const treeData = list || unref(treeDataRef);
const { key: keyField, children: childrenField } = unref(getReplaceFields);
......
......@@ -6,6 +6,7 @@ import { getSlot } from '/@/utils/helper/tsxHelper';
import './DragVerify.less';
import { CheckOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
import { tryTsxEmit } from '/@/utils/helper/vueHelper';
import type { DragVerifyActionType } from './types';
export default defineComponent({
name: 'BaseDargVerify',
props: basicProps,
......@@ -210,7 +211,7 @@ export default defineComponent({
contentEl.style.width = unref(getContentStyleRef).width;
}
tryTsxEmit((instance) => {
tryTsxEmit<DragVerifyActionType>((instance) => {
instance.resume = resume;
});
......
......@@ -46,7 +46,7 @@ export function useRootSetting() {
unref(getRootSetting).contentMode === ContentEnum.FULL ? ContentEnum.FULL : ContentEnum.FIXED
);
function setRootSetting(setting: RootSetting) {
function setRootSetting(setting: Partial<RootSetting>) {
appStore.commitProjectConfigState(setting);
}
......
......@@ -7,6 +7,7 @@ import {
onUnmounted,
nextTick,
reactive,
ComponentInternalInstance,
} from 'vue';
export function explicitComputed<T, S>(source: WatchSource<S>, fn: () => T) {
......@@ -29,8 +30,10 @@ export function tryOnUnmounted(fn: () => Promise<void> | void) {
getCurrentInstance() && onUnmounted(fn);
}
export function tryTsxEmit(fn: (_instance: any) => Promise<void> | void) {
const instance = getCurrentInstance();
export function tryTsxEmit<T extends any = ComponentInternalInstance>(
fn: (_instance: T) => Promise<void> | void
) {
const instance = getCurrentInstance() as any;
instance && fn.call(null, instance);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册