SplitButton 分裂按钮

将一个常用操作与附加菜单组合在一起,两侧分别响应点击。

文章准备就绪

<script setup lang="ts">
  import { ref } from 'vue'
  import { Clock, FilePen, Send } from '@lucide/vue'
  import { DropdownMenuItem, SplitButton, Stack, Text } from '@hina-ui/vue'

  const result = ref('文章准备就绪')
</script>

<template>
  <Stack align="center" gap="sm">
    <SplitButton label="文章操作" menu-label="其他发布方式" @click="result = '已执行:立即发布'">
      <template #icon><Send /></template>
      立即发布
      <template #content>
        <DropdownMenuItem @select="result = '已执行:保存草稿'">
          <template #icon><FilePen /></template>
          保存草稿
        </DropdownMenuItem>
        <DropdownMenuItem @select="result = '已执行:定时发布'">
          <template #icon><Clock /></template>
          定时发布
        </DropdownMenuItem>
      </template>
    </SplitButton>
    <Text role="status" size="sm" tone="muted">{{ result }}</Text>
  </Stack>
</template>
vue

用法

import { SplitButton, DropdownMenuItem } from '@hina-ui/vue'
ts

主按钮点击触发 click,尾部箭头打开菜单。将附加操作放进 #content,通过 DropdownMenuItem 的 select 处理选择。适合“立即发布 / 保存草稿”“导出 / 选择格式”等有明确默认操作的场景。

<SplitButton menu-label="其他保存方式" @click="save">
  保存
  <template #content>
    <DropdownMenuItem @select="saveCopy">另存副本</DropdownMenuItem>
  </template>
</SplitButton>
vue

如果几项操作同样常用,用 ButtonGroup;只有菜单而没有默认操作,用 DropdownMenu。菜单选择不会自动改变主按钮,是否记住所选操作由调用方决定。

示例

外观与色调

两侧共享 variant 和 tone,连接处不重复圆角。实底、浅底和无底色形态带有分隔线,描边形态共用一条边框。

每种外观都保留独立的主操作和菜单

<script setup lang="ts">
  import { ref } from 'vue'
  import { DropdownMenuItem, Flex, SplitButton, Stack, Text } from '@hina-ui/vue'

  const variants = ['solid', 'soft', 'outline', 'ghost'] as const
  const result = ref('每种外观都保留独立的主操作和菜单')
</script>

<template>
  <Stack align="center" gap="lg">
    <Flex wrap justify="center" gap="lg">
      <SplitButton
        v-for="variant in variants"
        :key="variant"
        :variant="variant"
        :menu-label="`${variant} 的更多操作`"
        @click="result = `${variant}:已保存`"
      >
        {{ variant }}
        <template #content>
          <DropdownMenuItem @select="result = `${variant}:另存副本`">另存副本</DropdownMenuItem>
        </template>
      </SplitButton>
    </Flex>
    <Flex wrap justify="center" gap="lg">
      <SplitButton tone="neutral" menu-label="下载方式" @click="result = '已执行:下载'">
        下载
        <template #content>
          <DropdownMenuItem @select="result = '已执行:复制链接'">复制链接</DropdownMenuItem>
        </template>
      </SplitButton>
      <SplitButton
        tone="danger"
        variant="soft"
        menu-label="归档方式"
        @click="result = '已执行:归档'"
      >
        归档
        <template #content>
          <DropdownMenuItem @select="result = '已执行:移至回收站'">移至回收站</DropdownMenuItem>
        </template>
      </SplitButton>
    </Flex>
    <Text role="status" size="sm" tone="muted">{{ result }}</Text>
  </Stack>
</template>
vue

尺寸与圆角

size 同时控制主按钮和菜单按钮的高度,pill 仅改变整组外侧的圆角。拼接的两侧均不使用按压缩放。

菜单按钮始终与主操作等高

<script setup lang="ts">
  import { ref } from 'vue'
  import { DropdownMenuItem, Flex, SplitButton, Stack, Text } from '@hina-ui/vue'

  const sizes = ['sm', 'md', 'lg'] as const
  const result = ref('菜单按钮始终与主操作等高')
</script>

<template>
  <Stack align="center">
    <Flex wrap align="center" justify="center" gap="lg">
      <SplitButton
        v-for="size in sizes"
        :key="size"
        :size="size"
        :menu-label="`${size} 的更多操作`"
        @click="result = `${size}:已保存`"
      >
        保存
        <template #content>
          <DropdownMenuItem @select="result = `${size}:另存副本`">另存副本</DropdownMenuItem>
        </template>
      </SplitButton>
      <SplitButton
        pill
        variant="outline"
        menu-label="圆角按钮的更多操作"
        @click="result = '已执行:保存'"
      >
        圆角按钮
        <template #content>
          <DropdownMenuItem @select="result = '已执行:另存副本'">另存副本</DropdownMenuItem>
        </template>
      </SplitButton>
    </Flex>
    <Text role="status" size="sm" tone="muted">{{ result }}</Text>
  </Stack>
