Skip to content

ExtensionManager 扩展管理组件 ​

ExtensionManager 用于浏览和管理通用 Extension。组件提供标签页切换、筛选、自动分区和分区展开,并自行维护筛选与分区展开状态。当前标签页既可以由父组件控制,也可以由组件维护。安装、卸载、开关状态和详情跳转等操作会触发事件,由应用决定如何更新 Extension 数据或打开后续界面。

概览 ​

适用场景 ​

  • 在一个面板中按标签页浏览 Extension,并按标签和关键词过滤。
  • 为已安装和可安装的 Extension 提供一致的卡片、操作和进度反馈。
  • 在应用页面、Modal 或 Drawer 中嵌入管理内容。组件本身不控制容器的显示、焦点和滚动。

选择建议 ​

ExtensionManager 只处理通用的扩展管理功能。installed 和 tags 用于分区与筛选,不会传入 Card 或 CardGrid 的 item 插槽。添加 MCP、导入 Skill 和查看详情可交给对应的专用 MCP 组件或 Skill 组件,并由应用负责加载、保存和打开界面。完整组合流程见 MCP 与 Skill 扩展管理实践。

快速开始 ​

下面以 MCP 和 Skills 两类 Extension 为例,展示一个可直接运行的管理场景。MCP 已安装,支持启用开关,并可从更多操作中卸载;Skills 可以安装,安装后会移动到“已安装”分区。卸载 MCP 后,条目会回到“可安装”分区,并可重新安装。

loading

常用用法 ​

浏览、筛选和自动分区 ​

组件会读取当前标签页中各条目的 tags,生成标签筛选选项。当前标签页没有可用标签时,组件隐藏标签筛选器,但仍显示关键词搜索。

搜索会忽略首尾空白,并匹配 name 和 description。同时设置标签和关键词时,条目需要同时满足两个条件。每个标签页保存自己的筛选条件。已选择的标签不再存在时,组件会清除该标签条件。

installed: true 的条目归入“已安装”;installed: false 或省略 installed 的条目归入“可安装”。两段始终显示,即使筛选后没有条目;原始数据的顺序会保留。

loading

状态与反馈 ​

进度 ​

Card 的 progress 可以是 0 至 100 的数值,超出范围会被限制;使用 'indeterminate' 表示不确定进度。

loading

空状态 ​

每个当前标签页都有“已安装”和“可安装”两个可折叠分区。没有匹配项时,ExtensionManager 的 empty 插槽接收分区标题;没有可显示的标签页时显示 empty-text。

loading

交互与状态管理 ​

状态所有权 ​

ExtensionManager 同时处理外部数据、可由父组件控制的值,以及组件自身的界面状态。接入时需要先明确每项数据由谁更新。用户点击“安装”、切换开关或点击名称时,组件不会直接修改 tabs 或条目数据。

状态 / 数据管理方更新方式
Extension 数据应用处理 action 后更新 tabs
当前标签页(受控)应用处理 update:active-tab 后更新绑定值
当前标签页(非受控)组件自动更新内部状态
标签筛选和搜索词组件按标签页分别保存筛选条件
分区展开状态组件先更新内部状态,再触发 section-toggle

当前标签页 ​

当前标签页同时支持受控和非受控两种使用方式:

  • 受控方式
    • 当前值来自 active-tab,default-active-tab 不生效。
    • 用户选择标签页时,组件依次触发 update:active-tab 和 tab-change;父组件需要更新 active-tab。
    • active-tab 无效时,组件显示第一个可用标签页并触发 update:active-tab。没有标签页时参数为 undefined。这两种情况都不会触发 tab-change。
  • 非受控方式
    • 当前值由组件保存,初始值来自 default-active-tab;未提供或无效时使用第一个标签页。
    • 用户选择标签页时,组件先更新内部状态,再依次触发 update:active-tab 和 tab-change。
    • tabs 变化导致当前值失效时,组件自动切换到新的第一个标签页并触发 update:active-tab,不触发 tab-change。

非受控模式初始化时,即使 default-active-tab 无效并改用第一个标签页,也不会额外触发 update:active-tab。父组件直接修改受控的 active-tab 也不会触发 tab-change,因为 tab-change 只表示用户选择了标签页。

loading

处理操作事件 ​

