新增前端vue

This commit is contained in:
2025-11-26 13:55:01 +08:00
parent ae391f1b94
commit ffd5a6ad66
781 changed files with 83348 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
import { withInstall } from '@jeesite/core/utils';
import basicModal from './src/BasicModal.vue';
export const BasicModal = withInstall(basicModal);
export type BasicModalInstance = InstanceType<typeof basicModal>;
export * from './src/typing';
export { useModalContext } from './src/hooks/useModalContext';
export { useModal, useModalInner } from './src/hooks/useModal';

View File

@@ -0,0 +1,416 @@
<template>
<Modal v-bind="getBindValue" @cancel="handleCancel">
<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" @dblclick="handleTitleDbClick">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</ModalHeader>
</template>
<template #footer v-if="!$slots.footer">
<ModalFooter v-bind="getBindValue" @ok="handleOk" @cancel="handleCancel">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</ModalFooter>
</template>
<ModalWrapper
:useWrapper="getProps.useWrapper"
:footerOffset="wrapperFooterOffset"
:fullScreen="fullScreenRef"
ref="modalWrapperRef"
:loading="getProps.loading"
:loading-tip="getProps.loadingTip"
:minHeight="getProps.minHeight"
:height="getWrapperHeight"
:open="openRef"
:modalFooterHeight="footer !== undefined && !footer ? 0 : undefined"
v-bind="omit(getProps.wrapperProps, 'open', 'height', 'modalFooterHeight')"
@ext-height="handleExtHeight"
@height-change="handleHeightChange"
>
<slot></slot>
</ModalWrapper>
<template #[item]="data" v-for="item in Object.keys(omit($slots, 'default'))">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</Modal>
</template>
<script lang="ts" setup name="BasicModal">
import type { ModalProps, ModalMethods } from './typing';
import { computed, ref, watch, unref, watchEffect, toRef, getCurrentInstance, nextTick, useAttrs } 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 '@jeesite/core/utils/is';
// import { deepMerge } from '@jeesite/core/utils';
import { basicProps } from './props';
import { useFullScreen } from './hooks/useModalFullScreen';
import { omit } from 'lodash-es';
defineOptions({
inheritAttrs: false,
});
const props = defineProps(basicProps);
const emit = defineEmits(['open-change', 'height-change', 'cancel', 'ok', 'register', 'update:open']);
const attrs = useAttrs();
const openRef = ref(false);
const propsRef = ref<Partial<ModalProps> | null>(null);
const modalWrapperRef = ref<any>(null);
// modal Bottom and top height
const extHeightRef = ref(0);
const modalMethods: ModalMethods = {
getModalProps,
setModalProps,
emitOpen: undefined,
redoModalHeight: () => {
nextTick(() => {
if (unref(modalWrapperRef)) {
(unref(modalWrapperRef) as any).setModalHeight();
}
});
},
};
const instance = getCurrentInstance();
if (instance) {
emit('register', modalMethods, instance.uid);
}
// Custom title component: get title
const getMergeProps = computed((): Recordable => {
return {
...props,
...(unref(propsRef) as any),
};
});
const { handleFullScreen, getWrapClassName, fullScreenRef } = useFullScreen({
modalWrapperRef,
extHeightRef,
wrapClassName: toRef(getMergeProps.value, 'wrapClassName'),
});
// modal component does not need title and origin buttons
const getProps = computed((): Recordable => {
const opt = {
...unref(getMergeProps),
// open: unref(openRef),
okButtonProps: undefined,
cancelButtonProps: undefined,
title: undefined,
};
return {
...opt,
wrapClassName: unref(getWrapClassName),
};
});
const getBindValue = computed((): Recordable => {
const values = {
...attrs,
...unref(getMergeProps),
open: unref(openRef),
wrapClassName: unref(getWrapClassName),
maskTransitionName: 'ant-fade',
transitionName: 'ant-fade',
} as any;
if (typeof values?.width === 'string') {
let width = Number(values.width);
if (!isNaN(width)) values.width = width;
}
if (unref(fullScreenRef)) {
return omit(values, ['height', 'title']);
}
return omit(values, 'title');
});
const getWrapperHeight = computed(() => {
if (unref(fullScreenRef)) return undefined;
return unref(getProps).height;
});
watchEffect(() => {
openRef.value = !!props.open;
fullScreenRef.value = !!props.defaultFullscreen;
});
watch(
() => unref(openRef),
(v) => {
emit('open-change', v);
emit('update:open', v);
instance && modalMethods.emitOpen?.(v, instance.uid);
nextTick(() => {
if (props.scrollTop && v && unref(modalWrapperRef)) {
(unref(modalWrapperRef) as any).scrollTop();
}
});
},
{
immediate: false,
},
);
// 取消事件
async function handleCancel(e: Event) {
e?.stopPropagation();
if (props.closeFunc && isFunction(props.closeFunc)) {
const isClose: boolean = await props.closeFunc();
openRef.value = !isClose;
return;
}
openRef.value = false;
emit('cancel', e);
}
function handleOk(e: Event) {
emit('ok', e);
}
function handleHeightChange(height: string) {
emit('height-change', height);
}
function handleExtHeight(height: number) {
extHeightRef.value = height;
}
function handleTitleDbClick(e) {
if (!props.canFullscreen) return;
e.stopPropagation();
handleFullScreen(e);
}
function getModalProps(): Partial<ModalProps> {
return getProps.value;
}
function setModalProps(props: Partial<ModalProps>): void {
if (typeof props.loading != 'undefined') {
props.confirmLoading = props.loading;
}
// Keep the last setModalProps
// propsRef.value = deepMerge(unref(propsRef) || ({} as any), props);
propsRef.value = { ...(unref(propsRef) as Recordable), ...props } as Recordable;
if (Reflect.has(props, 'open')) {
openRef.value = !!props.open;
}
if (Reflect.has(props, 'defaultFullscreen')) {
fullScreenRef.value = !!props.defaultFullscreen;
}
}
defineExpose({
open: (loading = false) => {
setModalProps({ open: true, loading });
},
close: () => {
setModalProps({ open: false });
},
loading: () => {
setModalProps({ loading: true });
},
closeLoading: () => {
setModalProps({ loading: false });
},
confirmLoading: () => {
setModalProps({ confirmLoading: true });
},
closeConfirmLoading: () => {
setModalProps({ confirmLoading: false });
},
getProps: getModalProps,
setProps: setModalProps,
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-basic-modal';
.ant-modal.@{prefix-cls} {
.ant-modal {
&-body {
padding: 0;
background-color: @component-background;
> .scrollbar {
> .scrollbar__wrap {
> .scrollbar__view {
margin: 12px 15px 1px;
> div {
> form:first-child {
margin-top: 13px;
margin-right: 5px;
}
}
}
}
> .is-horizontal {
display: none;
}
}
}
&-title {
font-size: 16px;
font-weight: normal;
line-height: 16px;
.base-title {
cursor: move !important;
}
.anticon {
color: @primary-color;
}
}
&-large {
top: 60px;
&--mini {
top: 16px;
}
}
&-content {
box-shadow:
0 4px 8px 0 rgb(0 0 0 / 20%),
0 6px 20px 0 rgb(0 0 0 / 19%);
padding: 0;
}
&-header {
padding: 14px;
margin-bottom: 0;
border-bottom: 1px solid fade(@border-color-base, 50%);
}
&-footer {
padding: 14px;
margin-top: 0;
// border-top: 1px solid fade(@border-color-base, 50%);
button + button {
margin-left: 10px;
}
}
&-close {
position: absolute;
top: 0;
right: 0;
width: auto;
height: auto;
font-weight: normal;
outline: none;
&:hover {
background-color: transparent;
}
}
// 注释掉,防止点击全屏误触关闭
//&-close-x {
// display: inline-block;
// width: 96px;
// height: 55px;
// line-height: 55px;
//}
&-confirm-body {
.ant-modal-confirm-content {
// color: #fff;
> * {
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;
}
}
// .ant-modal.@{prefix-cls} {
// top: 150px !important;
// vertical-align: top !important;
// }
// @media screen and (max-height: 600px) {
// .ant-modal.@{prefix-cls} {
// top: 60px !important;
// }
// }
// @media screen and (max-height: 540px) {
// .ant-modal.@{prefix-cls} {
// top: 30px !important;
// }
// }
// @media screen and (max-height: 480px) {
// .ant-modal.@{prefix-cls} {
// top: 10px !important;
// }
// }
.fullscreen-modal {
overflow: hidden;
.ant-modal {
top: 0 !important; /* stylelint-disable-line */
right: 0 !important; /* stylelint-disable-line */
bottom: 0 !important; /* stylelint-disable-line */
left: 0 !important; /* stylelint-disable-line */
width: 100% !important;
max-width: calc(100vw - 15px) !important;
&-content {
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,26 @@
import { Modal } from 'ant-design-vue';
import { defineComponent, toRefs, unref } from 'vue';
import { basicProps } from '../props';
import { useModalDragMove } from '../hooks/useModalDrag';
import { useAttrs } from '@jeesite/core/hooks/core/useAttrs';
import { extendSlots } from '@jeesite/core/utils/helper/tsxHelper';
export default defineComponent({
name: 'Modal',
inheritAttrs: false,
props: basicProps,
setup(props, { slots }) {
const { open, draggable, destroyOnClose } = toRefs(props);
const attrs = useAttrs();
useModalDragMove({
open,
destroyOnClose,
draggable,
});
return () => {
const propsData = { class: 'jeesite-basic-modal', ...unref(attrs), ...props } as Recordable;
return <Modal {...propsData}>{extendSlots(slots)}</Modal>;
};
},
});

View File

@@ -0,0 +1,91 @@
<template>
<div :class="getClass">
<template v-if="canFullscreen">
<Tooltip :title="t('component.modal.restore')" placement="bottom" v-if="fullScreen">
<Icon icon="i-ant-design:fullscreen-exit-outlined" role="full" @click="handleFullScreen" />
</Tooltip>
<Tooltip :title="t('component.modal.maximize')" placement="bottom" v-else>
<Icon icon="i-ant-design:fullscreen-outlined" role="close" @click="handleFullScreen" />
</Tooltip>
</template>
<Tooltip :title="t('component.modal.close')" placement="bottom">
<Icon icon="i-ant-design:close-outlined" class="anticon-close cursor-pointer" @click="handleCancel" />
</Tooltip>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { Icon } from '@jeesite/core/components/Icon';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { Tooltip } from 'ant-design-vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
export default defineComponent({
name: 'ModalClose',
components: { Tooltip, Icon },
props: {
canFullscreen: { type: Boolean, default: true },
fullScreen: { type: Boolean },
},
emits: ['cancel', 'fullscreen'],
setup(props, { emit }) {
const { prefixCls } = useDesign('basic-modal-close');
const { t } = useI18n();
const getClass = computed(() => {
return [
prefixCls,
`${prefixCls}--custom`,
{
[`${prefixCls}--can-full`]: props.canFullscreen,
},
];
});
function handleCancel(e: Event) {
emit('cancel', e);
}
function handleFullScreen(e: Event) {
e?.stopPropagation();
e?.preventDefault();
emit('fullscreen');
}
return {
t,
getClass,
prefixCls,
handleCancel,
handleFullScreen,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'jeesite-basic-modal-close';
.@{prefix-cls} {
float: right;
text-align: right;
margin-right: 10px;
.jeesite-icon {
padding: 16px 9px;
font-size: 18px;
color: @text-color-base !important;
cursor: pointer;
opacity: 0.6;
&:hover {
opacity: 1;
}
}
.anticon-close {
&:hover {
color: @error-color !important;
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<slot name="insertFooter"></slot>
<a-button v-bind="cancelButtonProps" @click="handleCancel" v-if="showCancelBtn">
<Icon icon="i-ant-design:close-outlined" />
{{ cancelText || (getOkAuth && showOkBtn ? t('common.cancelText') : t('common.closeText')) }}
</a-button>
<slot name="centerFooter"></slot>
<a-button
:type="okType"
@click="handleOk"
:loading="confirmLoading"
v-bind="okButtonProps"
v-if="showOkBtn && getOkAuth"
>
<Icon icon="i-ant-design:check-outlined" />
{{ okText || t('common.okText') }}
</a-button>
<slot name="appendFooter"></slot>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { Icon } from '@jeesite/core/components/Icon';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { usePermission } from '@jeesite/core/hooks/web/usePermission';
import { basicProps } from '../props';
export default defineComponent({
name: 'BasicModalFooter',
components: { Icon },
props: basicProps,
emits: ['ok', 'cancel'],
setup(props, { emit }) {
const { t } = useI18n();
const { hasPermission } = usePermission();
const getOkAuth = computed(() => {
return hasPermission(props.okAuth);
});
function handleOk(e: Event) {
emit('ok', e);
}
function handleCancel(e: Event) {
emit('cancel', e);
}
return { t, getOkAuth, handleOk, handleCancel };
},
});
</script>

View File

@@ -0,0 +1,22 @@
<template>
<BasicTitle :helpMessage="helpMessage">
{{ title }}
<slot name="appendHeader"></slot>
</BasicTitle>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import { BasicTitle } from '@jeesite/core/components/Basic';
export default defineComponent({
name: 'BasicModalHeader',
components: { BasicTitle },
props: {
helpMessage: {
type: [String, Array] as PropType<string | string[]>,
},
title: { type: String },
},
});
</script>

View File

@@ -0,0 +1,154 @@
<template>
<ScrollContainer ref="wrapperRef">
<div ref="spinRef" :style="spinStyle" v-loading="loading" :loading-tip="loadingTip">
<slot></slot>
</div>
</ScrollContainer>
</template>
<script lang="ts">
import type { CSSProperties } from 'vue';
import { defineComponent, computed, ref, watchEffect, unref, watch, onMounted, nextTick, onUnmounted } from 'vue';
import { useWindowSizeFn } from '@jeesite/core/hooks/event/useWindowSizeFn';
import { ScrollContainer } from '@jeesite/core/components/Container';
import { createModalContext } from '../hooks/useModalContext';
import { useMutationObserver } from '@vueuse/core';
const props = {
loading: { type: Boolean },
useWrapper: { type: Boolean, default: true },
modalHeaderHeight: { type: Number, default: 57 },
modalFooterHeight: { type: Number, default: 74 },
minHeight: { type: Number, default: 100 },
height: { type: Number },
footerOffset: { type: Number, default: 0 },
open: { type: Boolean },
fullScreen: { type: Boolean },
loadingTip: { type: String },
};
export default defineComponent({
name: 'ModalWrapper',
components: { ScrollContainer },
inheritAttrs: false,
props,
emits: ['height-change', 'ext-height'],
setup(props, { emit }) {
const wrapperRef = ref<ComponentRef>(null);
const spinRef = ref<ElRef>(null);
const realHeightRef = ref(0);
const minRealHeightRef = ref(0);
let realHeight = 0;
let stopElResizeFn: Fn = () => {};
useWindowSizeFn(setModalHeight.bind(null));
useMutationObserver(
spinRef,
() => {
setModalHeight();
},
{
attributes: true,
subtree: true,
},
);
createModalContext({
redoModalHeight: setModalHeight,
});
const spinStyle = computed((): CSSProperties => {
return {
minHeight: `${props.minHeight}px`,
[props.fullScreen ? 'height' : 'maxHeight']: `${unref(realHeightRef)}px`,
};
});
watchEffect(() => {
props.useWrapper && setModalHeight();
});
watch(
() => props.fullScreen,
(v) => {
setModalHeight();
if (!v) {
realHeightRef.value = minRealHeightRef.value;
} else {
minRealHeightRef.value = realHeightRef.value;
}
},
);
onMounted(() => {
const { modalHeaderHeight, modalFooterHeight } = props;
emit('ext-height', modalHeaderHeight + modalFooterHeight);
});
onUnmounted(() => {
stopElResizeFn && stopElResizeFn();
});
async function scrollTop() {
nextTick(() => {
const wrapperRefDom = unref(wrapperRef);
if (!wrapperRefDom) return;
(wrapperRefDom as any)?.scrollTo?.(0);
});
}
async function setModalHeight() {
// 解决在弹窗关闭的时候监听还存在,导致再次打开弹窗没有高度
// 加上这个,就必须在使用的时候传递父级的open
if (!props.open) 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 as Element).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;
await nextTick();
// if (!realHeight) {
realHeight = spinEl.scrollHeight;
// }
if (props.fullScreen) {
realHeightRef.value = window.innerHeight - props.modalFooterHeight - props.modalHeaderHeight - 28;
} else {
realHeightRef.value = props.height ? props.height : realHeight > maxHeight ? maxHeight : realHeight;
}
emit('height-change', unref(realHeightRef));
} catch (error) {
console.log(error);
}
}
return { wrapperRef, spinRef, spinStyle, scrollTop, setModalHeight };
},
});
</script>

View File

@@ -0,0 +1,170 @@
import type { UseModalReturnType, ModalMethods, ModalProps, ReturnMethods, UseModalInnerReturnType } from '../typing';
import { ref, onUnmounted, unref, getCurrentInstance, reactive, watchEffect, nextTick, toRaw } from 'vue';
import { isProdMode } from '@jeesite/core/utils/env';
import { isFunction } from '@jeesite/core/utils/is';
import { isEqual } from 'lodash-es';
import { tryOnUnmounted } from '@vueuse/core';
import { error } from '@jeesite/core/utils/log';
import { computed } from 'vue';
const dataTransfer = reactive<any>({});
const openData = reactive<{ [key: number]: boolean }>({});
/**
* @description: Applicable to independent modal and call outside
*/
export function useModal(): UseModalReturnType {
const modal = ref<Nullable<ModalMethods>>(null);
const loaded = ref<Nullable<boolean>>(false);
const uid = ref<number>(0);
function register(modalMethod: ModalMethods, uuid: number) {
if (!getCurrentInstance()) {
throw new Error('useModal() can only be used inside setup() or functional components!');
}
uid.value = uuid;
isProdMode() &&
onUnmounted(() => {
modal.value = null;
loaded.value = false;
dataTransfer[unref(uid)] = null;
});
if (unref(loaded) && isProdMode() && modalMethod === unref(modal)) return;
modal.value = modalMethod;
loaded.value = true;
modalMethod.emitOpen = (open: boolean, uid: number) => {
openData[uid] = open;
};
}
const getInstance = () => {
const instance = unref(modal);
if (!instance) {
error('useModal instance is undefined!');
}
return instance;
};
const methods: ReturnMethods = {
getModalProps: (): Partial<ModalProps> => {
return getInstance()?.getModalProps() || {};
},
setModalProps: (props: Partial<ModalProps>): void => {
getInstance()?.setModalProps(props);
},
getOpen: computed((): boolean => {
return openData[~~unref(uid)];
}),
openModal: <T = any>(open = true, data?: T, openOnSet = true): void => {
getInstance()?.setModalProps({
open: open,
});
if (!data) return;
const id = unref(uid);
if (openOnSet) {
dataTransfer[id] = null;
dataTransfer[id] = toRaw(data);
return;
}
const equal = isEqual(toRaw(dataTransfer[id]), toRaw(data));
if (!equal) {
dataTransfer[id] = toRaw(data);
}
},
closeModal: () => {
getInstance()?.setModalProps({ open: false });
},
setModalData: (data: any) => {
if (!data) return;
nextTick(() => {
setTimeout(() => {
const id = unref(uid);
dataTransfer[id] = null;
dataTransfer[id] = toRaw(data);
return;
}, 100);
});
},
redoModalHeight: () => {
getInstance()?.redoModalHeight?.();
},
};
return [register, methods];
}
export const useModalInner = (callbackFn?: Fn): UseModalInnerReturnType => {
const modalInstanceRef = ref<Nullable<ModalMethods>>(null);
const currentInstance = getCurrentInstance();
const uidRef = ref<number>(0);
const getInstance = () => {
const instance = unref(modalInstanceRef);
if (!instance) {
error('useModalInner instance is undefined!');
}
return instance;
};
const register = (modalInstance: ModalMethods, uuid: number) => {
isProdMode() &&
tryOnUnmounted(() => {
modalInstanceRef.value = null;
});
uidRef.value = uuid;
modalInstanceRef.value = modalInstance;
currentInstance?.emit('register', modalInstance, uuid);
};
watchEffect(() => {
const data = dataTransfer[unref(uidRef)];
if (!data) return;
if (!callbackFn || !isFunction(callbackFn)) return;
nextTick(() => {
callbackFn(data);
});
});
return [
register,
{
changeLoading: (loading = true) => {
getInstance()?.setModalProps({ loading });
},
changeOkLoading: (loading = true) => {
getInstance()?.setModalProps({ confirmLoading: loading });
},
getOpen: computed((): boolean => {
return openData[~~unref(uidRef)];
}),
closeModal: () => {
getInstance()?.setModalProps({ open: false });
},
getModalProps: (): Partial<ModalProps> => {
return getInstance()?.getModalProps() || {};
},
setModalProps: (props: Partial<ModalProps>) => {
getInstance()?.setModalProps(props);
},
redoModalHeight: () => {
const callRedo = getInstance()?.redoModalHeight;
callRedo && callRedo();
},
},
];
};

View File

@@ -0,0 +1,16 @@
import { InjectionKey } from 'vue';
import { createContext, useContext } from '@jeesite/core/hooks/core/useContext';
export interface ModalContextProps {
redoModalHeight: () => void;
}
const key: InjectionKey<ModalContextProps> = Symbol();
export function createModalContext(context: ModalContextProps) {
return createContext<ModalContextProps>(context, key);
}
export function useModalContext() {
return useContext<ModalContextProps>(key);
}

View File

@@ -0,0 +1,107 @@
import { Ref, unref, watchEffect } from 'vue';
import { useTimeoutFn } from '@jeesite/core/hooks/core/useTimeout';
export interface UseModalDragMoveContext {
draggable: Ref<boolean>;
destroyOnClose: Ref<boolean | undefined> | undefined;
open: 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.open) || !unref(context.draggable)) {
return;
}
useTimeoutFn(() => {
handleDrag();
}, 30);
});
}

View File

@@ -0,0 +1,44 @@
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`;
// }
window.dispatchEvent(new Event('resize'));
}
return { getWrapClassName, handleFullScreen, fullScreenRef };
}

View File

@@ -0,0 +1,82 @@
import type { PropType, CSSProperties } from 'vue';
import type { ModalWrapperProps } from './typing';
import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
export const modalProps = {
open: { type: Boolean },
scrollTop: { type: Boolean, default: true },
height: { type: Number },
minHeight: { type: Number },
// open drag
draggable: { type: Boolean, default: true },
centered: { type: Boolean, default: true },
cancelText: { type: String },
okText: { type: String },
closeFunc: Function as PropType<() => Promise<boolean>>,
};
export const basicProps = {
...modalProps,
defaultFullscreen: { type: Boolean },
// Can it be full screen
canFullscreen: { type: Boolean, default: true },
// After enabling the wrapper, the bottom can be increased in height
wrapperFooterOffset: { type: Number, default: 0 },
// Warm reminder message
helpMessage: [String, Array] as PropType<string | string[]>,
// Whether to setting wrapper
useWrapper: { type: Boolean, default: true },
loading: { type: Boolean },
loadingTip: { type: String },
/**
* @description: Show close button
*/
showCancelBtn: { type: Boolean, default: true },
/**
* @description: Show confirmation button
*/
showOkBtn: { type: Boolean, default: true },
wrapperProps: Object as PropType<Partial<ModalWrapperProps>>,
afterClose: Function as PropType<() => Promise<VueNode>>,
bodyStyle: Object as PropType<CSSProperties>,
closable: { type: Boolean, default: true },
closeIcon: Object as PropType<VueNode>,
confirmLoading: { type: Boolean },
destroyOnClose: { type: Boolean },
footer: Object as PropType<VueNode>,
getContainer: Function as PropType<() => any>,
mask: { type: Boolean, default: true },
maskClosable: { type: Boolean, default: true },
keyboard: { type: Boolean, default: true },
maskStyle: Object as PropType<CSSProperties>,
okType: { type: String, default: 'primary' },
okAuth: { type: String },
okButtonProps: Object as PropType<ButtonProps>,
cancelButtonProps: Object as PropType<ButtonProps>,
title: { type: String },
open: { type: Boolean },
width: [String, Number] as PropType<string | number>,
wrapClassName: { type: String },
zIndex: { type: Number },
};

View File

@@ -0,0 +1,212 @@
import type { ButtonProps } from 'ant-design-vue/lib/button/buttonTypes';
import type { CSSProperties, VNodeChild, ComputedRef } from 'vue';
/**
* @description: 弹窗对外暴露的方法
*/
export interface ModalMethods {
getModalProps: () => Partial<ModalProps>;
setModalProps: (props: Partial<ModalProps>) => void;
emitOpen?: (open: boolean, uid: number) => void;
redoModalHeight?: () => void;
}
export type RegisterFn = (modalMethods: ModalMethods, uuid: number) => void;
export interface ReturnMethods extends ModalMethods {
openModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void;
closeModal: () => void;
getOpen?: ComputedRef<boolean>;
setModalData: (data: any) => void;
}
export type UseModalReturnType = [RegisterFn, ReturnMethods];
export interface ReturnInnerMethods extends ModalMethods {
closeModal: () => void;
changeLoading: (loading: boolean) => void;
changeOkLoading: (loading: boolean) => void;
getOpen?: ComputedRef<boolean>;
redoModalHeight: () => void;
}
export type UseModalInnerReturnType = [RegisterFn, ReturnInnerMethods];
export interface ModalProps {
minHeight?: number;
height?: number;
// 启用wrapper后 底部可以适当增加高度
wrapperFooterOffset?: number;
draggable?: boolean;
scrollTop?: boolean;
// 是否可以进行全屏
canFullscreen?: boolean;
defaultFullscreen?: boolean;
open?: boolean;
// 温馨提醒信息
helpMessage: string | string[];
// 是否使用modalWrapper
useWrapper: boolean;
loading: boolean;
loadingTip?: string;
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 open on top right of the modal dialog or not
* @default true
* @type boolean
*/
closable?: boolean;
/**
* Whether a close (x) button is open 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';
okAuth: string;
/**
* 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 | any;
/**
* 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;
height: number;
open: boolean;
fullScreen: boolean;
useWrapper: boolean;
}