从"包裹器"到"确认按钮"——一个组件的三次重构

从"包裹器"到"确认按钮"------一个组件的三次重构

背景

后台管理系统中,"危险操作需要二次确认"是最高频的交互模式。表格操作列的删除、禁用,批量操作的批量删除,详情页的注销账号------这些场景都需要 tooltip 提示 + popconfirm 确认 + 按钮三者配合。

用 Ant Design Vue 原生写法,每个地方都要写三层嵌套 + 手动互斥控制:

html 复制代码
<a-tooltip :visible="popVisible ? false : undefined" title="删除该记录">
  <a-popconfirm v-model:visible="popVisible" title="确定删除?" @confirm="onDelete">
    <a-button icon="delete" danger />
  </a-popconfirm>
</a-tooltip>

ButtonConfirm 就是为了消灭这段重复代码而生的。


V1:slot 包裹器(89bb3e2)

设计思路: 做一个通用包裹器,用 slot 接收任意子元素,外面套上 tooltip 和 popconfirm。

html 复制代码
<dbButtonConfirm needConfirm confirmContent="确定删除?" tooltip="删除">
  <a-button type="primary" danger>删除</a-button>
</dbButtonConfirm>

Props:

  • needConfirm:默认 false,需要手动开启
  • disabled:独立的禁用状态
  • 无按钮相关属性,按钮由 slot 传入

模板结构: 4 个 v-if 分支处理 tooltip/popconfirm 的组合:

less 复制代码
1. tooltip && needConfirm && !disabled → tooltip > popconfirm > span > slot
2. needConfirm && !disabled           → popconfirm > span > slot
3. tooltip                            → tooltip > span(@click) > slot
4. else                               → span(@click) > slot

问题:

  • needConfirm 默认 false------组件叫"确认按钮",却默认不确认
  • 按钮通过 slot 传入,组件无法控制按钮的事件链
  • <span> 包裹导致布局问题
  • @click 事件会冒泡穿透,绕过 popconfirm 确认流程

V2:内置 Button + @click 防穿透(1030048)

核心改进: 不再用 slot 包裹外部按钮,改为内置 dbButton 渲染。

html 复制代码
<!-- V1: slot 包裹 -->
<dbButtonConfirm needConfirm confirmContent="确定删除?">
  <a-button danger>删除</a-button>
</dbButtonConfirm>

<!-- V2: 内置 Button,继承全部按钮属性 -->
<dbButtonConfirm danger confirmContent="确定删除?" @confirm="onDelete">
  删除
</dbButtonConfirm>

为什么必须内置 Button?

因为只有控制了按钮本身,才能从机制上解决 @click 穿透问题:

  1. inheritAttrs: false ------ 阻止外部属性直接落到内部元素
  2. safeAttrs computed ------ 过滤掉所有 on 开头的事件监听器
  3. 开发环境 console.error ------ 检测到 @click 时提醒开发者用 @confirm

移除 needConfirm prop: 通过 confirmContent 是否存在自动推导------有内容就确认,没有就不确认。理由是"组件名叫确认按钮就必须确认"。

模板结构简化为 2 个分支:

css 复制代码
1. tooltip → tooltip > popconfirm > Button
2. else    → popconfirm > Button

解决的问题:

  • 消灭了 <span> 包裹,按钮渲染正确
  • @click 被彻底屏蔽,只能通过 @confirm 接收回调
  • 继承 dbButton 全部能力(type/danger/icon/size/appearance 等)
  • API 表面更简洁,一个组件替代三层嵌套

遗留问题:

  • 移除 needConfirm 后,无法动态控制"这次点击要不要弹确认框"
  • 需要确认和不需要确认的场景,开发者被迫用 v-if/v-elsedbButtondbButtonConfirm 之间切换

V3:handleVisibleChange 拦截模式(f9d404c)

核心改进: 重新引入 needConfirm prop,但默认值改为 true,且实现方式完全不同。

V1 vs V3 的 needConfirm

V1 V3
默认值 false(需要手动开启) true(默认就确认)
实现方式 v-if 控制是否渲染 popconfirm handleVisibleChange 拦截是否弹出
false 时行为 点击 span 直接 emit 拦截 popconfirm 弹出,直接 emit

关键设计:参考 antd 官方的 visibleChange 模式

ts 复制代码
const handleVisibleChange = (visible: boolean) => {
  if (!visible) {
    confirmVisible.value = false
    return
  }
  if (props.needConfirm) {
    confirmVisible.value = true  // 正常弹出确认框
  } else {
    emits('confirm')             // 跳过确认,直接触发
  }
}

