CNCEC_APP/uni_modules/uview-pro/components/u-fab/u-fab.vue

329 lines
10 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
class="u-fab"
:style="$u.toStyle(fabStyle, customStyle)"
:class="[customClass]"
@touchstart="handleTouchstart"
@touchmove.stop.prevent="handleTouchmove"
@touchend="handleTouchend"
>
<view class="u-fab-trigger" id="trigger">
<slot name="trigger">
<u-button
custom-class="u-fab-trigger-btn"
custom-style="width:112rpx;height:112rpx;border-radius:112rpx;"
:type="type"
:disabled="disabled"
:throttle-time="0"
@click="handleBtnClick"
>
<u-icon :name="expansion ? 'close' : 'plus'" size="36rpx"></u-icon>
</u-button>
</slot>
</view>
<view class="u-fab-actions" id="actions" :class="{ 'u-fab-actions__show': expansion }" :style="actionsStyle">
<slot></slot>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'u-fab',
options: {
addGlobalClass: true,
// #ifndef MP-TOUTIAO
virtualHost: true,
// #endif
styleIsolation: 'shared'
}
};
</script>
<script setup lang="ts">
import { directionConfig, FabProps } from './types';
import { computed, getCurrentInstance, onMounted, reactive, ref, useSlots, watch } from 'vue';
import { $u } from '../../';
const props = defineProps(FabProps);
const emit = defineEmits(['trigger']);
const slots = useSlots();
const instance = getCurrentInstance();
const sysInfo = $u.sys();
const dragging = ref(true);
const minLeft = ref(0);
const maxLeft = ref(0);
const minTop = ref(0);
const maxTop = ref(0);
const expansion = ref(false);
const direction = ref(props.direction);
const effectiveWindowHeight = ref(sysInfo.windowHeight);
// #ifdef H5
effectiveWindowHeight.value = sysInfo.windowTop + sysInfo.windowHeight;
// #endif
const position = reactive({
top: 0,
left: 0
});
const actions = ref();
const btnInfo = ref({
width: 56,
height: 56
});
const start = reactive({
x: 0,
y: 0
});
// 计算悬浮按钮样式
const fabStyle = computed(() => {
const style: Record<string, any> = {
transition: dragging.value ? 'none' : 'all 0.3s ease',
zIndex: props.zIndex,
left: `${position.left}px`,
top: `${position.top}px`
};
return style;
});
// 动画样式
const actionsStyle = computed(() => {
const config = directionConfig[direction.value];
const base: Record<string, any> = {
[config.opposite]: '100%',
[config.sizeKey]: '100%',
[config.positionKey]: 0,
flexDirection: config.flexBase,
transition: 'all 0.28s cubic-bezier(0.3, 0, 0.2, 1)'
};
// 展开/收缩动画:根据方向设置初始偏移
const offset = 12; // rpx/px 相对轻微偏移,视觉更顺滑
if (expansion.value) {
base.transform = 'translate3d(0,0,0) scale(1)';
base.opacity = 1;
} else {
// 隐藏时做一个方向上的微位移并缩放以优化动画感
if (direction.value === 'top') base.transform = `translate3d(0, ${offset}px, 0) scale(0.96)`;
else if (direction.value === 'bottom') base.transform = `translate3d(0, -${offset}px, 0) scale(0.96)`;
else if (direction.value === 'left') base.transform = `translate3d(${offset}px, 0, 0) scale(0.96)`;
else base.transform = `translate3d(-${offset}px, 0, 0) scale(0.96)`;
base.opacity = 0;
}
return base;
});
watch(
() => [props.position, props.gap],
() => {
initPosition();
}
);
watch(
() => props.direction,
() => {
if (expansion.value) direction.value = calcDirection();
}
);
// helper支持 gap 为 number 或对象形式
function getGap(side: 'top' | 'left' | 'right' | 'bottom') {
const g = props.gap as any;
if (typeof g === 'number') return g;
if (g && typeof g === 'object' && g[side] !== undefined) return Number(g[side]);
return 0;
}
// 拖动开始事件
function handleTouchstart(e: TouchEvent) {
if (props.disabled || !props.draggable) return;
const touches = e.touches[0];
start.x = touches.clientX;
start.y = touches.clientY;
dragging.value = true;
}
// 拖动移动事件
function handleTouchmove(e: TouchEvent) {
if (props.disabled || !props.draggable) return;
const touches = e.touches[0];
const deltaX = touches.clientX - start.x;
const deltaY = touches.clientY - start.y;
position.left += deltaX;
position.top += deltaY;
start.x = touches.clientX;
start.y = touches.clientY;
// 设置边界,防止拖出边界
position.left = Math.max(minLeft.value, Math.min(maxLeft.value, position.left));
position.top = Math.max(minTop.value, Math.min(maxTop.value, position.top));
}
// 拖动结束事件
function handleTouchend() {
if (props.disabled || !props.draggable) return;
dragging.value = false;
// 如果 autoStick 为 false则释放后不进行自动吸边仅做边界限制
if (props.autoStick === false) {
position.left = Math.max(minLeft.value, Math.min(maxLeft.value, position.left));
position.top = Math.max(minTop.value, Math.min(maxTop.value, position.top));
return;
}
const middle = sysInfo.windowWidth / 2;
const buttonCenter = position.left + btnInfo.value.width / 2;
// 自动吸边,按钮中心位置大于视口的一半时,自动依附在右边,不然就是左边
position.left =
buttonCenter > middle ? sysInfo.windowWidth - btnInfo.value.width - getGap('right') : getGap('left');
if (expansion.value) direction.value = calcDirection();
}
// 按钮点击事件
function handleBtnClick() {
if (slots?.default) {
expansion.value = !expansion.value;
if (expansion.value) direction.value = calcDirection();
} else {
emit('trigger');
}
}
// 计算方向
function calcDirection() {
if (!actions.value) return props.direction;
let dir = props.direction;
const actionsHeight = actions.value?.height || 0;
const actionsWidth = actions.value?.width || 0;
// 菜单展开时,如果位置不够,则反转方向
if (dir === 'top') {
// 按钮上方剩余空间: 按钮顶部 - 顶部边距
if (position.top - minTop.value < actionsHeight) dir = 'bottom';
} else if (dir === 'bottom') {
// 按钮下方剩余空间: 有效窗口高度 - (按钮顶部 + 按钮高 + 边距)
const bottom = effectiveWindowHeight.value - (position.top + btnInfo.value.height + getGap('bottom'));
if (bottom < actionsHeight) dir = 'top';
} else if (dir === 'left') {
// 按钮左侧剩余空间: 有效窗口宽度 - 边距
if (position.left - getGap('left') < actionsWidth) dir = 'right';
} else if (dir === 'right') {
// 按钮右侧剩余空间: 有效窗口宽度 - (按钮左侧 + 按钮宽 + 边距)
const right = sysInfo.windowWidth - (position.left + btnInfo.value.width + getGap('right'));
if (right < actionsWidth) dir = 'left';
}
return dir;
}
// 初始化位置
function initPosition() {
// 根据 props.position 计算初始 left/top
const pos = props.position || 'right-bottom';
const winW = sysInfo.windowWidth;
const winH = effectiveWindowHeight.value;
switch (pos) {
case 'left-top':
position.left = getGap('left');
position.top = getGap('top') + sysInfo.windowTop;
break;
case 'right-top':
position.left = winW - btnInfo.value.width - getGap('right');
position.top = getGap('top') + sysInfo.windowTop;
break;
case 'left-bottom':
position.left = getGap('left');
position.top = winH - btnInfo.value.height - getGap('bottom');
break;
case 'right-bottom':
position.left = winW - btnInfo.value.width - getGap('right');
position.top = winH - btnInfo.value.height - getGap('bottom');
break;
case 'left-center':
position.left = getGap('left');
position.top = Math.round((winH - btnInfo.value.height) / 2);
break;
case 'right-center':
position.left = winW - btnInfo.value.width - getGap('right');
position.top = Math.round((winH - btnInfo.value.height) / 2);
break;
case 'top-center':
position.left = Math.round((winW - btnInfo.value.width) / 2);
position.top = getGap('top') + sysInfo.windowTop;
break;
case 'bottom-center':
position.left = Math.round((winW - btnInfo.value.width) / 2);
position.top = winH - btnInfo.value.height - getGap('bottom');
break;
default:
position.left = winW - btnInfo.value.width - getGap('right');
position.top = winH - btnInfo.value.height - getGap('bottom');
break;
}
}
onMounted(async () => {
btnInfo.value = await $u.getRect('#trigger', instance);
actions.value = await $u.getRect('#actions', instance);
initPosition();
minLeft.value = getGap('left');
minTop.value = getGap('top') + sysInfo.windowTop;
maxLeft.value = sysInfo.windowWidth - btnInfo.value.width - getGap('right');
maxTop.value = effectiveWindowHeight.value - btnInfo.value.height - getGap('bottom');
});
defineExpose({
toggle: handleBtnClick
});
</script>
<style scoped lang="scss">
.u-fab {
position: fixed;
.u-fab-trigger {
:deep(.u-fab-trigger-btn) {
width: 112rpx;
height: 112rpx;
border-radius: 112rpx;
&::after {
border-radius: 112rpx;
}
}
}
.u-fab-actions {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
visibility: hidden;
opacity: 0;
transform-origin: center;
will-change: transform, opacity;
transition: all 0.28s cubic-bezier(0.3, 0, 0.2, 1);
&.u-fab-actions__show {
visibility: visible;
opacity: 1;
}
}
}
</style>