yunshangxie/tuniao-ui/components/tn-fab/tn-fab.vue

524 lines
14 KiB
Vue
Raw Permalink Normal View History

2023-12-25 17:56:30 +08:00
<template>
<view class="tn-fab-class tn-fab" @touchmove.stop.prevent>
<view
class="tn-fab__box"
:class="{'tn-fab--right': left === 'auto'}"
:style="{
left: $tn.string.getLengthUnitValue(fabLeft || left),
right: $tn.string.getLengthUnitValue(fabRight || right),
bottom: $tn.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: $tn.string.getLengthUnitValue(item.textSize || 28)
}"
>{{ item.text || '' }}</view>
<!-- 带图片或者图标时的图片图标信息 -->
<view
class="tn-fab__item__btn"
:class="[!item.bgColor ? backgroundColorClass : '']"
:style="{
width: $tn.string.getLengthUnitValue(width),
height: $tn.string.getLengthUnitValue(height),
lineHeight: $tn.string.getLengthUnitValue(height),
backgroundColor: item.bgColor || backgroundColorStyle || '#01BEFF',
borderRadius: $tn.string.getLengthUnitValue(radius)
}"
>
<!-- 无图片和无图标时只显示文字 -->
<view
v-if="!item.imgUrl && !item.icon"
class="tn-fab__item__btn__title"
:style="{
color: item.textColor || '#fff',
fontSize: $tn.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: $tn.string.getLengthUnitValue(item.iconSize || iconSize || 64)
}"
></view>
<!-- 图片 -->
<image
v-else-if="!item.icon && item.imgUrl"
class="tn-fab__item__btn__image"
:style="{
width: $tn.string.getLengthUnitValue(item.imgWidth || 64),
height: $tn.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: $tn.string.getLengthUnitValue(width),
height: $tn.string.getLengthUnitValue(height),
backgroundColor: backgroundColorStyle || !backgroundColorClass ? '#01BEFF' : '',
color: fontColorStyle || '#fff',
borderRadius: $tn.string.getLengthUnitValue(radius),
zIndex: elZIndex - 1
}"
@tap.stop="fabClick"
>
<slot>
<view class="tn-fab__item__btn__icon" :class="[`tn-icon-${icon}`]" :style="{fontSize: $tn.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.$tn.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>