popconfirm 始终存在于 DOM 中,但通过 handleVisibleChange 在弹出瞬间拦截。needConfirm: false 时,popconfirm 根本不会展示,直接走 @confirm 回调。

解决了什么实际问题?

同一个按钮,根据业务状态动态决定是否需要确认:

html 复制代码
<!-- 一个组件覆盖两种情况,无需 v-if/v-else -->
<dbButtonConfirm
  icon="delete"
  danger
  :needConfirm="record.status !== 'draft'"
  confirmContent="确定删除该记录?"
  @confirm="onDelete(record)"
/>

草稿状态点击直接删除,已发布状态弹确认框。同一个 @confirm 回调,业务只需控制一个布尔值。


三个版本的对比

less 复制代码
V1(包裹器)
┌──────────────────────────────┐
│ dbButtonConfirm              │
│   ├─ tooltip (可选)          │
│   ├─ popconfirm (可选)       │
│   └─ <span>                  │
│       └─ <slot> ← 外部按钮  │  ← 无法控制事件链
└──────────────────────────────┘

V2(内置 Button)
┌──────────────────────────────┐
│ dbButtonConfirm              │
│   ├─ tooltip (可选)          │
│   ├─ popconfirm (始终渲染)    │
│   ├─ safeAttrs (过滤 @click) │
│   └─ <Button> ← 内置渲染    │  ← 完全控制事件链
└──────────────────────────────┘

V3(handleVisibleChange)
┌──────────────────────────────┐
│ dbButtonConfirm              │
│   ├─ tooltip (可选)          │
│   ├─ popconfirm (始终渲染)    │
│   ├─ handleVisibleChange     │  ← 拦截弹出,动态决定流程
│   ├─ safeAttrs (过滤 @click) │
│   └─ <Button> ← 内置渲染    │
└──────────────────────────────┘

最终运行时流程

arduino 复制代码
点击按钮
    │
    ▼
needConfirm?
    │
    ├── true ──► 弹出 popconfirm
    │                │
    │           ┌────┴────┐
    │           ▼         ▼
    │        确认       取消
    │           │         │
    │           ▼         ▼
    │     emit confirm  emit cancel
    │
    └── false ──► 直接 emit confirm

设计总结

迭代 关键决策 解决的问题
V1 slot 包裹任意元素 基础功能可用
V2 内置 Button + inheritAttrs: false @click 防穿透、消灭 span 包裹
V3 handleVisibleChange 拦截 一个组件覆盖"需确认"和"不需确认"两种场景

最终的 dbButtonConfirm 是一个真正的按钮组件 ,不是包裹器。它继承了 dbButton 的全部能力,内置了 tooltip/popconfirm 互斥处理和 @click 防穿透机制,通过 needConfirm 动态控制确认流程,让开发者用一个组件、一个 @confirm 回调覆盖所有操作按钮场景。

相关推荐
swipe13 分钟前
12|(前端转全栈)点击提交订单后,后端如何用事务守住价格、库存和订单?
前端·后端·面试
小林ixn16 分钟前
React + TypeScript 实战:从“类型体操”到“数据持久化”,一次讲透组件通信与副作用管理
前端·react.js·typescript
何时梦醒16 分钟前
React + TypeScript 企业级开发实战:从零搭建到组件架构演进
前端·react.js·全栈
Coffeeee17 分钟前
AGP9.0的主要变更项,给Gradle来一次大变样
android·前端·gradle
无人生还28 分钟前
从 Vue3 到 React · 快速上手系列第 7 篇:副作用与生命周期 —— watch/watchEffect 如何翻译为 useEffect
前端·vue.js·react.js
数聚天成DeepSData32 分钟前
外贸海关进出口数据去哪免费下载?从统计到明细的查找指南
linux·服务器·开发语言·前端·网络·人工智能·自然语言处理
半夜里咳嗽的狼38 分钟前
组件进侧栏就变形?用 CSS Container Queries 把响应式边界收回组件
前端·css
xx24061 小时前
前端性能优化笔记
前端·笔记·性能优化
带娃的IT创业者1 小时前
单文件架构的极致美学:深入解析 Bento 的 HTML 幻灯片技术实现
前端·架构·html·web开发·bento·单文件架构
muddjsv1 小时前
CSS 值的完整计算过程:指定值、计算值、使用值、实际值与动态依赖
前端·css