前言
在 vue3 + ts 使用 API 实现 全局控制 Modal 对话框
eslint
'no-unused-vars': ['warn', { vars: 'all', args: 'none' }],
'no-undef': 'warn'
在TS中,以.d.ts结尾的文件默认是全局模块,里面声明的类型,或者变量会被默认当成全局性质的,其他后缀结尾的文件默认是局部模块。对于局部模块要在文件里面显式写import或者export,否则会报错
也可以关闭配置文件中的 isolatedModules:false,取消这样的限制。此限制是为了将每个文件都当成独立的模块,方便其他编译器编译ts代码
ts /* global bbb */ 全局声明
1、建立 modal 模板,在 plugins/modal
-- Modal.vue
<template>
<Teleport to="body" :disabled="!isTeleport">
<div v-if="modelValue" class="modal">
<div class="mask" @click="maskClose && !loading && handleCancel()"></div>
<div class="modal_main">
<div class="modal_title">
<span>{{ title || '提示' }}</span>
<span v-if="close" title="关闭" class="close" @click="!loading && handleCancel()">✕</span>
</div>
<div class="modal_content">
<Content v-if="typeof content === 'function'" :render="content" />
<slot v-else>
<img src="./img/warn.png" class="content_img" alt="warn" v-if="icon == 'warn'">{{ content }}
</slot>
</div>
<div class="modal_btns">
<button class="btn confirm" :disabled="loading" @click="handleConfirm">
<span class="loading" v-if="loading"> ❍ </span>
{{ confirmText }}
</button>
<button class="btn cancel" @click="!loading && handleCancel()">{{ cancelText }}</button>
</div>
</div>
</div>
</Teleport>
</template>
<script lang="ts" setup name="RootModal">
import {
getCurrentInstance,
onBeforeMount,
PropType
} from 'vue'
import Content from './Content'
import config from './config'
import { IContent, IInstance } from './modal.type'
defineProps({
isTeleport: { type: Boolean, default: true },
modelValue: { type: Boolean, default: false, require: true },
title: {
type: String,
default: ''
},
icon: {
type: String,
default: 'none'
},
content: {
type: [String, Function] as PropType<string | IContent>,
default: '',
require: true
},
loading: {
type: Boolean,
default: false
},
close: {
type: Boolean,
default: () => config!.close
},
maskClose: {
type: Boolean,
default: () => config!.maskClose
},
confirmText: {
type: String,
default: '确定'
},
cancelText: {
type: String,
default: '取消'
}
})
const emit = defineEmits(['on-confirm', 'on-cancel', 'update:modelValue'])
let instance = getCurrentInstance() as IInstance
onBeforeMount(() => {
instance._hub = {
'on-cancel': () => { },
'on-confirm': () => { }
}
})
const handleConfirm = () => {
emit('on-confirm')
instance._hub['on-confirm']()
}
const handleCancel = () => {
emit('on-cancel')
emit('update:modelValue', false)
instance._hub['on-cancel']()
}
</script>
<style lang="less" scoped>
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.modal {
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.45);
}
.modal_main {
width: 400px;
min-height: 180px;
background: #FFFFFF;
border-radius: 10px;
z-index: 10000;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.modal_title {
width: 100%;
height: 50px;
padding: 0 20px;
box-sizing: border-box;
font-size: 18px;
color: #333333;
line-height: 50px;
font-weight: 400;
position: relative;
.close {
font-size: 10px;
color: #93969C;
position: absolute;
top: 50%;
right: 19px;
transform: translateY(-50%);
cursor: pointer;
}
}
.modal_content {
width: 100%;
min-height: 30px;
padding: 20px;
box-sizing: border-box;
font-size: 16px;
color: #333333;
line-height: 30px;
.content_img {
width: 30px;
height: 30px;
display: inline-block;
margin-right: 9px;
margin-top: -3px;
}
}
.modal_btns {
min-height: 60px;
padding: 10px 20px 20px;
box-sizing: border-box;
.loading {
display: inline-block;
margin-right: 5px;
animation: rotate 1s infinite linear;
}
.btn {
min-width: 80px;
height: 30px;
padding: 0 9px;
text-align: center;
font-size: 14px;
color: #333333;
line-height: 28px;
float: right;
border: 1px solid #EEEFF2;
background-color: #fff;
border-radius: 4px;
margin-left: 20px;
}
.confirm {
border: 1px solid @primary-color;
background-color: @primary-color;
color: #fff;
}
.cancel:hover {
border: 1px solid @primary-color;
color: @primary-color;
}
}
}
}
</style>
-- Content.tsx
import { h } from 'vue'
const Content = (props: { render: (h: any) => void }) => props.render(h)
Content.props = ['render']
export default Content
-- config.ts
import { ConfigType } from './modal.type'
const config: ConfigType = {
// 是否显示右上角的关闭按钮,默认开启
close: true,
// 点击蒙层是否允许关闭,默认开启
maskClose: true,
confirmText: '确定',
cancelText: '取消'
}
export default config
-- modal.type.ts
import { ComponentInternalInstance, VNode } from 'vue'
export interface ConfigType {
icon?: string,
close?: boolean,
maskClose?: boolean,
confirmText?: string,
cancelText?: string,
rootClassName?: string
}
export type IContent = string | ((h?: any) => VNode)
export interface IModalParams {
title?: string,
icon?: string,
content: IContent,
close?: boolean,
maskClose?: boolean,
confirmText?: string,
cancelText?: string,
rootClassName?: string,
onConfirm?: () => Promise<void> | void,
onCancel?: () => void
}
export interface IModal {
confirm(params: IModalParams): void
}
export interface IInstance extends ComponentInternalInstance {
_hub: {
'on-cancel': () => void,
'on-confirm': () => void
}
}
-- index.ts
import { App, createVNode, render } from 'vue'
import Modal from './Modal.vue'
import { ConfigType, IInstance, IModal } from './modal.type'
import config from './config'
Modal.install = (app: App, options: ConfigType = {}) => {
// 合并配置信息
Object.assign(config, options || {})
// 注册全局组件
app.component(Modal.name, Modal)
// 注册全局事件
app.config.globalProperties.$modal = {
confirm({
title = '',
icon = 'none',
content = '',
close = config!.close,
maskClose = config!.maskClose,
confirmText = config!.confirmText,
cancelText = config!.cancelText,
rootClassName = config!.rootClassName || '',
onConfirm,
onCancel
}) {
const container = document.createElement('div')
container.className = rootClassName
const vnode = createVNode(Modal)
render(vnode, container)
const instance = vnode.component as IInstance
document.body.appendChild(container)
const { props, _hub } = instance
const _closeModal = () => {
props.modelValue = false
container.parentNode!.removeChild(container)
}
Object.assign(_hub, {
t: app.config.globalProperties.$t,
async 'on-confirm'() {
if (onConfirm) {
const fn: any = onConfirm()
if (fn && fn.then) {
try {
props.loading = true
await fn
props.loading = false
_closeModal()
} catch (err) {
// 发生错误时,不关闭弹框
props.loading = false
}
} else {
_closeModal()
}
} else {
_closeModal()
}
},
'on-cancel'() {
onCancel && onCancel()
_closeModal()
}
})
Object.assign(props, {
isTeleport: false,
modelValue: true,
title,
icon,
content,
close,
maskClose,
confirmText,
cancelText
})
}
} as IModal
}
export default Modal
2、使用 hook useGlobal -> hooks/useGlobal/index.ts
import { getCurrentInstance } from 'vue'
/* global ICurrentInstance */
export default function useGlobal() {
const {
appContext: {
config: { globalProperties }
}
} = (getCurrentInstance() as unknown) as ICurrentInstance
return globalProperties
}
在 src 下新建 global.d.ts
import { ComponentInternalInstance } from 'vue'
import { IModal } from '@/plugins/modal/modal.type'
declare global {
interface IGlobalAPI {
$modal: IModal;
}
interface ICurrentInstance extends ComponentInternalInstance {
appContext: {
config: { globalProperties: IGlobalAPI };
};
}
}
export {}
3、在 main.ts 下挂载
-- plugins/useComponents.ts
import type { App } from 'vue'
import Modal from '@/plugins/modal'
const useComponents = (app: App<Element>) => {
app.use(Modal as any)
}
export default useComponents
-- main.ts
import useComponents from './plugins/useComponents'
useComponents(app)
4、在页面中使用
import useGlobal from '@/hooks/useGlobal'
const { $modal } = useGlobal()
const sleep = (delay: number = 3000) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return reject()
}, delay)
// setTimeout(resolve, delay)
})
}
const handleShowModal = () => {
$modal.confirm({
maskClose: true,
close: true,
icon: 'warn',
content: '确认删除?',
async onConfirm() {
console.log('点击确定 before')
await sleep(2000)
console.log('点击确定 after')
},
onCancel() {
console.log('取消----')
}
})
}