mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 19:29:04 +08:00
initial commit
This commit is contained in:
5
src/components/Modal/index.ts
Normal file
5
src/components/Modal/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import './src/index.less';
|
||||
export { default as BasicModal } from './src/BasicModal';
|
||||
export { default as Modal } from './src/Modal';
|
||||
export { useModal, useModalInner } from './src/useModal';
|
||||
export * from './src/types';
|
230
src/components/Modal/src/BasicModal.tsx
Normal file
230
src/components/Modal/src/BasicModal.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import type { ModalProps, ModalMethods } from './types';
|
||||
|
||||
import Modal from './Modal';
|
||||
import { Button } from 'ant-design-vue';
|
||||
import ModalWrapper from './ModalWrapper';
|
||||
import { BasicTitle } from '/@/components/Basic';
|
||||
import { defineComponent, computed, ref, watch, unref, watchEffect } from 'vue';
|
||||
|
||||
import { FullscreenExitOutlined, FullscreenOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { basicProps } from './props';
|
||||
|
||||
import { getSlot, extendSlots } from '/@/utils/helper/tsxHelper';
|
||||
import { isFunction } from '/@/utils/is';
|
||||
import { deepMerge } from '/@/utils';
|
||||
import { buildUUID } from '/@/utils/uuid';
|
||||
|
||||
// import { triggerWindowResize } from '@/utils/event/triggerWindowResizeEvent';
|
||||
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<any>(null);
|
||||
|
||||
// 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),
|
||||
};
|
||||
});
|
||||
// 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,
|
||||
};
|
||||
});
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
const { useWrapper, loading, wrapperProps } = unref(getProps);
|
||||
return useWrapper ? (
|
||||
<ModalWrapper
|
||||
footerOffset={props.wrapperFooterOffset}
|
||||
fullScreen={unref(fullScreenRef)}
|
||||
ref={modalWrapperRef}
|
||||
loading={loading}
|
||||
visible={unref(visibleRef)}
|
||||
{...wrapperProps}
|
||||
onGetExtHeight={(height: number) => {
|
||||
extHeightRef.value = height;
|
||||
}}
|
||||
onHeightChange={(height: string) => {
|
||||
emit('height-change', height);
|
||||
}}
|
||||
>
|
||||
{() => getSlot(slots)}
|
||||
</ModalWrapper>
|
||||
) : (
|
||||
getSlot(slots)
|
||||
);
|
||||
}
|
||||
// 取消事件
|
||||
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');
|
||||
}
|
||||
// 底部按钮自定义实现,
|
||||
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);
|
||||
if (!canFullscreen) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div class="custom-close-icon">
|
||||
{unref(fullScreenRef) ? (
|
||||
<FullscreenExitOutlined role="full" onClick={handleFullScreen} />
|
||||
) : (
|
||||
<FullscreenOutlined role="close" onClick={handleFullScreen} />
|
||||
)}
|
||||
<CloseOutlined onClick={handleCancel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function handleFullScreen(e: Event) {
|
||||
e.stopPropagation();
|
||||
fullScreenRef.value = !unref(fullScreenRef);
|
||||
|
||||
const modalWrapper = unref(modalWrapperRef);
|
||||
if (modalWrapper) {
|
||||
const modalWrapSpinEl = (modalWrapper.$el as HTMLElement).querySelector(
|
||||
'.ant-spin-nested-loading'
|
||||
);
|
||||
if (modalWrapSpinEl) {
|
||||
if (!unref(formerHeightRef) && unref(fullScreenRef)) {
|
||||
formerHeightRef.value = (modalWrapSpinEl as HTMLElement).offsetHeight;
|
||||
console.log(formerHeightRef);
|
||||
}
|
||||
if (unref(fullScreenRef)) {
|
||||
(modalWrapSpinEl as HTMLElement).style.height = `${
|
||||
window.innerHeight - unref(extHeightRef)
|
||||
}px`;
|
||||
} else {
|
||||
(modalWrapSpinEl as HTMLElement).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')) {
|
||||
visibleRef.value = !!props.visible;
|
||||
}
|
||||
}
|
||||
|
||||
const modalMethods: ModalMethods = {
|
||||
setModalProps,
|
||||
};
|
||||
const uuid = buildUUID();
|
||||
emit('register', modalMethods, uuid);
|
||||
return () => (
|
||||
<Modal onCancel={handleCancel} {...{ ...attrs, ...props, ...unref(getProps) }}>
|
||||
{{
|
||||
...extendSlots(slots, ['default']),
|
||||
default: () => renderContent(),
|
||||
closeIcon: () => renderClose(),
|
||||
footer: () => renderFooter(),
|
||||
title: () => renderTitle(),
|
||||
}}
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
});
|
109
src/components/Modal/src/Modal.tsx
Normal file
109
src/components/Modal/src/Modal.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { defineComponent, watchEffect } from 'vue';
|
||||
import { basicProps } from './props';
|
||||
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||
import { extendSlots } from '/@/utils/helper/tsxHelper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Modal',
|
||||
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) => {
|
||||
// 鼠标按下,计算当前元素距离可视区的距离
|
||||
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) {
|
||||
const display = getStyle(wrap, 'display');
|
||||
const draggable = wrap.getAttribute('data-drag');
|
||||
if (display !== 'none') {
|
||||
// 拖拽位置
|
||||
draggable === null && drag(wrap);
|
||||
}
|
||||
}
|
||||
};
|
||||
watchEffect(() => {
|
||||
if (!props.visible) {
|
||||
return;
|
||||
}
|
||||
// context.$nextTick();
|
||||
useTimeout(() => {
|
||||
handleDrag();
|
||||
}, 30);
|
||||
});
|
||||
|
||||
return () => {
|
||||
const propsData = { ...attrs, ...props } as any;
|
||||
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
|
||||
};
|
||||
},
|
||||
});
|
188
src/components/Modal/src/ModalWrapper.tsx
Normal file
188
src/components/Modal/src/ModalWrapper.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { PropType } from 'vue';
|
||||
import type { ModalWrapperProps } from './types';
|
||||
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
ref,
|
||||
watchEffect,
|
||||
unref,
|
||||
watch,
|
||||
onMounted,
|
||||
nextTick,
|
||||
onUnmounted,
|
||||
} from 'vue';
|
||||
import { Spin } from 'ant-design-vue';
|
||||
import { ScrollContainer } from '/@/components/Container/index';
|
||||
|
||||
import { useWindowSizeFn } from '/@/hooks/event/useWindowSize';
|
||||
import { useTimeout } from '/@/hooks/core/useTimeout';
|
||||
|
||||
import { getSlot } from '/@/utils/helper/tsxHelper';
|
||||
import { useElResize } from '/@/hooks/event/useElResize';
|
||||
export default defineComponent({
|
||||
name: 'ModalWrapper',
|
||||
emits: ['heightChange', 'getExtHeight'],
|
||||
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,
|
||||
},
|
||||
},
|
||||
setup(props: ModalWrapperProps, { slots, emit }) {
|
||||
const wrapperRef = ref<HTMLElement | null>(null);
|
||||
const spinRef = ref<any>(null);
|
||||
const realHeightRef = ref(0);
|
||||
|
||||
const wrapStyle = computed(() => {
|
||||
return {
|
||||
minHeight: `${props.minHeight}px`,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
});
|
||||
|
||||
// 重试次数
|
||||
let tryCount = 0;
|
||||
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) {
|
||||
useTimeout(() => {
|
||||
// retry
|
||||
if (tryCount < 3) {
|
||||
setModalHeight();
|
||||
}
|
||||
tryCount++;
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
tryCount = 0;
|
||||
|
||||
const realHeight = (spinEl.$el.querySelector('.ant-spin-container') as HTMLElement)
|
||||
.scrollHeight;
|
||||
|
||||
// 16为 p-2和m-2 加起来为4,基础4, 4*4=16
|
||||
// 32 padding
|
||||
if (props.fullScreen) {
|
||||
realHeightRef.value =
|
||||
window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight - 26;
|
||||
} 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();
|
||||
});
|
||||
start();
|
||||
onUnmounted(() => {
|
||||
stop();
|
||||
});
|
||||
}
|
||||
nextTick(() => {});
|
||||
watchEffect(() => {
|
||||
setModalHeight();
|
||||
});
|
||||
watch(
|
||||
() => props.fullScreen,
|
||||
(v) => {
|
||||
!v && setModalHeight();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const { modalHeaderHeight, modalFooterHeight } = props;
|
||||
emit('getExtHeight', modalHeaderHeight + modalFooterHeight);
|
||||
listenElResize();
|
||||
});
|
||||
|
||||
useWindowSizeFn(setModalHeight);
|
||||
|
||||
return () => {
|
||||
const height = unref(realHeightRef);
|
||||
return (
|
||||
<div ref={wrapperRef} style={unref(wrapStyle)}>
|
||||
<ScrollContainer>
|
||||
{() => (
|
||||
<Spin ref={spinRef} spinning={props.loading} style={{ height: `${height}px` }}>
|
||||
{() => getSlot(slots)}
|
||||
</Spin>
|
||||
)}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
151
src/components/Modal/src/index.less
Normal file
151
src/components/Modal/src/index.less
Normal file
@@ -0,0 +1,151 @@
|
||||
@import (reference) '../../../design/index.less';
|
||||
|
||||
.fullscreen-modal {
|
||||
overflow: hidden;
|
||||
|
||||
.ant-modal {
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal {
|
||||
width: 520px;
|
||||
padding-bottom: 0;
|
||||
|
||||
.ant-spin-nested-loading {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
|
||||
.base-title {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-close-icon {
|
||||
display: flex;
|
||||
height: 95%;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
& span:nth-child(1) {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
& span:nth-child(2) {
|
||||
&:hover {
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
// background: #f1f2f6;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-large {
|
||||
top: 60px;
|
||||
|
||||
&--mini {
|
||||
top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
// padding: 12.5px 24px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
&-footer {
|
||||
padding: 10px 26px 26px 16px;
|
||||
// border-top: none;
|
||||
|
||||
button + button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&-close {
|
||||
font-weight: normal;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&-close-x {
|
||||
display: inline-block;
|
||||
width: 96px;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
}
|
||||
|
||||
&-confirm-body {
|
||||
.ant-modal-confirm-content {
|
||||
color: @text-color-help-dark;
|
||||
|
||||
> * {
|
||||
color: @text-color-help-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-confirm-confirm.error .ant-modal-confirm-body > .anticon {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&-confirm-btns {
|
||||
.ant-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-confirm-info {
|
||||
.ant-modal-confirm-body > .anticon {
|
||||
color: @warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-confirm-confirm.success {
|
||||
.ant-modal-confirm-body > .anticon {
|
||||
color: @success-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-confirm .ant-modal-body {
|
||||
padding: 24px !important;
|
||||
}
|
||||
@media screen and (max-height: 600px) {
|
||||
.ant-modal {
|
||||
top: 60px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-height: 540px) {
|
||||
.ant-modal {
|
||||
top: 30px;
|
||||
}
|
||||
}
|
||||
@media screen and (max-height: 480px) {
|
||||
.ant-modal {
|
||||
top: 10px;
|
||||
}
|
||||
}
|
122
src/components/Modal/src/props.ts
Normal file
122
src/components/Modal/src/props.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { PropType } from 'vue';
|
||||
export const modalProps = {
|
||||
visible: Boolean as PropType<boolean>,
|
||||
// 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: '关闭',
|
||||
},
|
||||
okText: {
|
||||
type: String as PropType<string>,
|
||||
default: '保存',
|
||||
},
|
||||
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,
|
||||
},
|
||||
// After enabling the wrapper, the bottom can be increased in height
|
||||
wrapperFooterOffset: {
|
||||
type: Number as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
// Warm reminder message
|
||||
helpMessage: [String, Array] as PropType<string | string[]>,
|
||||
// Whether to use wrapper
|
||||
useWrapper: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* @description: Show close button
|
||||
*/
|
||||
showCancelBtn: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* @description: Show confirmation button
|
||||
*/
|
||||
showOkBtn: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
|
||||
wrapperProps: Object as PropType<any>,
|
||||
|
||||
afterClose: Function as PropType<() => Promise<any>>,
|
||||
|
||||
bodyStyle: Object as PropType<any>,
|
||||
|
||||
closable: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
|
||||
closeIcon: Object as PropType<any>,
|
||||
|
||||
confirmLoading: Boolean as PropType<boolean>,
|
||||
|
||||
destroyOnClose: Boolean as PropType<boolean>,
|
||||
|
||||
footer: Object as PropType<any>,
|
||||
|
||||
getContainer: Function as PropType<() => any>,
|
||||
|
||||
mask: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
|
||||
maskClosable: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
keyboard: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: true,
|
||||
},
|
||||
|
||||
maskStyle: Object as PropType<any>,
|
||||
|
||||
okType: {
|
||||
type: String as PropType<string>,
|
||||
default: 'primary',
|
||||
},
|
||||
|
||||
okButtonProps: Object as PropType<any>,
|
||||
|
||||
cancelButtonProps: Object as PropType<any>,
|
||||
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
},
|
||||
|
||||
visible: Boolean as PropType<boolean>,
|
||||
|
||||
width: [String, Number] as PropType<string | number>,
|
||||
|
||||
wrapClassName: {
|
||||
type: String as PropType<string>,
|
||||
},
|
||||
|
||||
zIndex: {
|
||||
type: Number as PropType<number>,
|
||||
},
|
||||
});
|
195
src/components/Modal/src/types.ts
Normal file
195
src/components/Modal/src/types.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { ButtonProps } from 'ant-design-vue/types/button/button';
|
||||
import type { CSSProperties, VNodeChild } from 'vue';
|
||||
/**
|
||||
* @description: 弹窗对外暴露的方法
|
||||
*/
|
||||
export interface ModalMethods {
|
||||
setModalProps: (props: Partial<ModalProps>) => void;
|
||||
}
|
||||
|
||||
export type RegisterFn = (modalMethods: ModalMethods, uuid?: string) => void;
|
||||
export interface ReturnMethods extends ModalMethods {
|
||||
openModal: (props?: boolean) => void;
|
||||
transferModalData: (data: any) => void;
|
||||
}
|
||||
export type UseModalReturnType = [RegisterFn, ReturnMethods];
|
||||
|
||||
export interface ReturnInnerMethods extends ModalMethods {
|
||||
closeModal: () => void;
|
||||
changeLoading: (loading: boolean) => void;
|
||||
changeOkLoading: (loading: boolean) => void;
|
||||
receiveModalDataRef: any;
|
||||
}
|
||||
export type UseModalInnerReturnType = [RegisterFn, ReturnInnerMethods];
|
||||
|
||||
export interface ModalProps {
|
||||
// 启用wrapper后 底部可以适当增加高度
|
||||
wrapperFooterOffset?: number;
|
||||
draggable?: boolean;
|
||||
|
||||
// 是否可以进行全屏
|
||||
canFullscreen?: boolean;
|
||||
visible?: boolean;
|
||||
// 温馨提醒信息
|
||||
helpMessage: string | string[];
|
||||
|
||||
// 是否使用modalWrapper
|
||||
useWrapper: boolean;
|
||||
|
||||
loading: boolean;
|
||||
|
||||
wrapperProps: Omit<ModalWrapperProps, 'loading'>;
|
||||
|
||||
showOkBtn: boolean;
|
||||
showCancelBtn: boolean;
|
||||
closeFunc: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Specify a function that will be called when modal is closed completely.
|
||||
* @type Function
|
||||
*/
|
||||
afterClose?: () => any;
|
||||
|
||||
/**
|
||||
* Body style for modal body element. Such as height, padding etc.
|
||||
* @default {}
|
||||
* @type object
|
||||
*/
|
||||
bodyStyle?: CSSProperties;
|
||||
|
||||
/**
|
||||
* Text of the Cancel button
|
||||
* @default 'cancel'
|
||||
* @type string
|
||||
*/
|
||||
cancelText?: string;
|
||||
|
||||
/**
|
||||
* Centered Modal
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
centered?: boolean;
|
||||
|
||||
/**
|
||||
* Whether a close (x) button is visible on top right of the modal dialog or not
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
closable?: boolean;
|
||||
/**
|
||||
* Whether a close (x) button is visible on top right of the modal dialog or not
|
||||
*/
|
||||
closeIcon?: VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* Whether to apply loading visual effect for OK button or not
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
confirmLoading?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to unmount child components on onClose
|
||||
* @default false
|
||||
* @type boolean
|
||||
*/
|
||||
destroyOnClose?: boolean;
|
||||
|
||||
/**
|
||||
* Footer content, set as :footer="null" when you don't need default buttons
|
||||
* @default OK and Cancel buttons
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
footer?: VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* Return the mount node for Modal
|
||||
* @default () => document.body
|
||||
* @type Function
|
||||
*/
|
||||
getContainer?: (instance: any) => HTMLElement;
|
||||
|
||||
/**
|
||||
* Whether show mask or not.
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
mask?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to close the modal dialog when the mask (area outside the modal) is clicked
|
||||
* @default true
|
||||
* @type boolean
|
||||
*/
|
||||
maskClosable?: boolean;
|
||||
|
||||
/**
|
||||
* Style for modal's mask element.
|
||||
* @default {}
|
||||
* @type object
|
||||
*/
|
||||
maskStyle?: CSSProperties;
|
||||
|
||||
/**
|
||||
* Text of the OK button
|
||||
* @default 'OK'
|
||||
* @type string
|
||||
*/
|
||||
okText?: string;
|
||||
|
||||
/**
|
||||
* Button type of the OK button
|
||||
* @default 'primary'
|
||||
* @type string
|
||||
*/
|
||||
okType?: 'primary' | 'danger' | 'dashed' | 'ghost' | 'default';
|
||||
|
||||
/**
|
||||
* The ok button props, follow jsx rules
|
||||
* @type object
|
||||
*/
|
||||
okButtonProps?: ButtonProps;
|
||||
|
||||
/**
|
||||
* The cancel button props, follow jsx rules
|
||||
* @type object
|
||||
*/
|
||||
cancelButtonProps?: ButtonProps;
|
||||
|
||||
/**
|
||||
* The modal dialog's title
|
||||
* @type any (string | slot)
|
||||
*/
|
||||
title?: VNodeChild | JSX.Element;
|
||||
|
||||
/**
|
||||
* Width of the modal dialog
|
||||
* @default 520
|
||||
* @type string | number
|
||||
*/
|
||||
width?: string | number;
|
||||
|
||||
/**
|
||||
* The class name of the container of the modal dialog
|
||||
* @type string
|
||||
*/
|
||||
wrapClassName?: string;
|
||||
|
||||
/**
|
||||
* The z-index of the Modal
|
||||
* @default 1000
|
||||
* @type number
|
||||
*/
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export interface ModalWrapperProps {
|
||||
footerOffset?: number;
|
||||
loading: boolean;
|
||||
modalHeaderHeight: number;
|
||||
modalFooterHeight: number;
|
||||
minHeight: number;
|
||||
visible: boolean;
|
||||
fullScreen: boolean;
|
||||
}
|
99
src/components/Modal/src/useModal.ts
Normal file
99
src/components/Modal/src/useModal.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
UseModalReturnType,
|
||||
ModalMethods,
|
||||
ModalProps,
|
||||
ReturnMethods,
|
||||
UseModalInnerReturnType,
|
||||
} from './types';
|
||||
import { ref, onUnmounted, unref, getCurrentInstance, reactive, computed } from 'vue';
|
||||
import { isProdMode } from '/@/utils/env';
|
||||
const dataTransferRef = reactive<any>({});
|
||||
|
||||
/**
|
||||
* @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!');
|
||||
}
|
||||
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;
|
||||
isProdMode() &&
|
||||
onUnmounted(() => {
|
||||
modalRef.value = null;
|
||||
loadedRef.value = false;
|
||||
dataTransferRef[unref(uidRef)] = null;
|
||||
});
|
||||
if (unref(loadedRef) && isProdMode() && modalMethod === unref(modalRef)) {
|
||||
return;
|
||||
}
|
||||
modalRef.value = modalMethod;
|
||||
}
|
||||
const getInstance = () => {
|
||||
const instance = unref(modalRef);
|
||||
if (!instance) {
|
||||
throw new Error('instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
|
||||
const methods: ReturnMethods = {
|
||||
setModalProps: (props: Partial<ModalProps>): void => {
|
||||
getInstance().setModalProps(props);
|
||||
},
|
||||
openModal: (visible = true): void => {
|
||||
getInstance().setModalProps({
|
||||
visible: visible,
|
||||
});
|
||||
},
|
||||
transferModalData(val: any) {
|
||||
dataTransferRef[unref(uidRef)] = val;
|
||||
},
|
||||
};
|
||||
return [register, methods];
|
||||
}
|
||||
|
||||
export const useModalInner = (): UseModalInnerReturnType => {
|
||||
const modalInstanceRef = ref<ModalMethods | null>(null);
|
||||
const currentInstall = getCurrentInstance();
|
||||
const uidRef = ref<string>('');
|
||||
|
||||
if (!currentInstall) {
|
||||
throw new Error('instance is undefined!');
|
||||
}
|
||||
const getInstance = () => {
|
||||
const instance = unref(modalInstanceRef);
|
||||
if (!instance) {
|
||||
throw new Error('instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
const register = (modalInstance: ModalMethods, uuid: string) => {
|
||||
uidRef.value = uuid;
|
||||
modalInstanceRef.value = modalInstance;
|
||||
currentInstall.emit('register', modalInstance);
|
||||
};
|
||||
return [
|
||||
register,
|
||||
{
|
||||
receiveModalDataRef: computed(() => {
|
||||
return dataTransferRef[unref(uidRef)];
|
||||
}),
|
||||
changeLoading: (loading = true) => {
|
||||
getInstance().setModalProps({ loading });
|
||||
},
|
||||
changeOkLoading: (loading = true) => {
|
||||
getInstance().setModalProps({ confirmLoading: loading });
|
||||
},
|
||||
closeModal: () => {
|
||||
getInstance().setModalProps({ visible: false });
|
||||
},
|
||||
setModalProps: (props: Partial<ModalProps>) => {
|
||||
getInstance().setModalProps(props);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
Reference in New Issue
Block a user