</template>
vue

加载与禁用

loading 会显示加载指示、禁用两侧并关闭已打开的菜单。disabled 禁用整组;primary-disabled 和 menu-disabled 可以单独禁用一侧,例如不能发布时仍可保存草稿。

可以分别禁用主操作和菜单

<script setup lang="ts">
  import { ref } from 'vue'
  import { DropdownMenuItem, FormField, SplitButton, Stack, Switch, Text } from '@hina-ui/vue'

  const loading = ref(false)
  const disabled = ref(false)
  const primaryDisabled = ref(false)
  const menuDisabled = ref(false)
  const result = ref('可以分别禁用主操作和菜单')
</script>

<template>
  <Stack class="w-full max-w-xs" gap="lg">
    <Stack gap="sm">
      <FormField label="加载中" orientation="horizontal"><Switch v-model="loading" /></FormField>
      <FormField label="全部禁用" orientation="horizontal"><Switch v-model="disabled" /></FormField>
      <FormField label="禁用主操作" orientation="horizontal">
        <Switch v-model="primaryDisabled" />
      </FormField>
      <FormField label="禁用菜单" orientation="horizontal">
        <Switch v-model="menuDisabled" />
      </FormField>
    </Stack>
    <Stack align="center" gap="sm">
      <SplitButton
        :loading="loading"
        :disabled="disabled"
        :primary-disabled="primaryDisabled"
        :menu-disabled="menuDisabled"
        menu-label="保存方式"
        @click="result = '已执行:保存'"
      >
        保存修改
        <template #content>
          <DropdownMenuItem @select="result = '已执行:另存副本'">另存副本</DropdownMenuItem>
        </template>
      </SplitButton>
      <Text role="status" size="sm" tone="muted">{{ result }}</Text>
    </Stack>
  </Stack>
</template>
vue

记住所选格式

菜单直接使用现有的 DropdownMenu 子组件,支持分组、单选、勾选项和子菜单。本例由应用持有导出格式,选择后更新主按钮的文字和操作参数。

选择格式后,主操作使用新的格式

<script setup lang="ts">
  import { ref } from 'vue'
  import { Download } from '@lucide/vue'
  import {
    DropdownMenuLabel,
    DropdownMenuRadioGroup,
    DropdownMenuRadioItem,
    SplitButton,
    Stack,
    Text,
  } from '@hina-ui/vue'

  const format = ref('CSV')
  const result = ref('选择格式后,主操作使用新的格式')
</script>

<template>
  <Stack align="center" gap="sm">
    <SplitButton variant="outline" menu-label="导出格式" @click="result = `已执行:导出 ${format}`">
      <template #icon><Download /></template>
      导出 {{ format }}
      <template #content>
        <DropdownMenuLabel>文件格式</DropdownMenuLabel>
        <DropdownMenuRadioGroup v-model="format">
          <DropdownMenuRadioItem value="CSV">CSV</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="JSON">JSON</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="XLSX">Excel (.xlsx)</DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </template>
    </SplitButton>
    <Text role="status" size="sm" tone="muted">{{ result }}</Text>
  </Stack>
</template>
vue

占满宽度

block 让主按钮填满剩余宽度,箭头仍保持方形;较长的默认文案会截断。class 与 style 都作用于组容器。

主操作伸展,菜单按钮保持方形

<script setup lang="ts">
  import { ref } from 'vue'
  import { Download } from '@lucide/vue'
  import { DropdownMenuItem, SplitButton, Stack, Text } from '@hina-ui/vue'

  const result = ref('主操作伸展,菜单按钮保持方形')
</script>

<template>
  <Stack class="w-full max-w-xs" gap="sm">
    <SplitButton block menu-label="报表的其他操作" @click="result = '已执行:导出完整报表'">
      <template #icon><Download /></template>
      导出本月所有项目的完整运行报表
      <template #content>
        <DropdownMenuItem @select="result = '已执行:仅导出选中项'">仅导出选中项</DropdownMenuItem>
        <DropdownMenuItem @select="result = '已执行:复制报表链接'">复制报表链接</DropdownMenuItem>
      </template>
    </SplitButton>
    <Text role="status" size="sm" tone="muted">{{ result }}</Text>
  </Stack>
</template>
vue

表单提交

type="submit" 仅作用于主按钮。菜单触发器始终为 type="button",打开菜单和选择菜单项都不会意外提交表单。name、value、form 等原生属性透传到主按钮。

本例模拟异步发布。发布经过 Form 校验,保存草稿由菜单单独处理,可以保留未完成的标题。

发布前校验标题,保存草稿可以保留未完成内容。

<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { DropdownMenuItem, Form, FormField, Input, SplitButton, Text } from '@hina-ui/vue'

  const values = reactive({ title: '' })
  const result = ref('')
  const schema = v.object({
    title: v.pipe(v.string(), v.trim(), v.minLength(2, '标题至少需要 2 个字')),
  })

  async function publish(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 800))
    result.value = `已发布:${(data as { title: string }).title}`
  }
  function saveDraft() {
    result.value = `已保存草稿:${values.title.trim() || '未命名文章'}`
  }