快速开始中的 MCP 开关与卸载操作,以及 Skills 安装按钮,展示了应用如何根据 action 更新数据。action 和 name-click 只说明用户执行了什么操作,不表示组件已经修改 tabs。例如,switch action 的 action.checked 是用户选择的新状态。Card 仍会显示传入的 checked,因此应用需要更新对应 action。用户点击安装或卸载按钮时,组件同样只触发事件。操作完成后,应用需要更新 item.installed 和对应 actions,组件随后会按照新的 tabs 重新分区。

分区展开状态按标签页和分区分别保存。组件会先更新展开状态,再触发 section-toggle。事件中的 expanded 是更新后的值,应用不需要再同步这个状态,可以按需监听该事件。

需要把列表事件、MCP/Skill 添加与详情界面、定义存储和用户选项接成一个流程时,参见 MCP 与 Skill 扩展管理实践。

组合与定制 ​

定制 ExtensionManager 区域 ​

header-actions、tab、item 和 empty 分别用于定制头部操作区、标签内容、条目和空状态。定制 tab 时,建议调用作用域中的 select() 选择标签页,不要依赖内部 DOM。

item 插槽会替换默认 Card。ExtensionManager 仍负责外层列表和条目顺序,并使用 item.id 标识条目,但不再提供默认 Card 的 action、name-click、键盘交互和 ARIA 语义。如果自定义条目需要这些功能,开发者需要在插槽内自行实现。插槽中的 item 是 ExtensionCardGridItem,不包含 ExtensionManager 使用的 installed 和 tags。

loading

独立组合 Card 与 CardGrid ​

ExtensionManager.Card 和 ExtensionManager.CardGrid 可以独立使用,也分别以 TrExtensionCard 和 TrExtensionCardGrid 导出。Card 直接接收 name、description 和 actions 等 Prop,不接收 item Prop。CardGrid 则接收带有稳定 id 的条目数组。

Card 会先过滤 hidden: true 的 action,再按数组顺序和 primary-actions-limit 划分主操作与更多操作。disabled action 不会被过滤,仍会占据原来的位置,但用户无法触发它。

primary-action 插槽只会处理位于主操作区域的 custom action。调用 trigger(payload) 后,Card 会触发标准 action 事件,并将参数放入 event.payload。action.data 是 action 自身的数据,不会自动复制到 event.payload。没有提供插槽时,custom action 会显示为普通按钮;进入更多操作菜单的 custom action 也使用默认菜单项。

loading

可访问性与设计约束 ​

键盘与焦点 ​

标签页使用原生按钮和 tab 语义。焦点位于标签页时,使用 ArrowLeft、ArrowRight 循环选择并聚焦相邻标签页,使用 Home 或 End 跳到首个或末个标签页;Enter 和 Space 也可以激活标签页。

Card 名称默认可以点击,并支持 Enter 和 Space。设置 name-clickable="false" 后,名称显示为普通文本,不再进入 Tab 顺序。分区标题、switch、更多操作按钮、筛选控件和清空搜索按钮均保留对应原生控件的键盘行为。系统启用“减少动态效果”后,活动标签指示条不再播放过渡动画。

响应式与长内容 ​

筛选区域根据 ExtensionManager 自身的容器宽度调整布局,不依赖浏览器视口宽度。存在标签筛选器且容器宽度不超过 480px 时,标签选择和搜索框由两列变为单列;没有可用标签时,搜索框始终独占一行。

CardGrid 根据自身容器宽度自动调整列数。每列默认至少为 320px,可以通过 --tr-extension-card-grid-card-min-width 调整。Card 名称和描述过长时会在单行内省略,完整内容保留在 title 中。

loading

API ​

以下组件和类型均从 @opentiny/tiny-robot 导出:

ts
import { TrExtensionCard, TrExtensionCardGrid, TrExtensionManager } from '@opentiny/tiny-robot'
import type {
  ExtensionCardAction,
  ExtensionCardGridItem,
  ExtensionManagerItem,
  ExtensionManagerTab,
} from '@opentiny/tiny-robot'

ExtensionManager Props ​

属性名说明类型默认值必填
tabs标签页及其 Extension 数据;每个 tab 和 item 的 id 应稳定且唯一。ExtensionManagerTab[]—是
active-tab当前标签页,优先于 default-active-tab。无效值会显示第一个可用标签页,并触发 update:active-tab 通知父组件更新当前值。string—否
default-active-tab未设置 active-tab 时使用的初始标签页;无效值会自动改为第一个标签页。string—否
title头部标题;与 header-actions 都未提供时不渲染头部。string—否
empty-text没有可显示的标签页时显示的提示文字。string'暂无内容'否

