vue3 dialog 和message 写成一个hook

张开发
2026/4/11 12:16:24 15 分钟阅读

分享文章

vue3 dialog 和message 写成一个hook
!--MyDialog.vue--templatetransition namefadediv v-ifvisibleclassdialog-overlayclick.selfhandleMaskClickdivclassdialog-wrapper!--头部--divclassdialog-headerspan{{title}}/spanbuttonclassclose-btnclickhandleCancel×/button/div!--内容--divclassdialog-bodyslot{{message}}/slot/div!--底部按钮--divclassdialog-footerbuttonclassbtnclickhandleCancel取消/buttonbuttonclassbtn btn-primaryclickhandleConfirm确定/button/div/div/div/transition/templatescript setupimport{ref,onMounted}fromvueconstpropsdefineProps({title:{type:String,default:提示},message:String,})constemitdefineEmits([confirm,cancel])constvisibleref(false)onMounted((){visible.valuetrue})consthandleConfirm(){visible.valuefalseemit(confirm)}consthandleCancel(){visible.valuefalseemit(cancel)}/scriptstyle scoped/* 简单的淡入淡出动画 */.fade-enter-active,.fade-leave-active{transition:opacity0.2s ease;}.fade-enter-from,.fade-leave-to{opacity:0;}/* MyDialog.css */.dialog-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);/* 半透明黑色遮罩 */display:flex;justify-content:center;align-items:center;z-index:9999;/* 保证在最上层 */}.dialog-wrapper{background:#fff;border-radius:8px;width:400px;box-shadow:04px 12pxrgba(0,0,0,0.15);overflow:hidden;animation:fade-in0.2s ease-out;/* 简单的入场动画 */}.dialog-header{padding:15px 20px;border-bottom:1px solid #eee;font-size:16px;font-weight:bold;display:flex;justify-content:space-between;align-items:center;}.close-btn{cursor:pointer;font-size:20px;color:#999;border:none;background:none;}.close-btn:hover{color:#333;}.dialog-body{padding:20px;font-size:14px;color:#333;line-height:1.5;}.dialog-footer{padding:10px 20px;border-top:1px solid #eee;display:flex;justify-content:flex-end;gap:10px;}.btn{padding:8px 16px;border-radius:4px;cursor:pointer;border:1px solid #ddd;background:#fff;font-size:14px;}.btn-primary{background:#409eff;color:#fff;border-color:#409eff;}.btn-primary:hover{background:#66b1ff;}.btn:hover{background:#f5f7fa;}keyframes fade-in{from{opacity:0;transform:scale(0.95);}to{opacity:1;transform:scale(1);}}/style// useConfirm.jsimport{h,render,createVNode}fromvueimportMyConfirmDialogfrom./dialog.vueconstMyConfirmfunction(options){// 1. 创建挂载容器constcontainerdocument.createElement(div)document.body.appendChild(container)// 2. 返回一个 PromisereturnnewPromise((resolve,reject){// 3. 定义组件卸载后的清理函数constdestroy(){render(null,container)// 移除 DOMdocument.body.removeChild(container)// 移除容器}// 4. 创建虚拟节点constvnodecreateVNode(MyConfirmDialog,{title:options.title,message:options.message,// 注入回调事件onConfirm:(){resolve()// 成功时 resolvedestroy()},onCancel:(){reject()// 取消时 rejectdestroy()}})// 5. 渲染render(vnode,container)})}exportdefaultMyConfirmimportMyConfirmDialogfrom./comment/dialog.jsconsttestDialog(){MyConfirmDialog({title:确认操作,message:你确定要执行这个操作吗}).then((){console.log(用户确认了操作)}).catch((){console.log(用户取消了操作)})}{ onConfirm:…, onCancel:… }) 注册的监听器其实对应事件名 confirm / cancel 为什么两个名字不一样 要在前面加上on因为 Vue 在“传入监听器”和“触发事件”两端用不同的命名约定 — 外部传入用 on组件内部 emit 用不带 on 的事件名。要点模板/创建 vnodeMyComp confirm“fn” / 会被编译/等价为 createVNode(MyComp, { onConfirm: fn })也就是外部传入的监听器是 onConfirm。组件内部发射组件应调用 emit(‘confirm’) 来触发外部的 onConfirm。错误写法如果在组件里写 emit(‘onConfirm’)框架会期望外部传入 onOnConfirm 才会匹配所以不会触发你传的 onConfirm。命名映射带短横的事件名会转成驼峰my-event - onMyEvent。第二个字段props中有那些参数简短回答createVNode(type, props?, children?) 的第二个参数是 props/attrs 对象可包含下面几类字段组件 Props: 组件定义的属性例如 title、message会以 props 形式传入组件实例。事件监听器: 以 on 开头的回调例如 onConfirm: () {}、onCancel: () {}对应组件内部 emit(‘confirm’) / emit(‘cancel’)注意emit 不带 on。kebab-case 事件会被转为驼峰emit(‘my-event’) - onMyEvent。原生 DOM 属性/特性: 当 vnode 是原生元素时可以传 id、class、style、disabled 等。VNode 专用字段: key、ref、ref_for、ref_key以及 vnode 钩子 onVnodeBeforeMount、onVnodeMounted 等。未声明字段 - $attrs: 对组件来说未在 props 中声明的字段会放到组件的 $attrs 中。插槽/children: 插槽或子节点通常放在第三个参数 children也可以传入 slots 对象例如 { default: () h(…) }。举例说明createVNode(MyConfirmDialog, { title: ‘x’, onConfirm: fn }) 会在组件执行 emit(‘confirm’) 时调用 fn如果你在组件里错误地写成 emit(‘onConfirm’)则需要外部传 onOnConfirm 才匹配因此不要在 emit 上加 on。close: () vnode.component.exposed.close() // 这个component.exposed 是什么意思!--Message.vue--templatetransition namefadeafter-leavehandleAfterLeavediv v-showvisibleclassmy-message:classmy-message--${type}:stylestylespanclassmy-message__content{{message}}/spanbuttonclassmy-message__closeclickclose×/button/div/transition/templatescript setupimport{ref,computed,onMounted}fromvueconstpropsdefineProps({message:String,type:{type:String,default:info},offset:Number,// 距离顶部的距离onClose:Function// 关闭回调})constvisibleref(false)lettimernull// 计算样式主要是 top 位置conststylecomputed(()({top:${props.offset}px}))onMounted((){visible.valuetrue// 自动关闭逻辑timersetTimeout((){close()},3000)})constclose(){visible.valuefalseclearTimeout(timer)}consthandleAfterLeave(){if(props.onClose)props.onClose()}/scriptstyle scoped.my-message{position:fixed;left:50%;transform:translateX(-50%);padding:10px 20px;border-radius:4px;background:#fff;box-shadow:02px 12px0rgba(0,0,0,.1);display:flex;align-items:center;z-index:9999;}.my-message--success{background:#f0f9eb;color:#67c23a;}.my-message--error{background:#fef0f0;color:#f56c6c;}/* 简单的淡入淡出动画 */.fade-enter-active,.fade-leave-active{transition:opacity0.3s;}.fade-enter-from,.fade-leave-to{opacity:0;}/style// useMessage.jsimport{h,render,createVNode}fromvueimportMessageComponentfrom./Message.vue// 用于存储当前的消息实例方便计算堆叠位置letinstances[]letseed1// 唯一 ID 生成器// 核心函数constMyMessagefunction(options){// 1. 生成唯一 IDconstidmessage_${seed}// 2. 计算垂直偏移量 (堆叠逻辑)// 简单的逻辑每个新消息比上一个多 offset 60px实际项目中需要根据高度动态计算constoffsetinstances.length*6020// 3. 创建虚拟节点// 这里使用了 h 函数第一个参数是组件第二个参数是 PropsconstvnodecreateVNode(MessageComponent,{message:options.message,type:options.type||info,offset:offset,onClose:(){// 4. 定义销毁逻辑// 当组件动画结束后调用 render(null, container) 将其从 DOM 移除render(null,container)// 从实例列表中移除constindexinstances.indexOf(id)if(index-1)instances.splice(index,1)}})// 5. 创建挂载容器并插入页面constcontainerdocument.createElement(div)document.body.appendChild(container)// 6. 渲染render(vnode,container)// 7. 记录实例 IDinstances.push(id)// 8. 返回控制对象 (可选)// 这样调用者可以手动关闭消息例如 const msg MyMessage(...); msg.close()return{close:()vnode.component.exposed.close()// 如果组件暴露了 close 方法}}// // 语法糖快捷方法// MyMessage.success (msg) MyMessage({ message: msg, type: success })// MyMessage.error (msg) MyMessage({ message: msg, type: error })exportdefaultMyMessageconsttest(){MyMessage({message:这是一个消息提示,type:success})}简短回答vnode.component.exposed 是组件挂载后由 expose / defineExpose 明确暴露出来的对象外部可以通过它安全地调用组件暴露的方法例如 close()。要点vnode.component组件的内部实例只有挂载后存在。exposed由组件通过 expose(…) 或 defineExpose(…) 显式导出的 API对外可见且更安全。proxyvnode.component.proxy 是组件的公有代理包含 data、props、methods、computed 等没有显式 expose 时可以从这里访问方法但语义上不如 exposed 明确。时机exposed 只有在组件挂载后存在在你的 message.js 中 render(vnode, container) 会同步挂载因此随后调用 vnode.component.exposed.close() 是可行的。如果 vnode.component.exposed 为 undefined说明组件没有用 expose/defineExpose 暴露对应方法此时可改用 vnode.component.proxy 或在组件内增加 defineExpose。示例在组件里暴露 close

更多文章