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--primary2. 使用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>待解决的技术难点: