DirectSurface UIDirectSurface UI
开始使用
组件/导航和工作区组件

导航和工作区组件 / COMPONENT

Tabs

RenderTabs 是标签条组件,用于同一区域内多个同级视图的切换。它只负责标签头的展示和交互,不负责内容区创建、销毁、缓存或页面生命周期。

文档 READY示例 1
PUBLIC APIRenderTabs

FUNCTION EXPLORER

可运行示例与完整源码。

这里始终保留组件的主运行入口;文档中的示例用于补充具体功能说明。

LIVE EXAMPLE NAVIGATION
全屏
正在启动 DirectSurface UI 运行时…

Tabs 标签

通用布局能力RenderTabs 实例统一支持 widthheight、min/max、margin 和槽位对齐;单个 tab 的文本、图标和内部 padding 仍由 Tabs 自身测量。详见组件通用布局属性

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

RenderTabs 是标签条组件,用于同一区域内多个同级视图的切换。它只负责标签头的展示和交互,不负责内容区创建、销毁、缓存或页面生命周期。

当前实现支持:选中、hover、关闭按钮、dirty 标记、图标、tooltip、右键上下文菜单回调、拖拽重排回调、overflow 下拉菜单和键盘导航。

API 总览

import {  RenderTabs,  type TabContextMenuHandler,  type TabDragHandler,  type TabItem,} from 'ds-ui'
API 类型 用途
RenderTabs class 标签条,只管理标签头展示、切换、关闭请求、拖拽回调和 overflow 菜单。
TabItem type 标签数据,包含稳定 keylabel、closable、dirty、tooltip 和 icon。
TabContextMenuHandler type 标签右键菜单回调类型。
TabDragHandler type 标签拖拽生命周期回调类型。

最小装配顺序是:准备 TabItem[],创建 RenderTabs({ tabs, activeKey, onTabChange }),再由业务根据 active key 渲染内容区。Tabs 不创建、不销毁、不缓存内容页。

何时使用

使用 Tabs:

  • 一个页面内有多个同级视图。
  • 需要一个轻量标签条,内容区由业务自己切换。
  • 需要可关闭、可拖拽重排或右键菜单的标签头。
  • 标签数量可能超过可视宽度,需要 overflow 菜单。

不要使用:

  • 需要完整 tab 内容生命周期管理,使用 TabView
  • 需要多文档工作区、关闭保护、持久化和文档管理,使用 TabbedWorkspace
  • 需要停靠、拆分、浮动窗口,使用 DockWorkspace
  • 少量状态切换按钮,使用 Toolbar 的 toggle 或 RadioGroup

最小示例

const tabs = new RenderTabs({  tabs: [    { key: 'summary', label: '摘要' },    { key: 'detail', label: '明细' },  ],  activeKey: 'summary',  onTabChange: key => {    state.activeTab = key  },})

业务根据 state.activeTab 决定内容区显示什么。Tabs 不会自动切换子页面。

可关闭和脏标记示例

const documentTabs = new RenderTabs({  tabs: [    { key: 'home', label: '首页', icon: 'window' },    { key: 'record-a', label: '病历 A', icon: 'document', closable: true, dirty: true },    { key: 'record-b', label: '病历 B', icon: 'document', closable: true },  ],  activeKey: 'record-a',  onTabChange: key => {    state.activeTab = key  },  onTabClose: key => {    state.closedTab = key  },})

关闭按钮只负责触发 onTabClose(key),不会自动从 tabs 数组删除该项。业务应在回调中更新 tabs 数据并重新布局或重绘。

拖拽重排示例

const reorderableTabs = new RenderTabs({  tabs: [    { key: 'a', label: 'A' },    { key: 'b', label: 'B' },    { key: 'c', label: 'C' },  ],  activeKey: 'a',  onTabReorder: (key, targetIndex) => {    state.reorderedTab = key    state.reorderTargetIndex = targetIndex  },})

拖拽距离达到阈值后才进入拖拽状态。拖拽过程中组件绘制插入线和 ghost;松开鼠标后触发 onTabReorder(key, targetIndex)。组件不会自己重排 tabs 数组。

右键菜单示例

const tabsWithMenu = new RenderTabs({  tabs: [    { key: 'summary', label: '摘要' },    { key: 'detail', label: '明细', closable: true },  ],  activeKey: 'summary',  onTabContextMenu: (key, position) => {    state.contextTab = key    state.contextX = position.x    state.contextY = position.y    return true  },})

onTabContextMenu 返回 false 时,组件认为没有处理该右键请求,会返回 false 给事件系统。

TabItem

