效果如下图:在线预览
APIs
FloatButton
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
left | 按钮定位的左边距,单位 px | number | string | undefined |
right | 按钮定位的右边距,单位 px | number | string | 24 |
top | 按钮定位的上边距,单位 px | number | string | undefined |
bottom | 按钮定位的下边距,单位 px | number | string | 48 |
width | 浮动按钮宽度,单位 px | number | string | 40 |
height | 浮动按钮高度,单位 px | number | string | 40 |
type | 浮动按钮类型 | ‘default’ | ‘primary’ | ‘default’ |
shape | 浮动按钮形状 | ‘circle’ | ‘square’ | ‘circle’ |
icon | 浮动按钮图标 | string | slot | undefined |
description | 文字描述信息 | string | slot | undefined |
href | 点击跳转的地址,指定此属性按钮的行为和 a 链接一致 | string | undefined |
target | 相当于 a 标签的 target 属性,href 存在时生效 | ‘self’ | ‘_blank’ | ‘self’ |
menuTrigger | 浮动按钮菜单显示的触发方式 | ‘click’ | ‘hover’ | undefined |
tooltip | 气泡卡片的内容 | sring | slot | undefined |
tooltipProps | Tooltip 组件属性配置,参考 Tooltip Props | object | {} |
badgeProps | 带徽标的浮动按钮(不支持 status 以及相关属性),参考 Badge Props | object | {} |
Events
名称 | 说明 | 类型 |
---|---|---|
click | 点击浮动按钮时的回调 | (e: Event) => void |
openChange | 浮动按钮菜单展开收起时的回调 | (open: boolean) => void |
创建浮动按钮组件FloatButton.vue
其中引入使用了以下组件和工具函数:
- Vue3文字提示(Tooltip)
- Vue3徽标(Badge)
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import Tooltip from '../tooltip'
import Badge from '../badge'
import { useSlotsExist } from '../utils'
interface Props {
left?: number | string // 按钮定位的左边距,单位 px
right?: number | string // 按钮定位的右边距,单位 px
top?: number | string // 按钮定位的上边距,单位 px
bottom?: number | string // 按钮定位的下边距,单位 px
width?: number | string // 浮动按钮宽度,单位 px
height?: number | string // 浮动按钮高度,单位 px
type?: 'default' | 'primary' // 浮动按钮类型
shape?: 'circle' | 'square' // 浮动按钮形状
icon?: string // 浮动按钮图标 string | slot
description?: string // 文字描述信息 string | slot
href?: string // 点击跳转的地址,指定此属性按钮的行为和 a 链接一致
target?: '_self' | '_blank' // 相当于 a 标签的 target 属性,href 存在时生效
menuTrigger?: 'click' | 'hover' // 浮动按钮菜单显示的触发方式
tooltip?: string // 气泡卡片的内容 string | slot
tooltipProps?: object // Tooltip 组件属性配置,参考 Tooltip Props
badgeProps?: object // 带徽标的浮动按钮(不支持 status 以及相关属性),参考 Badge Props
}
const props = withDefaults(defineProps<Props>(), {
left: undefined,
right: 24,
top: undefined,
bottom: 48,
width: 40,
height: 40,
type: 'default',
shape: 'circle',
icon: undefined,
description: undefined,
href: undefined,
target: '_self',
menuTrigger: undefined,
tooltip: undefined,
tooltipProps: () => ({}),
badgeProps: () => ({})
})
const showMenu = ref(false)
const emits = defineEmits(['click', 'openChange'])
const slotsExist = useSlotsExist(['icon', 'description', 'tooltip', 'menu'])
const floatBtnWidth = computed(() => {
if (typeof props.width === 'number') {
return props.width + 'px'
}
return props.width
})
const floatBtnHeight = computed(() => {
if (typeof props.height === 'number') {
return props.height + 'px'
}
return props.height
})
const floatBtnLeft = computed(() => {
if (typeof props.left === 'number') {
return props.left + 'px'
}
return props.left
})
const floatBtnRight = computed(() => {
if (props.left) {
return null
} else {
if (typeof props.right === 'number') {
return props.right + 'px'
}
return props.right
}
})
const floatBtnTop = computed(() => {
if (typeof props.top === 'number') {
return props.top + 'px'
}
return props.top
})
const floatBtnBottom = computed(() => {
if (props.top) {
return null
} else {
if (typeof props.bottom === 'number') {
return props.bottom + 'px'
}
return props.bottom
}
})
const showDescription = computed(() => {
return slotsExist.description || props.description
})
const showTooltip = computed(() => {
return slotsExist.tooltip || props.tooltip
})
watch(showMenu, (to) => {
emits('openChange', to)
})
function onClick(e: Event) {
emits('click', e)
if (props.menuTrigger === 'click' && slotsExist.menu) {
showMenu.value = !showMenu.value
}
}
</script>
<template>
<a
tabindex="0"
class="m-float-btn"
:class="`float-btn-${type} float-btn-${shape}`"
:style="`
--float-btn-width: ${floatBtnWidth};
--float-btn-height: ${floatBtnHeight};
--float-btn-left: ${floatBtnLeft};
--float-btn-right: ${floatBtnRight};
--float-btn-top: ${floatBtnTop};
--float-btn-bottom: ${floatBtnBottom}
`"
:href="href ? href : 'javascript:void(0);'"
:target="href ? target : '_self'"
@click="onClick"
@blur="menuTrigger === 'click' ? (showMenu = false) : null"
@mouseenter="menuTrigger === 'hover' ? (showMenu = true) : null"
@mouseleave="menuTrigger === 'hover' ? (showMenu = false) : null"
>
<Tooltip v-bind="tooltipProps" class="float-btn-tooltip">
<template v-if="showTooltip" #tooltip>
<slot name="tooltip">{{ tooltip }}</slot>
</template>
<Badge v-bind="badgeProps">
<div class="float-btn-body">
<div class="float-btn-content">
<div v-if="slotsExist.icon" class="float-btn-icon">
<Transition name="fade">
<slot v-if="!showMenu" name="icon"></slot>
<svg
v-else
class="close-svg"
focusable="false"
data-icon="close"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
fill-rule="evenodd"
viewBox="64 64 896 896"
>
<path
d="M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z"
></path>
</svg>
</Transition>
</div>
<div v-if="showDescription" class="float-btn-description">
<slot name="description">{{ description }}</slot>
</div>
</div>
</div>
</Badge>
</Tooltip>
<Transition v-show="showMenu" name="move">
<div class="float-btn-menu">
<slot name="menu"></slot>
</div>
</Transition>
</a>
</template>
<style lang="less" scoped>
.fade-move,
.fade-enter-active,
.fade-leave-active {
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-enter-from,
.fade-leave-to {
transform: scale(0.75);
opacity: 0;
}
.fade-leave-active {
position: absolute;
}
.move-enter-active,
.move-leave-active {
transform-origin: 0 0;
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.move-leave-active {
pointer-events: none;
}
.move-enter-from,
.move-leave-to {
transform: translate3d(0, var(--float-btn-height), 0);
transform-origin: 0 0;
opacity: 0;
}
.m-float-btn {
position: fixed;
left: var(--float-btn-left);
right: var(--float-btn-right);
top: var(--float-btn-top);
bottom: var(--float-btn-bottom);
z-index: 99;
font-size: 14px;
color: rgba(0, 0, 0, 0.88);
line-height: 1.5714285714285714;
display: inline-block;
width: var(--float-btn-width);
height: var(--float-btn-height);
cursor: pointer;
outline: none;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
.float-btn-tooltip {
width: 100%;
height: 100%;
:deep(.tooltip-content) {
width: 100%;
height: 100%;
.m-badge {
vertical-align: top;
width: 100%;
height: 100%;
}
}
}
.float-btn-body {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.2s;
.float-btn-content {
overflow: hidden;
text-align: center;
min-height: var(--float-btn-height);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2px 4px;
.float-btn-icon {
text-align: center;
margin: auto;
font-size: 18px;
line-height: 1;
.close-svg {
display: inline-block;
vertical-align: bottom;
}
:deep(svg) {
fill: currentColor;
}
:deep(img) {
vertical-align: bottom;
}
}
}
}
.float-btn-menu {
position: absolute;
bottom: 100%;
display: block;
z-index: -1;
.m-float-btn {
position: static;
}
& > * {
margin-bottom: 16px;
}
}
}
.float-btn-default {
background-color: #ffffff;
transition: background-color 0.2s;
& > .float-btn-tooltip {
.float-btn-body {
background-color: #ffffff;
transition: background-color 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.06);
}
.float-btn-content {
.float-btn-icon {
color: rgba(0, 0, 0, 0.88);
}
.float-btn-description {
display: flex;
align-items: center;
line-height: 16px;
color: rgba(0, 0, 0, 0.88);
font-size: 12px;
}
}
}
}
}
.float-btn-primary {
background-color: @themeColor;
& > .float-btn-tooltip {
.float-btn-body {
background-color: @themeColor;
transition: background-color 0.2s;
&:hover {
background-color: #4096ff;
}
.float-btn-content {
.float-btn-icon {
color: #fff;
}
}
.float-btn-description {
display: flex;
align-items: center;
line-height: 16px;
color: #fff;
font-size: 12px;
}
}
}
}
.float-btn-circle {
border-radius: 50%;
.m-badge {
:deep(.only-dot) {
top: 5.857864376269049px;
right: 5.857864376269049px;
}
}
& > .float-btn-tooltip {
.float-btn-body {
border-radius: 50%;
}
}
}
.float-btn-square {
height: auto;
min-height: var(--float-btn-height);
border-radius: 8px;
& > .float-btn-tooltip {
.float-btn-body {
height: auto;
border-radius: 8px;
}
}
}
</style>
在要使用的页面引入
其中引入使用了以下组件:
- Vue3卡片(Card)
<script setup lang="ts">
import FloatButton from './FloatButton.vue'
import {
GlobalOutlined,
QuestionCircleOutlined,
CustomerServiceOutlined,
StarFilled,
SettingOutlined,
SketchOutlined,
MessageOutlined,
CommentOutlined
} from '@ant-design/icons-vue'
function onClick(e: Event) {
console.log('click', e)
}
function onOpenChange(open: boolean) {
console.log('openChange', open)
}
</script>
<template>
<div>
<h1>{{ $route.name }} {{ $route.meta.title }}</h1>
<h2 class="mt30 mb10">基本使用</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton @click="onClick">
<template #icon>
<GlobalOutlined />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">位置</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton>
<template #icon>
<MessageOutlined />
</template>
</FloatButton>
<FloatButton shape="square" :top="48">
<template #icon>
<CommentOutlined />
</template>
</FloatButton>
<FloatButton type="primary" :left="24">
<template #icon>
<MessageOutlined />
</template>
</FloatButton>
<FloatButton type="primary" shape="square" :left="24" :top="48">
<template #icon>
<CommentOutlined />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">尺寸</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton :width="56" :height="56" :right="120">
<template #icon>
<MessageOutlined style="font-size: 24px" />
</template>
</FloatButton>
<FloatButton type="primary" shape="square" :width="56" :height="56">
<template #icon>
<CommentOutlined style="font-size: 24px" />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">类型</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton :right="80">
<template #icon>
<QuestionCircleOutlined />
</template>
</FloatButton>
<FloatButton type="primary">
<template #icon>
<QuestionCircleOutlined />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">形状</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton type="primary" :right="80">
<template #icon>
<CustomerServiceOutlined />
</template>
</FloatButton>
<FloatButton type="primary" shape="square">
<template #icon>
<CustomerServiceOutlined />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">图标</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton type="primary" :right="80">
<template #icon>
<StarFilled spin style="color: gold" />
</template>
</FloatButton>
<FloatButton shape="square">
<template #icon>
<SettingOutlined style="color: #1677ff" />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">文字描述信息</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton shape="square" description="HELP" :right="136">
<template #icon>
<GlobalOutlined />
</template>
</FloatButton>
<FloatButton shape="square" description="HELP INFO" :right="80" />
<FloatButton type="primary" shape="square" description="客服">
<template #icon>
<CustomerServiceOutlined />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">链接跳转</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton href="https://themusecatcher.github.io/vue-amazing-ui/" :right="80">
<template #icon>
<img style="width: 1em; height: 1em" src="https://themusecatcher.github.io/vue-amazing-ui/amazing-logo.svg" />
</template>
</FloatButton>
<FloatButton
type="primary"
shape="square"
description="CSDN"
href="https://blog.csdn.net/Dandrose"
target="_blank"
/>
</Card>
<h2 class="mt30 mb10">菜单模式</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton shape="square" description="HELP" :right="80" menu-trigger="click" @openChange="onOpenChange">
<template #icon>
<CustomerServiceOutlined />
</template>
<template #menu>
<FloatButton shape="square">
<template #icon>
<MessageOutlined />
</template>
</FloatButton>
<FloatButton>
<template #icon>
<CommentOutlined />
</template>
</FloatButton>
</template>
</FloatButton>
<FloatButton type="primary" menu-trigger="hover" @openChange="onOpenChange">
<template #icon>
<CustomerServiceOutlined />
</template>
<template #menu>
<FloatButton>
<template #icon>
<MessageOutlined />
</template>
</FloatButton>
<FloatButton>
<template #icon>
<CommentOutlined />
</template>
</FloatButton>
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">气泡卡片</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton tooltip="Diamond" :right="80">
<template #icon>
<SketchOutlined />
</template>
</FloatButton>
<FloatButton
type="primary"
tooltip="Diamond"
:tooltip-props="{
bgColor: '#fff',
tooltipStyle: {
fontWeight: 500,
color: 'rgba(0, 0, 0, 0.88)'
}
}"
>
<template #icon>
<SketchOutlined />
</template>
</FloatButton>
</Card>
<h2 class="mt30 mb10">徽标数</h2>
<Card width="50%" style="height: 300px; transform: translate(0)">
<FloatButton shape="circle" :badge-props="{ dot: true }" :right="136">
<template #icon>
<MessageOutlined />
</template>
</FloatButton>
<FloatButton :badge-props="{ value: 5, color: 'blue' }" :bottom="104">
<template #icon>
<CommentOutlined />
</template>
</FloatButton>
<FloatButton :badge-props="{ value: 5 }">
<template #icon>
<CommentOutlined />
</template>
</FloatButton>
<FloatButton :badge-props="{ value: 123 }" :right="80">
<template #icon>
<CommentOutlined />
</template>
</FloatButton>
</Card>
</div>
</template>