闽公网安备 35020302035485号
const isObject = (val: unknown): val is object => {
return val !== null && typeof val === "object";
};
const isArray = (val: unknown): val is string[] => {
return Array.isArray(val);
};
const isString = (val: unknown): val is string => {
return typeof val === "string";
};
type BEMElement = string;
type BEMModifier =
| (string | undefined | false)[]
| Record<string, boolean | string | undefined>;
const createModifier = (prefixClass: string, modifierObject?: BEMModifier) => {
let modifiers: string[] = [];
if (isArray(modifierObject)) {
modifiers = modifierObject
.map((modifier) => {
if (!modifier) return "";
return `${prefixClass}--${modifier}`;
})
.filter(Boolean);
} else if (isObject(modifierObject)) {
modifiers = Object.entries(modifierObject).map(([modifier, value]) => {
if (!value) return "";
return `${prefixClass}--${modifier}`;
});
}
return modifiers;
};
/**
* 堆代码 duidaima.com
* CSS BEM
*/
export const createCssScope = (prefix: string, identity = "front") => {
const prefixClass = `${identity}-${prefix.replace(identity, "")}`;
return (
elementOrModifier?: BEMElement | BEMModifier,
modifier?: BEMModifier,
modifierLater?: BEMModifier
) => {
if (!elementOrModifier) return prefixClass;
if (isString(elementOrModifier)) {
const element = `${prefixClass}__${elementOrModifier}`;
if (!modifier) return element;
return [
element,
...createModifier(element, modifier),
...createModifier(element, modifierLater),
];
}
return [
prefixClass,
...createModifier(prefixClass, elementOrModifier),
...createModifier(prefixClass, modifier),
];
};
};
/**
* CSS BEM
* @example 使用示例
*/
const bem = createCssScope('button')
bem()
// front-button
bem('label')
// front-button__label
bem({ disabled })
// front-button front-button--disabled
bem('label', { disabled })
// front-button__label front-button__label--disabled
bem(['disabled', 'primary'])
// front-button front-button--disabled front-button--primary
2. 使用Teleport渲染到指定位置<template>
<div>
<teleport to="body">
<!-- 弹出框内容 -->
</teleport>
</div>
</template>
在上面的例子中,我们使用了 <teleport> 标签,并通过 to 属性指定了要渲染到的目标位置。这样,弹出框的内容就会被挂载到 <body> 下面,而不是当前组件的位置。<template>
<Teleport :to="target">
<div>
<Transition name="fade" appear>
.........
</Transition>
<Transition name="zoom-in-top" appear>
.........
</Transition>
</div>
</Teleport>
</template>
4. 遮罩层、弹窗层的结构与样式<template>
<Teleport :to="target">
<div>
<!-- 遮罩层 -->
<Transition name="fade" appear>
<div
v-show="modelValue"
:class="[
bem('mask'),
bem({
maskBg: showMask,
}),
]"
></div>
</Transition>
<!-- 弹窗层 -->
<Transition name="zoom-in-top" appear>
<div
v-show="modelValue"
:class="bem('modal')"
@click.self="closeMaskToCloseModal"
>
<!-- 主体区域 -->
................
</div>
</Transition>
</div>
</Teleport>
</template>
.front-modal__mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
margin: auto;
width: 100%;
height: 100%;
}
.front-modal--maskBg {
background-color: #00000066;
}
.front-modal__modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9999;
display: flex;
align-items: center;
margin: auto;
width: 100%;
height: 100%;
}
5. 实现头部、底部、默认插槽
对于封装通用组件,为了实现自定义内容,需要准备好头部、底部、默认插槽,允许往里面插入各种需要的内容
<template>
<Teleport :to="target">
<div>
<!-- 遮罩层 -->
<Transition name="fade" appear>
.......
</Transition>
<!-- 弹窗层 -->
<Transition name="zoom-in-top" appear>
<div
v-show="modelValue"
:class="bem('modal')"
@click.self="closeMaskToCloseModal"
>
<!-- 主体区域 -->
<div
:class="[
bem('main')
]"
>
<!-- 头部区域 —— header插槽 -->
<div :class="bem('header')">
<slot name="header">
<div :class="bem('title')">
{{ title }}
</div>
</slot>
<!-- 右上角关闭按钮 -->
<div :class="bem('close')" @click="closeModal">
<IconCloseOutline />
</div>
</div>
<!-- 内容区域 —— 默认插槽 -->
<div :class="bem('content')">
<slot />
</div>
<!-- 底部区域 —— footer插槽 -->
<div v-if="showFooter" :class="bem('footer')">
<div class="front-modal-footer-option">
<slot name="footer">
.......
</slot>
</div>
</div>
</div>
</div>
</Transition>
</div>
</Teleport>
</template>
6. 处理props属性以及目标元素
export type modalBaseProps = {
title: string;
size?: "small" | "large";
modelValue: boolean;
to?: string;
scrollable?: boolean;
closeable?: boolean;
showMask?: boolean;
escapable?: boolean;
showFooter?: boolean;
};
const props = withDefaults(defineProps<modalBaseProps>(), {
modelValue: false,
title: "",
size: "large",
showFooter: true,
to: "body",
scrollable: false,
closeable: false,
showMask: true,
escapable: true,
});
接下来需要把对话框组件挂载到指定的结点,也就是 Teleport 的 to 属性指向的值,默认是将其挂载到 body 元素上<template>
<div>
<teleport :to="target">
<!-- 弹出框内容 -->
</teleport>
</div>
</template>
const target = ref<HTMLElement>(document.body);
const getElement = (selector: string): HTMLElement => {
return document.querySelector(selector) ?? document.body;
};
const initScroll = () => {
...........
if (props.to) {
target.value = getElement(props.to || "body");
}
};
watch(props, () => {
initScroll();
});
onMounted(() => {
initScroll();
});
7. 处理滚动条以及页面可否滚动

onUnmounted(() => {
document.body.style.overflow = "";
});
// 堆代码 duidaima.com
const initScroll = () => {
if (props.modelValue) {
.......
!props.scrollable && (document.body.style.overflow = "hidden");
} else {
document.body.style.overflow = "";
}
};
8. v-model 实现双向绑定<template>
<front-modal v-model="visible" title="这里是标题" @onCloseModal="closeModal" >
<span>这一块是modal对话框的内容部分,这里支持各种标签,组件</span>
</front-modal>
<button @click="showModal">modal 对话框</yk-button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref<boolean>(false)
const showModal = () => {
visible.value = true
}
const closeModal = () => {
........
}
</script>
front-modal 组件:<template>
<Teleport :to="target">
<div>
<!-- 遮罩层 -->
<Transition name="fade" appear>
<div
v-show="modelValue"
:class="[
bem('mask'),
bem({
maskBg: showMask,
}),
]"
></div>
</Transition>
<!-- 弹窗层 -->
<Transition name="zoom-in-top" appear>
<div
v-show="modelValue"
:class="bem('modal')"
@click.self="closeMaskToCloseModal"
>
<!-- 主体区域 -->
<div
:class="[
bem('main'),
bem({
shadow: !props.showMask,
size: size === 'small',
}),
]"
>
<div :class="bem('header')">
<slot name="header">
<div :class="bem('title')">
{{ title }}
</div>
</slot>
<!-- 右上角关闭按钮 -->
<div :class="bem('close')" @click="closeModal">
<IconCloseOutline />
</div>
</div>
............
</div>
</div>
</Transition>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { watch, onUnmounted, ref, onMounted } from "vue";
import { createCssScope } from "../../../utils";
import { modalBaseProps } from "./modal";
defineOptions({
name: "FrontModal",
});
const props = withDefaults(defineProps<modalBaseProps>(), {
modelValue: false,
.........
});
const bem = createCssScope("modal");
const emit = defineEmits(["onCloseModal", "update:modelValue", "onSubmit"]);
const closeModal = () => {
emit("update:modelValue", false);
emit("onCloseModal");
};
const initScroll = () => {
if (props.modelValue) {
if (props.escapable) {
document.body.addEventListener("keydown", escapeClose);
}
!props.scrollable && (document.body.style.overflow = "hidden");
} else {
document.body.style.overflow = "";
}
};
watch(props, () => {
initScroll();
});
onMounted(() => {
initScroll();
});
// 按esc键关闭 modal
const escapeClose = (ev: KeyboardEvent) => {
if (ev.key === "Escape") closeModal();
};
// 关闭弹窗的回调
const closeMaskToCloseModal = () => {
props.closeable && emit("update:modelValue", false);
};
// 点击底部确定按钮的回调
const handleSubmit = () => {
emit("onSubmit");
};
</script>
待解决的技术难点:
