Vue3+ts实现简单版本modal
项目中要求做一个询问弹框,有点类似ant-design-vue中的modal通过API调用的那种效果。虽然使用的ui库arco-design-vue中的modal也能满足我的需求,但是我懒得改样式,索性自己写一个阉割版的好了。
实现的这个modal只支持通过API来进行调用
结构组件
<template>
<div v-if="mask && visible" class="mask"></div>
<div class="container">
<div v-if="visible" class="confirm">
<span class="close" @click="cancel">
<IconClose />
</span>
<div ref="moveEl" :class="{ head: true, move: draggable }">
<!-- 这里可以换成自己需要的图标或者图片 -->
<img :src="url" alt="" />
<span class="title">{{ title }}</span>
</div>
<div class="msg">{{ content }}</div>
<div class="footer">
<button class="cancel" @click="cancel">取消</button>
<button class="sure" @click="ok">确认</button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from 'vue';
import { IconClose } from '@arco-design/web-vue/es/icon';
import url from '@/assets/icons/confirm-icon.png';
const emits = defineEmits(['ok', 'cancel']);
const moveEl = ref<HTMLElement | null>(null);
const props = defineProps({
visible: Boolean, // 是否展示弹框
title: String, // 标题
content: String, // 内容
mask: Boolean, // 是否展示蒙层
draggable: Boolean, // 是否可拖拽
});
function ok() {
emits('ok');
}
function cancel() {
emits('cancel');
}
onMounted(() => {
let dx = 0;
let dy = 0;
let sx = 0;
let sy = 0;
let box: HTMLElement | null = null;
function move(ev: MouseEvent) {
if (box) {
let left = ev.clientX - (dx - sx);
let top = ev.clientY - (dy - sy);
if (left <= 0) left = 0;
if (top <= 0) top = 0;
if (left >= window.innerWidth - box.clientWidth) left = window.innerWidth - box.clientWidth;
if (top >= window.innerHeight - box.clientHeight)
top = window.innerHeight - box.clientHeight;
box.setAttribute('style', `left:${left}px; top:${top}px;transform:none`);
}
}
function moveStart(ev: MouseEvent) {
dx = ev.clientX;
dy = ev.clientY;
const cssObj = box?.getBoundingClientRect();
sx = cssObj?.left as number;
sy = cssObj?.top as number;
document.addEventListener('mousemove', move, false);
}
if (props.draggable) {
if (moveEl.value) {
box = moveEl.value.parentElement as HTMLElement;
box.addEventListener('mousedown', moveStart, false);
}
document.addEventListener('mouseup', function () {
if (moveEl.value) {
document.removeEventListener('mousemove', move, false);
}
});
}
});
</script>
<style lang="less" scoped>
.mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.confirm {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, 0);
width: 427px;
height: 151px;
background-color: var(--color-bg-3);
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.1);
border: 1px solid var(--color-neutral-3);
.close {
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
top: 12px;
right: 16px;
cursor: pointer;
font-size: 12px;
color: var(--color-text-1);
transition: background-color 0.3s;
&:hover {
background-color: var(--color-fill-2);
transition: background-color 0.3s;
}
}
}
.confirm[theme='dark'] {
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.4);
}
.head {
display: flex;
align-items: center;
padding: 19px 23px 12px;
width: 100%;
font-size: 26px;
color: rgb(255, 125, 0);
.title {
margin-left: 13px;
font-size: 16px;
font-weight: 500;
color: var(--color-text-1);
line-height: 32px;
}
}
.move {
cursor: move;
}
.msg {
margin-left: 60px;
margin-bottom: 23px;
height: 19px;
font-size: 14px;
font-weight: 400;
color: var(--color-text-1);
line-height: 19px;
}
.footer {
display: flex;
justify-content: flex-end;
padding-right: 16px;
button {
border: none;
width: 85px;
height: 31px;
background-color: var(--color-secondary);
border-radius: 4px;
margin-left: 12px;
color: var(--color-text-1);
cursor: pointer;
}
.sure {
color: #ffffff;
background-color: rgb(var(--primary-6));
transition: background-color 0.3s;
&:hover {
background-color: rgb(var(--primary-5));
transition: background-color 0.3s;
}
}
}
</style>
<style scoped></style>
API封装代码
import { createVNode, render } from 'vue';
import _Confirm from './confirm.vue';
interface ConfigType {
title: string; // 标题
content: string; // 内容
mask?: boolean; // 蒙层
draggable?: boolean; // 是否可拖拽
onOk?: () => void; // 确认回调
onCancel?: () => void; // 取消回调
}
// 判断是不是函数类型
function isFunction(arg: any): boolean {
return typeof arg === 'function';
}
function open(config: ConfigType) {
let container: HTMLElement | null = document.querySelector('.confirm-modal');
if (!container) {
container = document.createElement('div');
container.className = 'confirm-modal';
container.setAttribute(
'style',
`position: fixed;top: 0;left: 0;right: 0;bottom: 0;z-index: 1001;display:block`,
);
} else {
container.style.zIndex = '1001';
container.style.display = 'block';
}
// 销毁组件、移除container
function hide() {
// eslint-disable-next-line no-use-before-define
if (vm.component) {
// eslint-disable-next-line no-use-before-define
vm.component.props.visible = false;
if (container) {
container.style.zIndex = '0';
container.style.display = 'none';
}
}
if (container) {
render(null, container);
document.body.removeChild(container);
}
container = null;
}
const handleOk = () => {
if (config.onOk && isFunction(config.onOk)) {
config?.onOk();
}
hide();
};
const handleCancel = () => {
if (config.onCancel && isFunction(config.onCancel)) {
config.onCancel();
}
hide();
};
const defaultConfig = {
visible: true,
mask: false,
draggable: true,
};
const vm = createVNode(_Confirm, {
...defaultConfig,
...config,
onOk: handleOk,
onCancel: handleCancel,
});
render(vm, container);
if (vm.component) {
vm.component.props.visible = true;
}
if (!document.querySelector('.my-confirm')) {
document.body.appendChild(container);
}
}
export default {
open,
};
封装得不是很好。大家轻点喷哈