</script>

<template>
  <Form
    v-slot="{ submitting }"
    :values="values"
    :rules="schema"
    class="w-full max-w-sm"
    @submit="publish"
  >
    <FormField
      name="title"
      label="文章标题"
      description="发布前校验标题,保存草稿可以保留未完成内容。"
      required
    >
      <Input v-model="values.title" placeholder="填写标题" />
    </FormField>
    <SplitButton type="submit" :loading="submitting" menu-label="其他保存方式" class="self-start">
      发布文章
      <template #content>
        <DropdownMenuItem @select="saveDraft">保存草稿</DropdownMenuItem>
      </template>
    </SplitButton>
    <Text v-if="result" role="status" size="sm" tone="muted">{{ result }}</Text>
  </Form>
</template>
vue

从右向左排列

dir="rtl" 同时控制按钮顺序、连接圆角和菜单方向。菜单对齐整组按钮的逻辑末端。

菜单按钮随书写方向排列

<script setup lang="ts">
  import { ref } from 'vue'
  import { DropdownMenuItem, SplitButton, Stack, Text } from '@hina-ui/vue'

  const result = ref('菜单按钮随书写方向排列')
</script>

<template>
  <Stack align="center" gap="sm">
    <SplitButton dir="rtl" menu-label="خيارات النشر" @click="result = '已执行:发布'">
      نشر المقال
      <template #content>
        <DropdownMenuItem @select="result = '已执行:保存草稿'">حفظ المسودة</DropdownMenuItem>
        <DropdownMenuItem @select="result = '已执行:定时发布'">جدولة النشر</DropdownMenuItem>
      </template>
    </SplitButton>
    <Text role="status" size="sm" tone="muted">{{ result }}</Text>
  </Stack>
</template>
vue

行为与无障碍

  • Tab 分别访问主按钮与菜单按钮。主按钮按 Enter 或空格执行操作,按 ArrowDown 打开菜单;链接形态遵循链接的键盘行为。
  • 菜单按钮按 Enter、空格或 ArrowDown 打开菜单,键盘打开时聚焦首个可用项。菜单沿用 DropdownMenu 的方向键、首尾跳转和字符搜索。
  • Escape 关闭菜单并将焦点返回菜单按钮。点选普通菜单项后同样关闭;需要保留菜单时在该项上使用 @select.prevent。
  • menu-label 应描述附加操作,默认使用当前语言的“更多操作”。label 为整组提供可选的无障碍名称。
  • #icon 和 #trailing 只属于主操作;菜单箭头使用 DisclosureIcon,随打开状态变化。

API

Props

属性
类型
默认值
说明
variant
'solid' | 'soft' | 'outline' | 'ghost'
'solid'
两侧共用的外观
tone
'accent' | 'neutral' | 'danger'
'accent'
两侧共用的色调
size
'xs' | 'sm' | 'md' | 'lg'
'md'
按钮尺寸
block
boolean
false
占满容器宽度
pill
boolean
false
整组外侧使用全圆角
ripple
boolean
true
启用涟漪反馈
loading
boolean
false
显示加载指示并禁用两侧
disabled
boolean
false
禁用两侧
primaryDisabled
boolean
false
仅禁用主操作
menuDisabled
boolean
false
仅禁用菜单
as
string | Component
'button'
主按钮的元素或组件
type
'button' | 'submit' | 'reset'
'button'
主按钮类型
label
string
—
按钮组的无障碍名称
menuLabel
string
当前语言的“更多操作”
菜单按钮及菜单的无障碍名称
modal
boolean
true
菜单打开时限制外部交互
dir
'ltr' | 'rtl'
继承
按钮和菜单的书写方向
side
'top' | 'right' | 'bottom' | 'left'
'bottom'
菜单的首选方向
align
'start' | 'center' | 'end'
'end'
菜单相对于整组的对齐方式
sideOffset
number
8
菜单与按钮组的间距
class
string
—
组容器的类名
style
StyleValue
—
组容器的样式
menuClass
string
—
菜单内容的类名

其他属性和事件监听器(例如 id、aria-label、href、to、keydown)透传到主按钮。

Models

模型
类型
默认值
说明
v-model:open
boolean
false
菜单打开状态;加载或禁用菜单时关闭

Events

事件
参数
说明
click
MouseEvent
主操作点击;菜单不会触发此事件
update:open
boolean
菜单打开状态变化

Slots

插槽
参数
说明
default
—
主按钮文案
icon
—
主按钮前置图标
trailing
—
主按钮尾部内容
content
{ close: () => void }
菜单内容,通常由 DropdownMenuItem 等子组件组成

Expose

方法
类型
说明
focus
() => void
聚焦可用的主按钮
openMenu
() => Promise<void>
聚焦菜单触发器并打开菜单;菜单不可用时无操作
closeMenu
() => void
关闭菜单