Ant Design Vue在真实项目中的高频问题与实用解决方案

馨冉 框架 阅读 1,705
赞 22 收藏
二维码
手机扫码查看
反馈

又踩坑了,Table组件里用Popover嵌套Select,点击就消失

今天上线前测一个权限配置页,Ant Design Vue 的 a-table 里每行有个操作列,点「分配角色」弹出一个 a-popover,里面放了个 a-select 多选框。结果一点击 Select 的下拉箭头,Popover 直接关了——连选项都来不及点。

Ant Design Vue在真实项目中的高频问题与实用解决方案

我第一反应是:是不是触发了自动隐藏?赶紧翻 Ant Design Vue 官网文档,查 triggervisibleopen 这几个 prop,试了 trigger="click" + :visible.sync="popoverVisible",手动控制显隐,没用;加 destroyTooltipOnHide?这个是 Tooltip 的,Popover 没这属性;又去扒源码,发现 a-popover 底层用的是 a-tooltip + trigger 事件监听,内部做了 click-outside 关闭逻辑……好家伙,原来问题在这儿。

这里我踩了个坑:以为 Popover 和 Select 是两个独立组件,互不干扰,其实不是。Select 下拉菜单渲染出来后,DOM 节点默认是挂到 body 下的(为了层级和滚动兼容),而 Popover 的「点击外部关闭」逻辑,检测的是当前 Popover 的 content 区域以外的所有点击。但 Select 的下拉面板不在 Popover 内部 DOM 树里,所以点击它,就被当成「外部点击」,直接触发关闭。

折腾了半天发现,这不是 bug,是预期行为。Ant Design Vue 的 Popover 默认就是这么设计的——它只管自己 content 的边界,不管其他组件动态挂载的浮层。

后来试了下发现三种解法:

  • 把 Select 的 getPopupContainer 改成 Popover 的 ref 容器(最正统)
  • 给 Popover 加 overlayClassName 并手动阻止 click 事件冒泡(有点野)
  • 直接换用 a-dropdown + a-menu 模拟 Select 行为(绕开问题)

我选了第一种,因为它是官方支持的方案,也最干净。关键是得让 Select 的下拉菜单,挂载到 Popover 的 DOM 节点内部,这样点击它就不会被判定为「外部」。

具体怎么做?先给 Popover 加个 ref="popoverRef",再在 Select 上用 :getPopupContainer="() => popoverRef.$el"。注意!这里不能写成 getPopupContainer="popoverRef.$el",必须是个函数,否则初始化时 $el 还是 undefined。

还有个细节坑:Popover 默认是用 div 包裹 content 的,但它的 $el 是整个 Popover 组件根节点(含 trigger 元素)。我们真正要的是 content 容器。查了下源码,Ant Design Vue 的 Popover 在渲染时会把 content 渲染进一个 class 为 ant-popover-inner-content 的 div 里。所以更稳妥的做法,是等 Popover 打开后再找这个子节点。

不过实际测试下来,直接用 popoverRef.$el 也能 work,因为 Popover 的 click-outside 判定逻辑,是基于整个组件根节点做事件代理的,只要下拉菜单挂到这个根节点内,就不会触发关闭。我就没再深究那层 inner-content 了,够用就行。

另外别忘了加 placement="bottomRight" 或其它合适位置,不然下拉菜单可能被遮住。我们项目里用了 placement="bottomLeft",配合 offset 微调了 4px 偏移,防止贴边。

最终代码长这样(Vue 3 + Composition API):

<template>
  <a-table :columns="columns" :data-source="dataSource">
    <template #bodyCell="{ column, record }">
      <template v-if="column.key === 'action'">
        <a-popover
          ref="popoverRef"
          trigger="click"
          placement="bottomLeft"
          :overlay-style="{ width: '300px' }"
        >
          <template #title>
            <span>分配角色</span>
          </template>
          <template #content>
            <a-select
              mode="multiple"
              placeholder="请选择角色"
              :options="roleOptions"
              :getPopupContainer="() => popoverRef?.$el || document.body"
              @change="handleRoleChange(record.key, $event)"
              style="width: 100%"
            />
          </template>
          <a-button type="link" size="small">分配</a-button>
        </a-popover>
      </template>
    </template>
  </a-table>
</template>

<script setup>
import { ref } from 'vue'

const popoverRef = ref(null)
const roleOptions = [
  { value: 'admin', label: '管理员' },
  { value: 'editor', label: '编辑' },
  { value: 'viewer', label: '查看员' }
]

const columns = [
  { title: '用户', dataIndex: 'name', key: 'name' },
  { title: '邮箱', dataIndex: 'email', key: 'email' },
  { title: '操作', key: 'action' }
]

const dataSource = [
  { key: '1', name: '张三', email: 'zhangsan@example.com' },
  { key: '2', name: '李四', email: 'lisi@example.com' }
]

const handleRoleChange = (userId, values) => {
  console.log('用户', userId, '分配角色:', values)
  // 这里调接口
  fetch('https://jztheme.com/api/roles/assign', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId, roles: values })
  })
}
</script>

关键点我标一下::getPopupContainer="() => popoverRef?.$el || document.body" 这行一定要有可选链和 fallback,不然服务端渲染或初始加载时会报错。我一开始漏了 || document.body,页面直接白屏,报 Cannot read property '$el' of null,又倒回去加了个空值判断。

顺带提一句,如果你用的是 Vue 2(我们老项目还有几块在用),ref 写法是 ref="popoverRef",取值是 this.$refs.popoverRef.$elgetPopupContainer 一样得写成函数形式。

改完之后确实好了,点击 Select 下拉箭头不会再关闭 Popover。但还有个小问题:当鼠标快速连续点击两次「分配」按钮,Popover 会闪一下(打开又马上关闭)。查了下是点击穿透导致的,Popover 还没完全 render 出来,第二次点击触发了关闭。加个 v-prevent-re-click 指令或者简单节流就能压住,我们暂时没处理,因为线上几乎没人这么点,先放过。

另外补充个原理细节:Ant Design Vue 的 Popover 的 click 触发逻辑,依赖 rc-trigger 这个底层库,它内部用了 addEventListener('click', ...) 绑定在 document 上,然后比对 event.target 是否在 trigger 和 popup 的 DOM 范围内。所以只要 popup 内容(比如 Select 下拉)挂在了 Popover 根节点下,target 就能被识别为「内部」,不会触发关闭。

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案,比如用 Teleport + to 指向 Popover 的某个 slot,或者封装个自定义 hook 来统一处理这类嵌套,欢迎评论区交流。这个技巧后续我们也在表单校验弹窗、操作确认二次确认等场景复用,挺实用的。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论