pet/uni_modules/uview-next/components/u-fab/u-fab.vue

459 lines
12 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.

<template>
<view
@touchmove.stop.prevent="handleTouchMove"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
class="u-fab"
:class="[`u-fab--${this.position}`]"
:style="[rootStyle]"
@click.stop=""
>
<view :style="{ visibility: inited ? 'visible' : 'hidden' }" class="u-fab__trigger-wrapper">
<slot name="trigger" v-if="$slots.trigger || $slots.$trigger"></slot>
<u-button
v-else
shape="circle"
@click="handleClick"
:type="type"
:disabled="disabled"
:custom-style="triggerStyle"
:icon="isActive ? activeIcon : inactiveIcon"
></u-button>
</view>
<u-transition
v-if="expandable"
:show="isActive"
mode="fade"
:duration="300"
:custom-style="actionsStyle"
>
<view class="u-fab__actions" :class="[`u-fab__actions--${this.fabDirection}`]">
<slot></slot>
</view>
</u-transition>
</view>
</template>
<script>
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom')
// #endif
import props from './props.js';
import mixin from '../../libs/mixin/mixin'
import mpMixin from '../../libs/mixin/mpMixin';
/**
* fab 悬浮按钮
* @description 悬浮动作按钮组件,按下可显示一组动作按钮
* @tutorial https://uview.d3u.cn/components/fab.html
* @property {Boolean} active 是否激活 (默认 false
* @property {String} type 类型,可选值为 primary、success、info、warning、error、default (默认 'primary'
* @property {String} position 悬浮按钮位置,可选值为 left-top、right-top、left-bottom、right-bottom、left-center、right-center、top-center、bottom-center (默认 'right-bottom'
* @property {String} draggable 按钮拖动模式,可选值为 auto自动吸附、free自由拖动、none不可拖动 (默认 'auto'
* @property {String} direction 悬浮按钮菜单弹出方向,可选值为 top、right、bottom、left (默认 'top'
* @property {Boolean} disabled 是否禁用 (默认 false
* @property {String} inactiveIcon 悬浮按钮未展开时的图标 (默认 'plus'
* @property {String} activeIcon 悬浮按钮展开时的图标 (默认 'close'
* @property {String} iconColor 悬浮按钮图标颜色 (默认 '#fff'
* @property {Number} zIndex 自定义悬浮按钮层级 (默认 99
* @property {Object} gap 自定义悬浮按钮与可视区域边缘的间距 (默认 {top: 16, left: 16, right: 16, bottom: 16}
* @property {Boolean} expandable 用于控制点击时是否展开菜单 (默认 true
* @property {Object} customStyle 定义需要用到的外部样式
*
* @event {Function} click expandable 设置为 false 时,点击悬浮按钮触发
* @event {Function} change 菜单状态改变时触发
* @event {Function} update:active 激活状态改变时触发
* @example <u-fab v-model:active="active" :type="type" :position="position" :direction="direction"></u-fab>
*/
export default {
name: "u-fab",
mixins: [mpMixin, mixin, props],
data() {
return {
inited: false, // 是否初始化完成
isActive: false, // 是否激活状态
fabDirection: this.direction, // 实际弹出方向
top: 0,
left: 0,
screen: { width: 0, height: 0 },
fabSize: { width: 56, height: 56 },
bounding: {
minTop: 0,
minLeft: 0,
maxTop: 0,
maxLeft: 0
},
touchOffset: { x: 0, y: 0 }, // 按下时坐标相对于元素的偏移量
attractTransition: false // 是否显示吸附动画
}
},
computed: {
rootStyle() {
const style = {
position: 'fixed',
top: this.$u.addUnit(this.top),
left: this.$u.addUnit(this.left),
zIndex: this.zIndex,
transition: this.attractTransition ? 'all ease 0.3s' : 'none'
}
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
},
triggerStyle() {
return {
width: this.$u.addUnit(this.size),
height: this.$u.addUnit(this.size),
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)'
}
},
actionsStyle() {
const style = {
position: 'absolute',
zIndex: 1,
display: 'flex',
alignItems: 'center'
}
switch (this.fabDirection) {
case 'top':
style.bottom = '100%';
style.left = '50%';
style.transform = 'translateX(-50%)';
style.flexDirection = 'column-reverse';
break;
case 'right':
style.left = '100%';
style.top = '50%';
style.transform = 'translateY(-50%)';
style.flexDirection = 'row';
break;
case 'bottom':
style.top = '100%';
style.left = '50%';
style.transform = 'translateX(-50%)';
style.flexDirection = 'column';
break;
case 'left':
style.right = '100%';
style.top = '50%';
style.transform = 'translateY(-50%)';
style.flexDirection = 'row-reverse';
break;
}
return uni.$u.deepMerge(style, uni.$u.addStyle(this.customStyle))
}
},
watch: {
active: {
handler(newVal) {
this.isActive = newVal
},
immediate: true
},
direction: {
handler(newVal) {
this.fabDirection = newVal
}
},
position() {
this.initPosition()
}
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
// #ifdef VUE3
emits: ['update:active', 'click','change'],
// #endif
methods: {
async init() {
await this.getBounding()
this.initPosition()
this.inited = true
},
async getBounding() {
const windowInfo = this.$u.window()
// 获取触发器尺寸
try {
// #ifndef APP-NVUE
const triggerInfo = await this.$uGetRect('.u-fab__trigger-wrapper')
this.fabSize.width = triggerInfo.width || 56
this.fabSize.height = triggerInfo.height || 56
// #endif
// #ifdef APP-NVUE
this.fabSize.width = 56
this.fabSize.height = 56
// #endif
} catch (error) {
console.log('获取触发器尺寸失败:', error)
}
const { top = 16, left = 16, right = 16, bottom = 16 } = this.gap
this.screen.width = windowInfo.windowWidth
this.screen.height = windowInfo.windowHeight
// #ifdef H5
this.screen.height = windowInfo.windowTop + windowInfo.windowHeight
this.bounding.minTop = windowInfo.windowTop + top
// #endif
// #ifndef H5
this.bounding.minTop = top
// #endif
this.bounding.minLeft = left
this.bounding.maxLeft = this.screen.width - this.fabSize.width - right
this.bounding.maxTop = this.screen.height - this.fabSize.height - bottom
},
initPosition() {
const pos = this.position
const { minLeft, minTop, maxLeft, maxTop } = this.bounding
const centerY = (maxTop + minTop) / 2
const centerX = (maxLeft + minLeft) / 2
switch (pos) {
case 'left-top':
this.top = minTop
this.left = minLeft
break
case 'right-top':
this.top = minTop
this.left = maxLeft
break
case 'left-bottom':
this.top = maxTop
this.left = minLeft
break
case 'right-bottom':
this.top = maxTop
this.left = maxLeft
break
case 'left-center':
this.top = centerY
this.left = minLeft
break
case 'right-center':
this.top = centerY
this.left = maxLeft
break
case 'top-center':
this.top = minTop
this.left = centerX
break
case 'bottom-center':
this.top = maxTop
this.left = centerX
break
}
},
handleTouchStart(e) {
if (this.draggable === 'none') return
const touch = e.touches[0]
this.touchOffset.x = touch.clientX - this.left
this.touchOffset.y = touch.clientY - this.top
this.attractTransition = false
},
handleTouchMove(e) {
if (this.draggable === 'none') return
const touch = e.touches[0]
const { minLeft, minTop, maxLeft, maxTop } = this.bounding
let x = touch.clientX - this.touchOffset.x
let y = touch.clientY - this.touchOffset.y
if (x < minLeft) x = minLeft
else if (x > maxLeft) x = maxLeft
if (y < minTop) y = minTop
else if (y > maxTop) y = maxTop
this.top = y
this.left = x
},
handleTouchEnd() {
const screenCenterX = this.screen.width / 2
const fabCenterX = this.left + this.fabSize.width / 2
this.attractTransition = true
// 自动吸附模式
// 检查指定方向是否有足够空间
const hasEnoughSpace = this.checkDirectionSpace(this.direction)
// 自动计算最佳位置
if (this.draggable === 'auto') {
if (fabCenterX < screenCenterX) {
this.left = this.bounding.minLeft
} else {
this.left = this.bounding.maxLeft
}
}
if (hasEnoughSpace) {
// 使用指定的方向
this.fabDirection = this.direction
this.adjustPositionForDirection(this.direction)
} else {
// 自动计算最佳位置
if (fabCenterX < screenCenterX) {
this.fabDirection = 'right'
} else {
this.fabDirection = 'left'
}
}
},
// 检查指定方向是否有足够空间
checkDirectionSpace(direction) {
const { minLeft, minTop, maxLeft, maxTop } = this.bounding
const fabCenterX = this.left + this.fabSize.width / 2
const fabCenterY = this.top + this.fabSize.height / 2
const minSpaceNeeded = 100 // 菜单需要的最小空间
switch (direction) {
case 'top':
return fabCenterY - minTop >= minSpaceNeeded
case 'bottom':
return maxTop - fabCenterY >= minSpaceNeeded
case 'left':
return fabCenterX - minLeft >= minSpaceNeeded
case 'right':
return maxLeft - fabCenterX >= minSpaceNeeded
default:
return false
}
},
// 根据方向调整位置
adjustPositionForDirection(direction) {
const screenCenterX = this.screen.width / 2
const screenCenterY = this.screen.height / 2
switch (direction) {
case 'top':
case 'bottom':
// 垂直方向时,水平居中或靠近边缘
if (Math.abs(this.left + this.fabSize.width / 2 - screenCenterX) < 50) {
// 如果接近中心,保持中心位置
this.left = screenCenterX - this.fabSize.width / 2
}
break
case 'left':
case 'right':
// 水平方向时,垂直居中或靠近边缘
if (Math.abs(this.top + this.fabSize.height / 2 - screenCenterY) < 50) {
// 如果接近中心,保持中心位置
this.top = screenCenterY - this.fabSize.height / 2
}
break
}
},
// 根据当前位置调整弹出方向(用于自由拖动模式)
adjustDirectionByPosition() {
const screenCenterX = this.screen.width / 2
const screenCenterY = this.screen.height / 2
const fabCenterX = this.left + this.fabSize.width / 2
const fabCenterY = this.top + this.fabSize.height / 2
// 根据位置智能选择弹出方向
const distanceToLeft = fabCenterX - this.bounding.minLeft
const distanceToRight = this.bounding.maxLeft - fabCenterX
const distanceToTop = fabCenterY - this.bounding.minTop
const distanceToBottom = this.bounding.maxTop - fabCenterY
// 找到空间最大的方向
const maxDistance = Math.max(distanceToLeft, distanceToRight, distanceToTop, distanceToBottom)
if (maxDistance === distanceToTop) {
this.fabDirection = 'top'
} else if (maxDistance === distanceToBottom) {
this.fabDirection = 'bottom'
} else if (maxDistance === distanceToLeft) {
this.fabDirection = 'left'
} else {
this.fabDirection = 'right'
}
},
handleClick() {
if (this.disabled) {
return
}
if (!this.expandable) {
this.$emit('click')
return
}
this.isActive = !this.isActive
this.$emit('change')
}
}
}
</script>
<style lang="scss" scoped>
@import '../../libs/css/components.scss';
.u-fab {
position: fixed;
&__trigger-wrapper {
position: relative;
z-index: 2;
}
&__actions {
position: absolute;
z-index: 1;
display: flex;
align-items: center;
&--top {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 8px;
flex-direction: column-reverse;
}
&--right {
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 8px;
flex-direction: row;
}
&--bottom {
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 8px;
flex-direction: column;
}
&--left {
right: 100%;
top: 50%;
transform: translateY(-50%);
margin-right: 8px;
flex-direction: row-reverse;
}
}
}
</style>