Skip to content

Float ​

通用浮窗组件栈系统,不遮罩、不抢焦点、不阻塞背景交互——形态类似 picture-in-picture,可拖拽、可定位、支持多浮窗共存。提供声明式 <Float> 与命令式 float.show() 两种用法。命令式用法需要在应用根部挂载 <FloatOpener>(Nuxt 项目自动处理)。

与 Dialog 的区别

  • Dialog:模态、遮罩、抢焦、Esc / 点遮罩关闭、抢占整个屏幕
  • Float:非模态、无遮罩、不抢焦、不阻塞背景交互、可拖拽到任意位置

演示页已挂载 <FloatOpener />

本页脚本顶部已渲染 <FloatOpener />,点击下方按钮即可打开浮窗。自己接入时必须在应用 root 显式挂一次(Nuxt 通过 @tripo3d/design/nuxt module 自动注册)。

Anatomy ​

  • Float浮窗壳:Teleport + Transition + 拖拽定位 + 默认 header(拖把手 + title + 关闭按钮)。面板外观(背景/圆角/阴影/padding)不内置,业务通过 props.class 自加
  • FloatOpener单例挂载点,在应用 root 挂一次。渲染当前所有活动浮窗
  • float命令式 helper:show / update / close,支持多例栈
  • useFloat浮窗内部子组件读 close 上下文

声明式用法 ​

适合"一进页面就显示一个固定浮窗"或简单受控场景。

v-model:open 控制开关

命令式用法 ​

适合异步流程、跨组件触发、需要动态传参、或同时挂多个浮窗的场景。

float.show

命令式打开,返回 key 可用于 update/close。

挂载 FloatOpener

命令式 API 依赖单例挂载点 <FloatOpener />:

  • Nuxt 项目:通过 @tripo3d/design/nuxt 模块自动注入
  • Vue 3 项目:需在 App.vue 或根布局中手动挂载一次
vue
<!-- App.vue -->
<template>
  <RouterView />
  <FloatOpener />
</template>

在 Float 内部关闭自己 ​

命令式打开的组件可通过 useFloat() 拿到 close 方法:

vue
<script setup lang="ts">
import { Float, useFloat } from '@tripo3d/design';

const { close } = useFloat();
const props = defineProps<{ taskId: string }>();
</script>

<template>
  <Float :title="`Task ${props.taskId}`" class="p-3 rounded-3 bg-gray-4">
    <div class="cancel-drag">
      <button @click="close">关闭自己</button>
    </div>
  </Float>
</template>

useFloat 仅命令式有效

  • 命令式:float.show 注入 opener prop,useFloat().close 走全局栈关掉自己
  • 声明式:opener 为空,useFloat().close 是 noop——请改用 v-model:open 绑定值关闭

业务封装组件不要把 opener 声明为 prop

useFloat() 通过 useAttrs().opener 读取 float-opener 注入的 key。如果你的业务组件把 opener 也声明为 defineProps 字段,attrs 路径会拿不到 opener(被消化为 prop)。两种规避:

  1. 仅在当前组件外层(以 <Float :opener> 包内容)调 useFloat,让 inject 路径接管 —— 推荐
  2. 显式从 props.opener 自己构造 close:() => float.close(props.opener),不依赖 useFloat

多浮窗共存 ​

float.show 每次返回独立 key,多个浮窗可同时存在;关闭一个不影响其它。

同时打开两个浮窗

点击后两个浮窗同时挂载,各自可独立拖动 / 关闭。

⚠ Float 自身不做单例守卫。如果业务需要"再次 show 自动关旧"(类似 imgViewer.show)的语义,需在业务侧自己维护 activeKey 并在 show 前 close 旧的。

自定义 header(slot 三档) ​

Float 默认 header 包含"拖把手 icon + title prop + 关闭按钮"。通过 slot 可三档替换:

vue
<!-- 1. 完全用默认 header,只传 title prop -->
<Float title="设置面板" :width="320" class="p-3 rounded-3 bg-gray-4">
  <div class="cancel-drag">主内容</div>
</Float>

<!-- 2. 仅替换中间标题文本,保留拖把手与关闭按钮 -->
<Float :width="320" class="p-3 rounded-3 bg-gray-4">
  <template #title>
    <span class="c-purple-1">自定义标题样式</span>
  </template>
  <div class="cancel-drag">主内容</div>
</Float>

