<template> <view class="tn-fab-class tn-fab" @touchmove.stop.prevent> <view class="tn-fab__box" :class="{'tn-fab--right': left === 'auto'}" :style="{ left: $t.string.getLengthUnitValue(fabLeft || left), right: $t.string.getLengthUnitValue(fabRight || right), bottom: $t.string.getLengthUnitValue(fabBottom || bottom), zIndex: elZIndex }" > <view v-if="visibleSync" class="tn-fab__btns" :class="[`tn-fab__btns__animation--${animationType}`, showFab ? `tn-fab__btns--visible--${animationType}` : '' ]" > <view v-for="(item, index) in btnList" :key="index" class="tn-fab__item" :class="[ `tn-fab__item__animation--${animationType}`, {'tn-fab__item--left': right === 'auto'} ]" :style="[fabItemStyle(index)]" @tap.stop="handleClick(index)" > <!-- 带图标或者图片时显示的文字信息 --> <view v-if="animationType !== 'around' && (item.imgUrl || item.icon)" :class="[left === 'auto' ? 'tn-fab__item__text--right' : 'tn-fab__item__text--left']" :style="{ color: item.textColor || '#FFF', fontSize: $t.string.getLengthUnitValue(item.textSize || 28) }" >{{ item.text || '' }}</view> <!-- 带图片或者图标时的图片、图标信息 --> <view class="tn-fab__item__btn" :class="[!item.bgColor ? backgroundColorClass : '']" :style="{ width: $t.string.getLengthUnitValue(width), height: $t.string.getLengthUnitValue(height), lineHeight: $t.string.getLengthUnitValue(height), backgroundColor: item.bgColor || backgroundColorStyle || '#01BEFF', borderRadius: $t.string.getLengthUnitValue(radius) }" > <!-- 无图片和无图标时只显示文字 --> <view v-if="!item.imgUrl && !item.icon" class="tn-fab__item__btn__title" :style="{ color: item.textColor || '#fff', fontSize: $t.string.getLengthUnitValue(item.textSize || 28) }" >{{ item.text || '' }}</view> <!-- 图标 --> <view v-if="item.icon" class="tn-fab__item__btn__icon" :class="[`tn-icon-${item.icon}`]" :style="{ color: item.iconColor || '#fff', fontSize: $t.string.getLengthUnitValue(item.iconSize || iconSize || 64) }" ></view> <!-- 图片 --> <image v-else-if="!item.icon && item.imgUrl" class="tn-fab__item__btn__image" :style="{ width: $t.string.getLengthUnitValue(item.imgWidth || 64), height: $t.string.getLengthUnitValue(item.imgHeight || 64), }" :src="item.imgUrl" ></image> </view> </view> </view> <view class="tn-fab__item__btn tn-fab__item__btn--fab" :class="[backgroundColorClass, fontColorClass, {'tn-fab__item__btn--active': showFab}]" :style="{ width: $t.string.getLengthUnitValue(width), height: $t.string.getLengthUnitValue(height), backgroundColor: backgroundColorStyle || !backgroundColorClass ? '#fff' : '', color: fontColorStyle || '#fff', borderRadius: $t.string.getLengthUnitValue(radius), zIndex: elZIndex - 1 }" @tap.stop="fabClick" > <slot> <view class="tn-fab__item__btn__icon" :class="[`tn-icon-${icon}`]" :style="{fontSize: $t.string.getLengthUnitValue(iconSize || 64)}"></view> </slot> </view> </view> <view v-if="visibleSync && showMask" class="tn-fab__mask" :class="{'tn-fab__mask--visible': showFab}" :style="{zIndex: elZIndex - 3}" @tap="clickMask()"></view> </view> </template> <script> import componentsColorMixin from '../../libs/mixin/components_color.js' export default { name: 'tn-fab', mixins: [componentsColorMixin], props: { // 按钮列表 // { // // 背景颜色 // bgColor: '#fff', // // 图片地址 // imgUrl: '', // // 图片宽度 // imgWidth: 60, // // 图片高度 // imgHeight: 60, // // 图标名称 // icon: '', // // 图标尺寸 // iconSize: 60, // // 图标颜色 // iconColor: '#fff', // // 提示文字 // text: '', // // 文字大小 // textSize: 30, // // 字体颜色 // textColor: '#fff' // } btnList: { type: Array, default() { return [] } }, // 自定义悬浮按钮内容 customBtn: { type: Boolean, default: false }, // 悬浮按钮的宽度 width: { type: [String, Number], default: 88 }, // 悬浮按钮的高度 height: { type: [String, Number], default: 88 }, // 图标大小 iconSize: { type: [String, Number], default: 64 }, // 图标名称 icon: { type: String, default: 'open' }, // 按钮圆角 radius: { type: [String, Number], default: '50%' }, // 按钮距离左边的位置 left: { type: [String, Number], default: 'auto' }, // 按钮距离右边的位置 right: { type: [String, Number], default: 'auto' }, // 按钮距离底部的位置 bottom: { type: [String, Number], default: 100 }, // 展示动画类型 up 往上展示 around 环绕 animationType: { type: String, default: 'up' }, // 当动画为圆环时,每个弹出按钮之间的距离, 单位px aroundBtnDistance: { type: Number, default: 10 }, zIndex: { type: Number, default: 0 }, // 显示遮罩 showMask: { type: Boolean, default: true }, // 点击遮罩是否可以关闭 maskCloseable: { type: Boolean, default: true } }, data() { return { showFab: false, visibleSync: false, timer: null, fabLeft: 0, fabRight: 0, fabBottom: 0, fabBtnInfo: { width: 0, height: 0, left: 0, right: 0, bottom: 0 }, systemInfo: { width: 0, height: 0 }, updateProps: false } }, computed: { elZIndex() { return this.zIndex || this.$t.zIndex.fab }, propsData() { return [this.width, this.height, this.left, this.right, this.bottom] }, fabItemStyle() { return (index) => { let style = { zIndex: this.elZIndex - 2 } if (this.animationType === 'up' || !this.showFab) { return style } let base = 1 if (this.left === 'auto') { base = 1 } else if (this.right === 'auto') { base = -1 } style.transform = `rotate(${base * index * 60}deg) translateX(${(this.aroundBtnDistance + this.fabBtnInfo.width) * (-(base))}px)` return style } } }, watch: { propsData() { // 更新按钮信息 this.updateProps = true } }, mounted() { this.$nextTick(() => { this.getFabBtnRectInfo() }) }, beforeDestroy() { if (this.timer) { clearTimeout(this.timer) } }, methods: { // 按钮点击事件 handleClick(index) { this.close() this.$emit('click', {index: index}) }, // 点击悬浮按钮 fabClick() { if (this.showFab) { this.close() } else { // console.log(this.visibleSync); if (this.visibleSync) { this.visibleSync = false return } this.open() } }, // 点击遮罩 clickMask() { if (!this.showMask || !this.maskCloseable) return this.close() }, open() { this.change('visibleSync', 'showFab', true) this.translateFabPosition() }, close() { this.change('showFab', 'visibleSync', false) this.fabLeft = 0 this.fabRight = 0 this.fabBottom = 0 }, // 关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件 // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用 change(param1, param2, status) { this[param1] = status if (status) { // #ifdef H5 || MP this.timer = setTimeout(() => { this[param2] = status this.$emit(status ? 'open' : 'close') clearTimeout(this.timer) }, 10) // #endif // #ifndef H5 || MP this.$nextTick(() => { this[param2] = status this.$emit(status ? 'open' : 'close') }) // #endif } else { this.timer = setTimeout(() => { this[param2] = status this.$emit(status ? 'open' : 'close') clearTimeout(this.timer) }, 250) } }, /******************** 旋转动画相关函数 ********************/ // 获取按钮的信息 async getFabBtnRectInfo() { const systemInfo = uni.getSystemInfoSync() const btnRectInfo = await this._tGetRect('.tn-fab__item__btn--fab') if (!btnRectInfo) { setTimeout(() => { this.getFabBtnRectInfo() }, 10) return } console.log(btnRectInfo); this.systemInfo = { width: systemInfo.windowWidth, height: systemInfo.windowHeight } this.fabBtnInfo.width = btnRectInfo.width this.fabBtnInfo.height = btnRectInfo.height this.fabBtnInfo.left = btnRectInfo.left this.fabBtnInfo.right = btnRectInfo.right this.fabBtnInfo.bottom = btnRectInfo.bottom }, // 更新悬浮按钮的位置 translateFabPosition() { if (this.updateProps) { this.getFabBtnRectInfo() this.updateProps = false } if (this.animationType === 'up') return // 按钮组的宽度 const btnGroupWidth = this.fabBtnInfo.width + this.aroundBtnDistance + 10 // 判断当前按钮是在左边还是右边 if (this.left === 'auto' && btnGroupWidth > this.systemInfo.width - this.fabBtnInfo.right) { // 距离不够需要移动 this.fabRight = btnGroupWidth + 'px' } else if (this.right === 'auto' && btnGroupWidth > this.fabBtnInfo.left) { this.fabLeft = btnGroupWidth + 'px' } if (btnGroupWidth > this.systemInfo.height - this.fabBtnInfo.bottom) { this.fabBottom = btnGroupWidth + 'px' } } } } </script> <style lang="scss" scoped> .tn-fab { &__box { display: flex; justify-content: center; align-items: flex-start; flex-direction: column; position: fixed; transition: all 0.25s ease-in-out; } &--right { align-items: flex-end; } &__btns { transition: all 0.25s cubic-bezier(0,.13,0,1.43); transform-origin: 80% bottom; &__animation--up { opacity: 0; transform: translateY(100%); } &__animation--around { position: absolute; top: 0; left: 0; } &--visible--up { // visibility: visible; opacity: 1; transform: translateY(0); } &--visible--around { // visibility: visible; // opacity: 1; } } &__item { display: flex; justify-content: flex-end; align-items: center; padding-bottom: 20rpx; &__animation--around { position: absolute; top: 0; left: 0; transition: transform 0.25s ease-in-out; transform-origin: 50% 50%; padding-bottom: 0 !important; } &--left { flex-flow: row-reverse; } &__text { &--left { padding-left: 14rpx; } &--right { padding-right: 14rpx; } } &__btn { display: flex; align-items: center; justify-content: center; box-shadow: 0 0 5rpx 2rpx rgba(0, 0, 0, 0.07); transition: all 0.2s linear; &--active { animation-name: fab-button-animation; animation-duration: 0.2s; animation-timing-function: cubic-bezier(0,.13,0,1.43); } &__title { width: 90%; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } &__icon { text-align: center; font-size: 64rpx; } &__image { display: block; } } } &__mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: $tn-mask-bg-color; transition: all 0.2s ease-in-out; opacity: 0; &--visible { opacity: 1; } } } @keyframes fab-button-animation { 0% { transform: scale(0.6); } // 20% { // transform: scale(1.8); // } // 40% { // transform: scale(0.4); // } // 50% { // transform: scale(1.4); // } // 80% { // transform: scale(0.8); // } 100% { transform: scale(1); } } </style>