Files
schisandra-cloud-album-front/src/components/MyUI/FloatButton/FloatButton.vue
landaiqing 33d76461f1 refine MyUI
2024-11-07 21:39:16 +08:00

399 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import {computed, ref, watch} from 'vue';
import Tooltip from '../Tooltip/Tooltip.vue';
import Badge from '../Badge/Badge.vue';
import {useSlotsExist} from '../Utils';
interface Props {
top?: number | string // 按钮定位的上边距,单位 px
bottom?: number | string // 按钮定位的下边距,单位 px
left?: number | string // 按钮定位的左边距,单位 px
right?: 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>(), {
top: undefined,
bottom: 40,
left: undefined,
right: 40,
width: 44,
height: 44,
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>
<component
:is="href ? 'a' : 'div'"
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"
:target="target"
@click="onClick"
@blur="menuTrigger === 'click' ? (showMenu = false) : null"
@mouseenter="menuTrigger === 'hover' ? (showMenu = true) : null"
@mouseleave="menuTrigger === 'hover' ? (showMenu = false) : null"
>
<Tooltip placement="left" 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>
</component>
</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%;
.m-badge-value:not(.only-dot) {
transform: translate(0, 0);
transform-origin: center;
top: -6px;
right: -6px;
}
}
}
}
.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 {
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%;
:deep(.m-badge) {
.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;
:deep(.m-badge) {
.only-dot {
top: 2.3431457505076194px;
right: 2.3431457505076194px;
}
}
& > .float-btn-tooltip {
.float-btn-body {
height: auto;
border-radius: 8px;
}
}
}
</style>