<!-- 3. 完全覆盖 header(自己画拖把手 + 关闭) -->
<Float :width="320" class="p-3 rounded-3 bg-gray-4">
  <template #header>
    <header class="flex justify-between">
      <span>自定义</span>
      <button class="cancel-drag">×</button>
    </header>
  </template>
  <div class="cancel-drag">主内容</div>
</Float>

面板外观由消费方负责

Float 默认仅渲染 header(拖把手 + title + 关闭按钮),不内置面板背景 / 圆角 / 阴影 / padding。需要带视觉的面板时通过 class 透传到外层 wrapper,header 与 default slot 都会 被包在面板内(wrapper 是 flex flex-col gap-3 的容器,padding/背景作用于整体)。

设计与 Dialog 一致:Float 决定"在哪 + 怎么动 + header 形态",长什么样由业务定义。

cancel-drag 契约 ​

Float 外层 wrapper 默认整个区域可拖。需要"不参与窗口拖动"的子元素(按钮、可点击区、滚动区、文本框)必须加 cancel-drag class。

vue
<Float title="任务面板" class="p-3 rounded-3 bg-gray-4">
  <!-- 整块业务区不参与拖动 -->
  <div class="cancel-drag">
    <button @click="onAction">点我执行</button>
  </div>
</Float>

未加 cancel-drag 的区域按住即开始移动整个浮窗。

API ​

Float Props ​

名称 类型 默认值 说明
openbooleanfalse受控开关;命令式由 float-opener 注入,声明式可 v-model:open
openerstring—浮窗 key,命令式自动注入;声明式留空
titlestring—默认 header 中间的文本(可用 #title slot 覆盖)
widthnumber448浮窗整体宽度(px)
default-position'center' | { x?: number; y?: number }'center'首次出现的初始位置;x/y 缺失轴自动居中
classstring—透传到外层 wrapper,通常用来注入面板视觉(bg/rounded/shadow/padding)

Float Events ​

事件 Payload 说明
update:open(open: boolean)声明式受控模式下的开关变化事件

Float Slots ​

插槽 作用域 说明
default—主内容区,与 header 之间靠外层 gap-3 自动分隔
title—仅替换默认 header 中间文本(保留拖把手与关闭按钮)
header—完全覆盖默认 header,业务自行画拖把手与关闭按钮

float 命令式 API ​

方法签名说明
float.show(component, props?, key?) => key挂载浮窗,返回 key。不传 key 自动生成新 key(多例);传 key 时 upsert——已存在则合并 params 保持实例 mount,不存在则用此 key 新建(单例)
float.update(key, props) => void更新已打开浮窗的 props
float.close(key) => void关闭浮窗(带 150ms 退场动画)

useFloat() ​

字段类型说明
close() => void在命令式打开的组件内部关闭自身(声明式无效,请用 v-model:open)

单例语义:用固定 key 实现 ​

float.show 的第三参数 key 是控制单例的关键:同一 key 的 show 走 upsert 路径,组件实例复用,业务 state 切换通过自身 reactive 通知模板。视觉上"再次 show 原地替换内容",不会触发 fade-out → 重新 fade-in 的抖动。

固定 key upsert 演示

两个按钮共用同一 key,先后点击 'Task A' / 'Task B' 浮窗内容原地切换,实例不卸载;关闭后再点又重新挂载。可观察 header 上 taskId 变化无 fade 抖动。

key 命名约定

固定 key 是进程内全局共享的——所有 float.show(_, _, '相同 key') 都会被识别为同一浮窗。为防止撞名:

  • 库内部保留 @tripo3d/design: 前缀(例如 imgViewer 用 @tripo3d/design:img-viewer)
  • 业务侧用自己的命名空间前缀(@my-app:xxx / feature:xxx / <UUID> 等),不要用裸字符串如 'modal' / 'panel' 这种通用词

无显式 key(多例)的场景不受此约束,key 是自动生成的随机串。

imgViewer.show 就是基于此模式实现的图片预览业务封装,在此基础上额外做了"ObjectURL 资源回收 + 响应式视图暴露 + token 守卫旧 close 句柄"。复杂浮窗业务可参考它。

SSR ​

float.show / close / update 在 SSR 环境(globalThis.window === undefined)下是 noop:show 返回空 key,close/update 直接返回。<FloatOpener /> 与 <Float /> 内部用 <Teleport to="body">,仅在客户端 hydrate 后挂载,不影响 SSR 渲染。

基于 MIT 协议发布(内部使用)