Skip to content

父子组件以及插槽统一范式 ​

本库有两条贯穿全部组件的约定:

  • 由「父组件 + 一组子项」构成的组件(Tabs / Tab、NavigationBar / NavigationItem 等)使用同一套 选中值、子项标识与状态传递方式;
  • 组件的每块内容都可以用属性或插槽描述:简单内容用属性,复杂内容用插槽,插槽优先。

已有父子组件实现:

父组件子项选中值类型
TabsTabstring | number
SelectOptionstring | number
NavigationBarNavigationItemstring | number
NavigationRailNavigationRailItemstring | number
NavigationDrawerNavigationDrawerItemstring | number
SegmentedButtonsSegmentedButtonstring | number
RadioGroupRadiostring | number

新增同类组件(如 Steps / Step)时沿用这套写法,不要另立规则。

属性与插槽:两种写法 ​

同一个位置若同时提供属性和插槽,属性接收纯文本或图标数据,插槽接收任意内容,插槽优先。两者只写一处, 不要既传属性又在插槽里重复同样的内容。

vue
<!-- 简单内容:属性 -->
<Tab label="航班" :icon="flight" value="flights" />
<Card title="本周概览" subtitle="10 月 20 日 – 10 月 26 日">…</Card>

<!-- 富内容:同名插槽 -->
<Tab value="custom">
  <span class="font-medium">航班</span>
  <Badge value="3" label="3 个更新" />
</Tab>
<Card>
  <template #title>本周<b>概览</b></template>
</Card>
组件属性对应插槽
Tab / NavigationItem 等子项label、icon默认插槽(文字区域)
Cardtitle、subtitle#title、#subtitle、默认插槽(正文)
AppBartitle、subtitle默认插槽、#subtitle、#left、#right
Dialogtitle、icon#icon、默认插槽(正文)、footer-*
ListItemlabel、description、icon默认插槽、#leading、#trailing
MenuItemlabel、icon默认插槽、#leading、#trailing、#submenu
Checkbox / Chip / Fablabel默认插槽
IconButtonicon默认插槽、#badge
Button—默认插槽(文字与图标)
InputleadingIcon、trailingIcon#leading、#trailing
Select / Optionlabel、icon默认插槽(选项内容)
Tooltiplabel、title、text、action-label默认插槽(触发元素)、#content、#action
FormItemlabel、description默认插槽(字段控件)

选择原则:

  • 纯文本、单个图标:用属性。组件会处理截断、字号、间距与无障碍名称。
  • 链接、徽标、多段文字、自定义控件:用插槽,排版由使用方负责。
  • 图标优先用 icon 属性;需要不受组件排版限制的尺寸或换用别的图形时,才改用插槽放 <Icon>。
  • 具体组件的插槽表与优先级说明见各组件文档。

选中值是一个模型 ​

父组件用 defineModel 暴露选中值,消费方写 v-model:value。默认值是 0,因此未进行任何交互时首个 子项就是选中态。

vue
<Tabs v-model:value="tab">
  <Tab label="概览" value="overview" />
  <Tab label="规格" value="specs" />
</Tabs>

点击已选中的子项不会重复写模型,也不会触发 update:value。

子项内容:label、icon 与插槽 ​

子项用三种方式描述内容,可以组合:

  • label:纯文本;
  • icon:与 Icon 的 icon 同源,直接传图标数据,渲染在文字或插槽之前;
  • 默认插槽:其它自定义内容,优先于 label——插槽与 label 同时存在时 label 不参与渲染。
vue
<Tab label="航班" :icon="flight" value="flights" />
<Tab label="这行文字会被插槽覆盖" :icon="flight" value="custom">
  <span class="font-medium">插槽内容</span>
</Tab>

图标尺寸由各组件的排版决定(例如 Tab 用 1.25em,NavigationItem 用 1.5em),需要不受限制的图标时 改用插槽放 <Icon>。

Select 多一条约束:收起态的触发器只读选中项的 label,因此用插槽描述 Option 时 label 仍然必须提供, 否则触发器没有文字可显示(回退到 placeholder)。

子项标识:value 或索引 ​

子项用 value 声明标识;省略时使用自己的数字索引(0 起)。索引由父组件按最终渲染顺序注入: 展开 v-for 产生的 Fragment 后逐个 cloneVNode(node, { index }),因此 v-if 增删、v-for 循环都会 重新编号。

vue
<Tabs v-model:value="index">
  <Tab label="第一步" />
  <Tab label="第二步" />
  <Tab label="第三步" />
</Tabs>

索引是内部细节,使用方无需关心。需要跨版本稳定、或与路由 / 数据主键对应的标识时显式传 value。

状态通过 context 传递 ​

父组件把选中值与布局标记放进一个带类型的 InjectionKey context,子组件 inject 并在没有父组件时 回退为 null——子项可以独立渲染,不会因为缺少 context 而报错。

ts
// parent
const model = defineModel<TabsValue>("value", { default: 0 });
provide(tabsContextKey, { model, variant: toRef(props, "variant") });

// child
const tabs = inject(tabsContextKey, null);
const resolvedValue = computed<TabsValue>(() => props.value ?? props.index);

插槽在渲染期读取 ​

父组件不在 setup() 或模块作用域的辅助函数里调用 slots.default(),那样会丢失依赖追踪并产生警告。 需要展开插槽时,把读取放进只在模板中求值的 computed,让依赖在渲染期间正常收集。

类型从包入口导出 ​

选中值类型(TabsValue、NavigationDrawerValue 等)从 src/index.ts 导出,消费方不必深入组件目录。