ExtensionManager Events ​

update:active-tab 和 tab-change 在受控、非受控模式下的更新方式不同,详见“交互与状态管理”。其他事件的状态变化和应用处理方式如下。

事件名触发时机回调参数
update:active-tab用户选择不同标签页,或当前标签页因 active-tab / tabs 变化而改用其他标签页时。非受控模式初始化不会触发该事件。(tabId: string | undefined) => void
tab-change用户选择不同标签页后;在 update:active-tab 之后触发。(event: ExtensionManagerTabChangeEvent) => void
section-toggle组件更新分区展开状态后触发;expanded 是更新后的值,应用不需要再同步该状态。(event: ExtensionManagerSectionToggleEvent) => void
action默认 Card 的操作被触发时触发;组件不会修改 tabs,应用需要处理操作并更新数据。(event: ExtensionManagerActionEvent) => void
name-click默认 Card 名称被鼠标、Enter 或 Space 激活时触发;应用根据该事件决定如何打开详情。(event: ExtensionManagerNameClickEvent) => void

ExtensionManager Slots ​

插槽名用途作用域参数
header-actions扩展标题右侧的操作区。—
tab替换单个标签的内容。建议调用 select() 选择标签页,不要依赖内部 DOM。{ tab: ExtensionManagerTab; active: boolean; select: () => void }
item替换默认 Card。组件仍负责外层列表和条目顺序,并使用 item.id 标识条目;插槽内容需要自行实现 Card 的事件、键盘交互和 ARIA 语义。{ tab: ExtensionManagerTab; sectionKey: ExtensionManagerSectionKey; item: ExtensionCardGridItem; index: number }
empty替换当前标签页内空分区的内容。{ tab: ExtensionManagerTab; sectionKey: ExtensionManagerSectionKey; title: string }

CardGrid Props ​

以下 Card 配置按 item 字段、CardGrid Prop、Card 默认值的顺序生效。0 和 false 都是有效配置,不会继续使用下一层默认值。

属性名说明类型有效默认值必填
items要渲染的卡片数据;每项都必须有稳定且唯一的 id。ExtensionCardGridItem[]—是
empty-textitems 为空且未提供 empty 插槽时的提示。string'暂无内容'否
primary-actions-limititem 未设置同名字段时,每个 Card 放入主操作区域的 action 数量。0 会将所有可见 action 放入更多操作菜单。number1否
name-clickableitem 未设置同名字段时,Card 名称是否可以点击和键盘激活。booleantrue否
overflow-menu-labelitem 未设置同名字段时,更多操作按钮的可访问名称和 title。string'更多操作'否
overflow-menu-placementitem 未设置同名字段时,更多操作菜单的首选位置。空间不足时,组件可能切换到另一方向。ExtensionCardOverflowMenuPlacement'bottom-end'否
overflow-menu-show-iconsitem 未设置同名字段时,更多操作菜单是否显示 action 图标。booleantrue否

CardGrid Events ​

事件名触发时机回调参数
action默认 Card 的 action 被触发时,事件包含所属条目的 itemId。(event: ExtensionCardGridActionEvent) => void
name-click默认 Card 名称被激活时,事件包含所属条目的 itemId。(event: ExtensionCardGridNameClickEvent) => void

CardGrid Slots ​

插槽名用途作用域参数
item替换默认 Card。Grid 仍负责外层列表和条目顺序,并使用 item.id 标识条目;插槽内容不会自动触发 Grid 的 action 或 name-click。{ item: ExtensionCardGridItem; index: number }
empty替换空列表提示,优先于 empty-text。—

Card Props ​