const tabItem: TabItem = {  key: 'record',  label: '病历',  icon: 'document',  closable: true,  dirty: true,  tooltip: '病历文书尚未保存',}
字段 类型 必填 说明
key string 标签稳定标识。用于 active、关闭、拖拽、右键和 overflow 选择。
label string 标签显示文本。
closable boolean 是否显示关闭按钮。关闭按钮在 active、hover 或 close hover 时可见。
dirty boolean 是否显示未保存圆点。
tooltip string hover 标签或关闭按钮时写入组件 tooltip。
icon IconName 标签左侧图标。

RenderTabs 不校验 key 唯一性,但业务必须保证同一 Tabs 内 key 唯一。

构造参数

const appTabs = new RenderTabs({  tabs: [    { key: 'overview', label: '概览', icon: 'window' },    { key: 'settings', label: '设置', icon: 'settings', closable: true },  ],  activeKey: 'overview',  disabled: false,  onTabChange: key => {    state.activeTab = key  },  onTabClose: key => {    state.closedTab = key  },  onTabReorder: (key, targetIndex) => {    state.reorderedTab = `${key}:${targetIndex}`  },})
参数 类型 默认值 说明
tabs TabItem[] 必填 标签数组。构造函数保留该数组引用。
activeKey string 必填 当前活动标签 key。
disabled boolean false 整个标签条是否禁用。禁用后不响应指针和键盘。
onTabChange (key: string) => void undefined 用户激活新标签时触发。直接设置 activeKey 不触发。
onTabClose (key: string) => void undefined 点击关闭按钮,或键盘 Delete/Backspace 关闭 active closable tab 时触发。
onTabReorder (key: string, targetIndex: number) => void undefined 拖拽结束且目标位置变化时触发。
onTabContextMenu (key: string, position: Offset) => boolean | void undefined 右键标签时触发。返回 false 表示未处理。
onTabDragStart (key: string, position: Offset) => void undefined 进入拖拽状态时触发。
onTabDragMove (key: string, position: Offset) => void undefined 拖拽移动时触发。
onTabDragEnd (key: string, position: Offset) => void undefined 拖拽释放时触发。
onTabDragCancel (key: string, position: Offset) => void undefined 拖拽取消时触发。

属性

属性 类型 说明
tabs TabItem[] 当前标签数组。可由业务整体替换。
activeKey string 当前活动标签。setter 会同步 underline 并重绘,但不会触发 onTabChange
disabled boolean 整体禁用。设为 true 会释放拖拽捕获、清理 hover 和拖拽状态。
onTabChange (key) => void | undefined 标签切换回调。
onTabClose (key) => void | undefined 标签关闭回调。
onTabReorder (key, targetIndex) => void | undefined 重排回调。
onTabContextMenu TabContextMenuHandler | undefined 右键回调。
onTabDragStart/Move/End/Cancel TabDragHandler | undefined 拖拽生命周期回调。
isFocused boolean 继承自 FocusableControl

动态更新:

const tabs = new RenderTabs({  tabs: [    { key: 'summary', label: '摘要' },  ],  activeKey: 'summary',}) tabs.tabs = [  { key: 'summary', label: '摘要' },  { key: 'detail', label: '明细', closable: true },  { key: 'logs', label: '日志', dirty: true },]tabs.activeKey = 'detail'tabs.markNeedsLayout()

如果只改变 dirty/closable/tooltip 等不影响顶层宽度的状态,通常重绘即可;改变 label、icon、数量或 active 时建议请求布局。

鼠标行为

操作 行为
主按钮点击标签 请求焦点,激活该标签,触发 onTabChange(key)
主按钮点击关闭按钮 触发 onTabClose(key),不自动删除标签。
主按钮点击 overflow 按钮 打开 overflow 下拉菜单。
hover 标签 更新 hover 状态,并把该标签 tooltip 写入组件 tooltip。
hover 关闭区域 显示关闭按钮 hover 效果。
指针离开 非拖拽时清除 hover 和 tooltip。
拖拽标签 达到 4px 位移阈值后进入拖拽,绘制插入线和 ghost。
pointer cancel 取消拖拽并触发 onTabDragCancel

关闭按钮可见规则:

  • 标签 closable === true
  • 标签是 active,或标签处于 hover,或关闭按钮本身处于 hover。
  • 标签宽度足够容纳关闭按钮。

键盘行为

Tabs 获得焦点后:

按键 行为
ArrowLeft / ArrowUp 激活上一个 tab,循环。
ArrowRight / ArrowDown 激活下一个 tab,循环。
Home 激活第一个 tab。
End 激活最后一个 tab。
Delete / Backspace 如果 active tab 可关闭,触发 onTabClose(activeKey)

