--- title: Popover 气泡卡片 description: 浮层基座:12 方位定位、自动翻转、外部点击与 Esc 关闭 --- # Popover 气泡卡片 `Popover` 是**所有浮层的基础**:Tooltip、Dropdown、Select、DatePicker 都建立在它之上。它负责定位、Teleport、关闭时机与滚动跟随,你只需要提供触发器和内容。 ## 何时使用 - 需要在触发器旁展示一段内容(说明、小表单、卡片)时; - 需要容器级别的自定义浮层时。更具体的场景请直接用上层封装:[Tooltip](/ui/overlay/tooltip)、[Dropdown](/ui/overlay/dropdown)、[Select](/ui/form/select)。 ## 基础用法 `#trigger` 是触发器,`#content` 是浮层内容;默认 click 触发、显示箭头。 ## 方位 `placement` 支持 12 个方位:`top` / `bottom` / `left` / `right` 各自带 `-start` / `-end` 对齐。空间不足时**自动翻转到对面**,再不够会沿视口边缘收缩(shift),始终保持 8px 安全边距。 ## 触发方式 | `trigger` | 行为 | | --- | --- | | `click` | 点击触发器切换(默认) | | `hover` | 移入打开(`mouseEnterDelay`)、移出关闭(`mouseLeaveDelay`),移入浮层不会关闭 | | `focus` | 聚焦打开,焦点移出关闭(焦点落在浮层内不关闭) | | `contextmenu` | 右键时在**鼠标位置**打开,越界自动回退 | | `manual` | 组件不接管开关,完全由 `open` 控制 | ## 受控用法 传 `open` 即为受控:所有开关意图都会通过 `update:open` / `openChange` 抛出,你可以在里面做权限校验、埋点,或选择不关闭。 ## 箭头与宽度 `arrow=false` 去掉尖角(菜单类浮层常用);`followTriggerWidth` 让浮层宽度以触发器宽度为下限(表单控件常用)。 ## API ### Props | 名称 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `open` | `boolean` | — | 受控打开状态;不传即非受控 | | `defaultOpen` | `boolean` | `false` | 非受控时的初始状态 | | `trigger` | `'click' \| 'hover' \| 'focus' \| 'contextmenu' \| 'manual'` | `'click'` | 触发方式 | | `placement` | `'top' \| 'top-start' \| … \| 'right-end'` | `'top'` | 期望方位(12 种),越界自动翻转 + 收缩 | | `offset` | `number` | `12` | 与触发器的间距(px) | | `arrow` | `boolean` | `true` | 是否显示箭头 | | `disabled` | `boolean` | `false` | 禁用:不响应触发(受控 `open` 仍可强制打开) | | `mouseEnterDelay` | `number` | `100` | hover 打开延迟(ms) | | `mouseLeaveDelay` | `number` | `100` | hover 关闭延迟(ms) | | `contentClass` | `string` | `''` | 透传到内容盒的 class,供派生组件定制内容样式 | | `followTriggerWidth` | `boolean` | `false` | 浮层宽度以触发器宽度为下限 | | `unwrapped` | `boolean` | `false` | 零包裹模式:把事件注入触发器单个根节点,不再包一层 `` | | `popupContainer` | `string \| HTMLElement \| (() => …)` | ConfigProvider 配置 | 浮层挂载容器,默认 `body` | ### Events | 名称 | 参数 | 说明 | | --- | --- | --- | | `update:open` | `(open: boolean)` | 打开状态变化(受控与非受控都会触发) | | `openChange` | `(open: boolean)` | 同上,语义别名 | ### Slots | 名称 | 参数 | 说明 | | --- | --- | --- | | `trigger` | — | 触发器内容。`unwrapped` 时**要求单根元素** | | `content` | — | 浮层内容 | ### 类型定义 ```ts export type PopoverTrigger = 'click' | 'hover' | 'focus' | 'contextmenu' | 'manual' export type Placement = | 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end' export interface PopoverProps { open?: boolean defaultOpen?: boolean trigger?: PopoverTrigger placement?: Placement offset?: number arrow?: boolean disabled?: boolean mouseEnterDelay?: number mouseLeaveDelay?: number contentClass?: string followTriggerWidth?: boolean unwrapped?: boolean popupContainer?: PopupContainer } ``` ## 实现说明 - **定位**:纯函数 `computePosition` 计算落点(先按方位摆放 → 主轴溢出则翻转 → 最后 clamp 回视口),浮层用 `position: fixed` + `top/left`,因此**不会**受父级 `overflow: hidden` 影响(配合 Teleport); - **跟随**:打开期间监听捕获阶段的 `scroll`(覆盖内部滚动容器)、`resize`,并用 `ResizeObserver` 观察触发器与浮层尺寸,变化时重算(用 rAF 合并); - **层级**:打开时从 z-index 注册表取一个 `popup` 档的值,关闭时回收(见 `z-index` 分层设计),所以弹窗里的下拉也能正确盖在弹窗之上; - `unwrapped` 模式的两个坑(已在实现里处理):① `#trigger` 里再嵌 `` 会产出多层 Fragment,需要循环解包到真实元素;② 必须用渲染函数把注入了事件的 vnode 原样渲染,`` 会丢掉事件监听与指令; - 进出场只过渡 `opacity` / `transform`,**不**过渡 `top/left`(否则打开瞬间会从屏幕左上角滑过来)。