属性名说明类型默认值必填
name卡片名称,也是图片 icon 的替代文本和进度条可访问名称。string—是
description名称下方的单行描述。string—否
icon图片 URL 或 Vue 组件;缺省时显示名称首字母占位。string | Component—否
actionsaction 列表。Card 先过滤 hidden 项,再划分主操作和更多操作。disabled 项保留原排列位置,但不会触发事件。action id 应在同一 Card 内唯一。ExtensionCardAction[][]否
primary-actions-limit可见 action 中放入主操作区域的数量。负数按 0 处理,小数向下取整;0 表示所有可见 action 都进入更多操作菜单。number1否
progress数值进度会限制在 0–100;'indeterminate' 显示不确定进度。number | 'indeterminate'—否
name-clickable是否将名称显示为可通过键盘激活的按钮。booleantrue否
overflow-menu-label溢出菜单按钮的可访问名称和 title。string'更多操作'否
overflow-menu-placement更多操作菜单的首选位置,支持 'bottom-end' 和 'top-end'。首选方向空间不足时,组件可能切换到另一方向。ExtensionCardOverflowMenuPlacement'bottom-end'否
overflow-menu-show-icons是否在更多操作菜单中显示 action 图标。设为 true 且至少一个 action 有图标时,菜单会为所有 action 保留统一的图标列。booleantrue否

Card Events ​

事件名触发时机回调参数
action可见且未禁用的 action 被触发时。switch 的 checked 是新值;只有 primary-action 插槽调用 trigger(payload) 时,参数才会进入事件的 payload。(event: ExtensionCardActionEvent) => void
name-click可点击名称被鼠标、Enter 或 Space 激活时。(event: MouseEvent | KeyboardEvent) => void

Card Slots ​

插槽名用途作用域参数
primary-action自定义主操作区域中 type: 'custom' 的 action。调用 trigger(payload) 会触发标准 action 事件;未提供插槽时显示普通按钮。该插槽不处理更多操作菜单中的 custom action。{ action: ExtensionCardCustomAction; trigger: (payload?: unknown) => void }

Types ​

以下索引按组件分组。所有类型均从 @opentiny/tiny-robot 导出。

Card Types ​

类型名类型或签名说明
ExtensionCardActionunionCard 的 switch、button 和 custom action。
ExtensionCardActionBaseinterface所有 Card action 共有的标识、文案、图标和状态字段。
ExtensionCardSwitchActioninterfacechecked 值由应用提供的 switch action。
ExtensionCardButtonActioninterface普通 button action。
ExtensionCardCustomActioninterface可由 primary-action 插槽渲染的 custom action。
ExtensionCardRenderableActionunion从每种 ExtensionCardAction 中移除 hidden 字段后得到的联合类型。
ExtensionCardActionEventinterfaceCard 触发 action 事件时提供的数据。
ExtensionCardPropsinterfaceCard 的公开 Props 类型。
ExtensionCardEmitsinterfaceCard 的公开事件类型。
ExtensionCardSlotsinterfaceCard 的公开插槽类型。
ExtensionCardOverflowMenuPlacementunion'bottom-end' | 'top-end'。

CardGrid Types ​

类型名类型或签名说明
ExtensionCardGridItemtype带 id 的 Card 数据。
ExtensionCardGridActionEventinterface包含 itemId 和 Card action 的事件数据。
ExtensionCardGridNameClickEventinterface包含 itemId 和 Card name-click 原生事件的数据。
ExtensionCardGridPropsinterfaceCardGrid 的公开 Props 类型。
ExtensionCardGridEmitsinterfaceCardGrid 的公开事件类型。
ExtensionCardGridSlotsinterfaceCardGrid 的公开插槽类型。

ExtensionManager Types ​

类型名类型或签名说明
ExtensionManagerItemtypeExtensionManager 接收的条目;在 Grid item 基础上增加分区和筛选字段。
ExtensionManagerTabinterfaceExtensionManager 的标签页数据。
ExtensionManagerSectionKeyunion'installed' | 'available'。
ExtensionManagerTagOptioninterface组件根据当前数据生成的标签选项结构。
ExtensionManagerTabChangeEventinterface标签切换事件数据。
ExtensionManagerSectionToggleEventinterface分区折叠事件数据。
ExtensionManagerActionEventinterface包含标签页、分区、条目和 Card action 的事件数据。
ExtensionManagerNameClickEventinterface包含标签页、分区、条目和 Card name-click 原生事件的数据。
ExtensionManagerPropsinterfaceExtensionManager 的公开 Props 类型。
ExtensionManagerEmitsinterfaceExtensionManager 的公开事件类型。
ExtensionManagerSlotsinterfaceExtensionManager 的公开插槽类型。

核心数据与事件类型(节选) ​

下面的代码块选择性展开 Card action、条目结构和事件 payload,便于理解它们之间的关系,并不是全部公开类型定义。Props、Events 和 Slots 的完整公开契约见前面的对应 API 表;未展开的类型仍可通过上面的 Types 索引查找。