键盘切换会触发 onTabChange。禁用状态下不处理键盘。

右键菜单

onContextMenu(position) 会:

  1. 判断 Tabs 是否启用且命中标签区域。
  2. 找到 position 所在的可见 tab。
  3. 调用 onTabContextMenu(key, position)
  4. 如果回调没有返回 false,请求焦点、更新 hover、同步 tooltip 并返回 true

空白 strip、overflow 按钮和被裁剪到 overflow 的 tab 不会触发 tab 右键回调。

拖拽重排

拖拽流程:

  1. pointerDown 命中标签后记录起点和 pointerId。
  2. 移动距离达到 4px 后进入拖拽状态,并触发 onTabDragStart
  3. 拖拽期间持续触发 onTabDragMove,并绘制插入线。
  4. pointerUp 时计算目标下标。
  5. 如果目标下标变化,触发 onTabReorder(key, targetIndex)
  6. 最后触发 onTabDragEnd 并释放 pointer capture。

targetIndex 是“移除当前拖拽 tab 后”的插入位置。组件不会修改 tabs 顺序,业务应在 onTabReorder 中完成数组重排。

Overflow 菜单

当所有 tab 的自然宽度超过组件宽度时:

  • 组件保留 active tab 可见。
  • 其余无法放下的 tab 进入 overflow 菜单。
  • 右侧绘制一个 chevron-down overflow 按钮。
  • 点击 overflow 按钮打开 MenuPopup
  • overflow 中的 active tab 会显示 前缀并禁用。
  • 选择 overflow 菜单中的其他 tab 会激活该 tab 并关闭菜单。

Overflow 菜单由组件内部创建和释放,业务无需直接管理。

布局和绘制

  • 组件没有子 render object,标签、关闭按钮、dirty dot、图标、overflow 按钮和拖拽 ghost 都由自身绘制。
  • 宽度使用父布局给定的 maxWidth;如果是无限宽,默认 400
  • 高度来自主题 panelHeaderHeight
  • 每个 tab 宽度由 label 文本、padding、icon、dirty dot、close button 和间距累加。
  • 文本绘制时会裁剪到当前 tab 可用区域。
  • active tab 绘制顶部 indicator 和 active 背景。
  • hover tab 绘制 hover 背景。
  • focused active tab 绘制 focus ring。
  • active tab 最后绘制,保证它在视觉上压过相邻 inactive tab。

主题来源

样式来自 deriveTabsStyle(theme)

样式 来源语义
barBg chrome strip surface 与标题栏 surface 混合
separator strip 背景与 theme.borderSubtle 混合
indicatorBg theme.accentPrimary
activeTabBg theme.surfaceWindow
height theme.panelHeaderHeight
fontSize/fontFamily 当前主题字体
padding theme.framePadding 下限 6
closeButtonSize fontSize + 2 下限 16

当前 tab 圆角为 0,indicator 高度为 2。

生命周期

  • 构造时创建 underline 动画。
  • activeKey 变化会同步 underline;浏览器支持 animation frame 时可动画过渡。
  • 打开 overflow 会创建 MenuPopup
  • 禁用时会释放 pointer capture 并清理拖拽/hover。
  • dispose() 会关闭 overflow、释放 pointer capture、释放动画并清空所有回调引用。

性能边界

  • layout 和 hit-test 会测量所有可管理 tab 的 label 宽度。
  • 标签数量通常应保持有限。大量文档标签建议使用 TabbedWorkspace 或 DockWorkspace,并配合 overflow/搜索/最近列表。
  • Tabs 只绘制标签条。真正的性能风险通常来自内容区,请由业务决定隐藏页是否保留、销毁或延迟创建。

常见问题

为什么点击关闭后标签还在?

Tabs 只触发 onTabClose(key),不会修改 tabs 数组。业务必须在回调里删除对应 tab 并重新布局。

为什么直接设置 activeKey 没有触发 onTabChange?

activeKey setter 表示外部同步状态,不触发回调。只有用户交互激活标签时才触发 onTabChange

为什么 inactive tab 的关闭按钮有时点不到?

关闭按钮只在 active、hover 或关闭按钮 hover 时显示。未 hover 的 inactive tab 不暴露关闭 hit target。

Tabs 能管理内容页面吗?

不能。Tabs 只管理标签条。需要“tab + 内容 + 生命周期”时使用 TabView、TabbedWorkspace 或 DockWorkspace。

shortcut 或 Ctrl+W 关闭由 Tabs 处理吗?

Tabs 只处理自身获得焦点后的 Delete/Backspace。全局快捷键由业务、CommandShortcutController 或工作区组件处理。

相关文档