文章准备就绪
<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>
用法
import { SplitButton, DropdownMenuItem } from '@hina-ui/vue'
主按钮点击触发 click,尾部箭头打开菜单。将附加操作放进 #content,通过 DropdownMenuItem 的 select 处理选择。适合“立即发布 / 保存草稿”“导出 / 选择格式”等有明确默认操作的场景。
<SplitButton menu-label="其他保存方式" @click="save">
保存
<template #content>
<DropdownMenuItem @select="saveCopy">另存副本</DropdownMenuItem>
</template>
</SplitButton>
如果几项操作同样常用,用 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>
尺寸与圆角
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>
加载与禁用
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>
记住所选格式
菜单直接使用现有的 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>
占满宽度
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>
表单提交
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>
从右向左排列
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>
主操作作为链接
as 只改变主按钮。应用内导航传入路由组件,to 等属性透传到该组件;菜单触发器仍是普通按钮。
<SplitButton :as="NuxtLink" :to="localePath('/components/button')" menu-label="相关文档">
查看 Button
<template #content>
<DropdownMenuItem @select="navigateTo(localePath('/components/button-group'))">
查看 ButtonGroup
</DropdownMenuItem>
</template>
</SplitButton>
行为与无障碍
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 | 关闭菜单 |