ts
import type { Component } from 'vue'

export type ExtensionCardOverflowMenuPlacement = 'bottom-end' | 'top-end'

export interface ExtensionCardActionBase {
  id: string
  label: string
  icon?: Component
  hidden?: boolean
  disabled?: boolean
  danger?: boolean
}

export interface ExtensionCardSwitchAction extends ExtensionCardActionBase {
  type: 'switch'
  checked: boolean
}

export interface ExtensionCardButtonAction extends ExtensionCardActionBase {
  type: 'button'
}

export interface ExtensionCardCustomAction extends ExtensionCardActionBase {
  type: 'custom'
  data?: unknown
}

export type ExtensionCardAction = ExtensionCardSwitchAction | ExtensionCardButtonAction | ExtensionCardCustomAction

type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never
export type ExtensionCardRenderableAction = DistributiveOmit<ExtensionCardAction, 'hidden'>

export interface ExtensionCardActionEvent {
  id: string
  type: ExtensionCardAction['type']
  checked?: boolean
  payload?: unknown
}

export interface ExtensionCardProps {
  name: string
  description?: string
  icon?: string | Component
  actions?: ExtensionCardAction[]
  primaryActionsLimit?: number
  progress?: number | 'indeterminate'
  nameClickable?: boolean
  overflowMenuLabel?: string
  overflowMenuPlacement?: ExtensionCardOverflowMenuPlacement
  overflowMenuShowIcons?: boolean
}

export type ExtensionCardGridItem = ExtensionCardProps & { id: string }

export type ExtensionManagerItem = ExtensionCardGridItem & {
  installed?: boolean
  tags?: string[]
}

export type ExtensionManagerSectionKey = 'installed' | 'available'

export interface ExtensionManagerTagOption {
  value: string
  label: string
}

export interface ExtensionManagerTab {
  id: string
  label: string
  items: ExtensionManagerItem[]
}

export interface ExtensionCardGridActionEvent {
  itemId: string
  action: ExtensionCardActionEvent
}

export interface ExtensionCardGridNameClickEvent {
  itemId: string
  event: MouseEvent | KeyboardEvent
}

export interface ExtensionManagerTabChangeEvent {
  tabId: string
}

export interface ExtensionManagerSectionToggleEvent {
  tabId: string
  sectionKey: ExtensionManagerSectionKey
  expanded: boolean
}

export interface ExtensionManagerActionEvent {
  tabId: string
  sectionKey: ExtensionManagerSectionKey
  itemId: string
  action: ExtensionCardActionEvent
}

export interface ExtensionManagerNameClickEvent {
  tabId: string
  sectionKey: ExtensionManagerSectionKey
  itemId: string
  event: MouseEvent | KeyboardEvent
}

ExtensionCardRenderableAction 是从每种 ExtensionCardAction 中移除 hidden 字段后得到的联合类型。ExtensionCardActionBase、ExtensionCardSwitchAction、ExtensionCardButtonAction 和 ExtensionCardCustomAction 也从包根导出,字段与上面的完整 action 定义一致。ExtensionManager 的 action 通过 tabId、sectionKey 和 itemId 标记操作来自哪个条目,嵌套的 action 使用 Card 的完整事件结构。name-click 会提供原生鼠标或键盘事件,应用可以根据该事件打开详情或处理焦点。

CSS Variables ​

下表仅列出当前作为公共定制接口的 CSS Variables。将这些变量设置在 Card、CardGrid 或其父级元素上,即可调整对应样式。未列出的组件内部变量不属于公共定制接口,不应依赖。

变量名说明默认值
--tr-extension-card-bg-colorCard 背景色。var(--tr-container-bg-default-2)
--tr-extension-card-bg-color-hover主操作按钮、默认图标占位、进度条轨道及更多操作按钮悬停背景色。var(--tr-container-bg-hover)
--tr-extension-card-focus-color可点击名称的 focus-visible 描边颜色。var(--tr-text-primary)
--tr-extension-card-icon-color更多操作按钮的三点图标颜色。var(--tr-text-tertiary)
--tr-extension-card-switch-bg-color未选中 switch 轨道颜色。var(--tr-text-disabled)
--tr-extension-card-switch-bg-color-checked选中 switch 轨道颜色。var(--tr-color-primary)
--tr-extension-card-grid-card-min-widthCardGrid 每个响应式网格轨道的最小宽度。320px